Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ src/utils/vendor/
# AI tool runtime directories
.agents/
.claude/
.codex/
.omx/
.docs/task/
# Binary / screenshot files (root only)
Expand All @@ -30,3 +29,12 @@ __pycache__/
logs

data
.omc
.codex/*
!.codex/agents/
!.codex/agents/**
!.codex/skills/
!.codex/skills/**
.codex/skills/.system/**
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

skills/.system is being re-ignored despite the surrounding allowlist rules.

Line 38 overrides the prior !.codex/skills/** and prevents tracking .codex/skills/.system/**. If this subtree should be versioned like the rest of skills, this should be negated instead.

Suggested fix
-.codex/skills/.system/**
+!.codex/skills/.system/**
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.codex/skills/.system/**
!.codex/skills/.system/**
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 38, The .gitignore currently re-ignores the
.codex/skills/.system/** subtree which overrides the earlier allowlist
!.codex/skills/** and prevents tracking files under .codex/skills/.system/**;
update the ignore rules so that .codex/skills/.system/** is negated (e.g., add a
negation like !.codex/skills/.system/** or remove the explicit
.codex/skills/.system/** entry) so the .system subtree is treated the same as
the rest of .codex/skills; ensure you keep the existing !.codex/skills/**
allowlist and add the corresponding negation for .codex/skills/.system/** if you
want it versioned.

!.codex/prompts/
!.codex/prompts/**
9 changes: 6 additions & 3 deletions docs/features/kairos.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# KAIROS — 常驻助手模式

> Feature Flag: `FEATURE_KAIROS=1`(及子 Feature)
> 实现状态:核心框架完整,部分子模块为 stub
> 实现状态:核心框架完整,部分子模块为 stub;proactive/sleep 节奏控制已可用
> 引用数:154(全库最大)

## 一、功能概述
Expand Down Expand Up @@ -74,8 +74,9 @@ KAIROS 在系统提示中注入两大段落:

SleepTool 是 KAIROS/Proactive 的节奏控制核心。工具描述让模型理解"休眠"概念:
- 工具名:`Sleep`
- 功能:等待指定时间后响应 tick prompt
- 功能:等待指定时间后响应 tick prompt;若队列出现新工作或 proactive 被关闭,会提前唤醒
- 与 `<tick_tag>` 配合实现心跳式自主工作
- 远程控制 surfaces 可通过 `automation_state` 看到 `standby` / `sleeping` 两种状态

### 3.3 Bridge 集成

Expand Down Expand Up @@ -172,8 +173,10 @@ FEATURE_KAIROS=1 FEATURE_TOKEN_BUDGET=1 bun run dev
| `src/assistant/AssistantSessionChooser.ts` | — | Session 选择 UI(stub) |
| `src/tools/BriefTool/` | — | BriefTool 实现(stub) |
| `src/tools/SleepTool/prompt.ts` | ~30 | SleepTool 工具提示 |
| `src/tools/SleepTool/SleepTool.ts` | ~200 | 休眠/唤醒与 automation metadata |
| `src/services/mcp/channelNotification.ts` | 5 | 频道消息接入(stub) |
| `src/memdir/memdir.ts` | — | 记忆目录管理(stub) |
| `src/constants/prompts.ts:552-554,843-914` | 72 | 系统提示注入 |
| `src/components/tasks/src/tasks/DreamTask/` | 3 | Dream 任务(stub) |
| `src/proactive/index.ts` | — | Proactive 核心(stub,KAIROS 共享) |
| `src/proactive/index.ts` | — | Proactive 核心(KAIROS 共享) |
| `src/utils/sessionState.ts` | — | 向 bridge/CCR 暴露 automation 状态 |
34 changes: 19 additions & 15 deletions docs/features/proactive.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# PROACTIVE — 主动模式

> Feature Flag: `FEATURE_PROACTIVE=1`(与 `FEATURE_KAIROS=1` 共享功能)
> 实现状态:核心模块全部 Stub,布线完整
> 实现状态:核心循环与 SleepTool 已落地,部分外围文档仍在补齐
> 引用数:37

## 一、功能概述
Expand All @@ -21,13 +21,13 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持

| 模块 | 文件 | 状态 | 说明 |
|------|------|------|------|
| 核心逻辑 | `src/proactive/index.ts` | **Stub** | `activateProactive()`、`deactivateProactive()`、`isProactiveActive() => false` |
| 核心逻辑 | `src/proactive/index.ts` | **已实现** | `activateProactive()`、`deactivateProactive()`、`pause/resume`、`nextTickAt` 调度状态 |
| SleepTool 提示 | `src/tools/SleepTool/prompt.ts` | **完整** | 工具提示定义(工具名:`Sleep`) |
| 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` |
| 工具注册 | `src/tools.ts:26-28` | **布线** | SleepTool 动态加载 |
| REPL 集成 | `src/screens/REPL.tsx` | **布线** | tick 驱动逻辑、占位符、页脚 UI |
| REPL 集成 | `src/screens/REPL.tsx` | **已实现** | tick 驱动、standby/sleeping 状态、页脚与 bridge automation metadata 上报 |
| 系统提示 | `src/constants/prompts.ts:860-914` | **完整** | 自主工作行为指令(~55 行详细 prompt) |
| 会话存储 | `src/utils/sessionStorage.ts:4892-4912` | **布线** | tick 消息注入对话流 |
| 远控状态镜像 | `src/utils/sessionState.ts` | **已实现** | 向 remote-control/CCR 暴露 `automation_state` 元数据 |

### 2.2 系统提示内容

Expand All @@ -46,7 +46,7 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
### 2.3 数据流

```
activateProactive() [需要实现]
activateProactive()
Tick 调度器启动
Expand All @@ -62,20 +62,22 @@ Tick 调度器启动
└── 无事可做 → 必须调用 SleepTool
SleepTool 等待 [需要实现]
SleepTool 等待
├── 用户插入新工作 / 队列中有命令 → 立即唤醒
├── proactive 被关闭 → 立即中断
└── 进入休眠时向远端 surfaces 上报 `automation_state = sleeping`
下一个 tick 到达
```

## 三、需要补全的内容
## 三、当前行为补充

| 优先级 | 模块 | 工作量 | 说明 |
|--------|------|--------|------|
| 1 | `src/proactive/index.ts` | 中 | Tick 调度器、activate/deactivate 状态机、pause/resume |
| 2 | `src/tools/SleepTool/SleepTool.ts` | 小 | 工具执行(等待指定时间后触发 tick) |
| 3 | `src/commands/proactive.js` | 小 | `/proactive` 斜杠命令处理器 |
| 4 | `src/hooks/useProactive.ts` | 中 | React hook(REPL 引用但不存在) |
- `standby`:proactive 已开启,当前没有执行中的 turn,且已调度下一个 tick。
- `sleeping`:模型显式调用 `SleepTool` 进入等待窗口。
- remote-control/CCR 通过 `external_metadata.automation_state` 接收这两个状态,用于 Web UI 的 Autopilot 状态显示。
- `SleepTool` 现在不是纯定时器;它会在共享命令队列出现新工作时提前醒来。

## 四、关键设计决策

Expand All @@ -101,9 +103,11 @@ FEATURE_PROACTIVE=1 FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 bun run dev

| 文件 | 职责 |
|------|------|
| `src/proactive/index.ts` | 核心逻辑(stub) |
| `src/proactive/index.ts` | 核心逻辑与 next-tick 状态 |
| `src/tools/SleepTool/prompt.ts` | SleepTool 工具提示 |
| `src/tools/SleepTool/SleepTool.ts` | 休眠/唤醒执行逻辑 |
| `src/constants/prompts.ts:860-914` | 自主工作系统提示 |
| `src/screens/REPL.tsx` | REPL tick 集成 |
| `src/screens/REPL.tsx` | REPL tick 集成与 automation 状态上报 |
| `src/utils/sessionStorage.ts:4892-4912` | Tick 消息注入 |
| `src/utils/sessionState.ts` | bridge/CCR metadata 镜像 |
| `src/components/PromptInput/PromptInputFooterLeftSide.tsx` | 页脚 UI 状态 |
10 changes: 10 additions & 0 deletions docs/features/remote-control-self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ claude bridge
- 查看已注册的运行环境(environment 模式)
- 创建和管理会话
- 实时查看对话消息和工具调用
- 查看 Autopilot 状态(`standby` / `sleeping`)和自动运行指示
- 查看 authoritative task snapshots 驱动的 Tasks 面板
- 审批 Claude Code 的工具权限请求

Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境。
Expand Down Expand Up @@ -215,6 +217,7 @@ Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境
9. 双向通信
CLI ──消息/工具调用结果──► RCS ──► Browser
CLI ◄──权限审批/指令───── RCS ◄──── Browser
CLI ──automation_state / task_state──► RCS ──► Browser

10. 心跳保活(每 20 秒)
CLI ──POST /v1/environments/:id/work/:workId/heartbeat──► RCS
Expand All @@ -224,6 +227,13 @@ Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境

## 故障排查

### Web UI 看不到当前 Autopilot 状态

- `standby`:proactive 已开启,正在等待下一个 tick
- `sleeping`:模型正在 `SleepTool` 等待窗口中

这两个状态通过 worker `external_metadata.automation_state` 上报。如果页面只显示普通 working spinner,优先检查 CLI 和 RCS 之间的 worker metadata PUT 是否成功。

### CLI 无法连接

```
Expand Down
152 changes: 115 additions & 37 deletions packages/builtin-tools/src/tools/SleepTool/SleepTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { z } from 'zod/v4'
import type { ToolResultBlockParam } from 'src/Tool.js'
import { buildTool } from 'src/Tool.js'
import { lazySchema } from 'src/utils/lazySchema.js'
import { notifyAutomationStateChanged } from 'src/utils/sessionState.js'
import { SLEEP_TOOL_NAME, DESCRIPTION, SLEEP_TOOL_PROMPT } from './prompt.js'

const SLEEP_WAKE_CHECK_INTERVAL_MS = 500

const inputSchema = lazySchema(() =>
z.strictObject({
duration_seconds: z
Expand All @@ -19,6 +22,36 @@ type SleepInput = z.infer<InputSchema>

type SleepOutput = { slept_seconds: number; interrupted: boolean }

function isProactiveAutomationEnabled(): boolean {
if (!(feature('PROACTIVE') || feature('KAIROS'))) {
return false
}

const mod =
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
return mod.isProactiveActive()
}

function isProactiveSleepAllowed(): boolean {
if (!(feature('PROACTIVE') || feature('KAIROS'))) {
return true
}

const mod =
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
return mod.isProactiveActive()
}

function hasQueuedWakeSignal(): boolean {
const queue =
require('src/utils/messageQueueManager.js') as typeof import('src/utils/messageQueueManager.js')
return queue.hasCommandsInQueue()
}

function shouldInterruptSleep(): boolean {
return !isProactiveSleepAllowed() || hasQueuedWakeSignal()
}

export const SleepTool = buildTool({
name: SLEEP_TOOL_NAME,
searchHint: 'wait pause sleep rest idle duration timer',
Expand All @@ -42,6 +75,9 @@ export const SleepTool = buildTool({
isReadOnly() {
return true
},
interruptBehavior() {
return 'cancel'
},

userFacingName() {
return SLEEP_TOOL_NAME
Expand All @@ -67,53 +103,84 @@ export const SleepTool = buildTool({
},

async call(input: SleepInput, context) {
// Refuse to sleep when proactive mode is off — prevents the model from
// re-issuing Sleep after an interruption caused by /proactive disable.
if (feature('PROACTIVE') || feature('KAIROS')) {
const mod =
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
if (!mod.isProactiveActive()) {
return {
data: {
slept_seconds: 0,
interrupted: true,
},
}
// Don't enter sleep if proactive was disabled or new work arrived while
// the model was deciding to wait.
if (shouldInterruptSleep()) {
return {
data: {
slept_seconds: 0,
interrupted: true,
},
}
}

const { duration_seconds } = input
const startTime = Date.now()
const sleepUntil = startTime + duration_seconds * 1000

if (isProactiveAutomationEnabled()) {
notifyAutomationStateChanged({
enabled: true,
phase: 'sleeping',
next_tick_at: null,
sleep_until: sleepUntil,
})
}

try {
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(resolve, duration_seconds * 1000)
let timer: ReturnType<typeof setTimeout> | null = null
let wakeCheck: ReturnType<typeof setInterval> | null = null
let settled = false

// Abort via user interrupt
context.abortController.signal.addEventListener(
'abort',
() => {
const cleanup = () => {
if (timer !== null) {
clearTimeout(timer)
clearInterval(proactiveCheck)
reject(new Error('interrupted'))
},
{ once: true },
)

// Poll proactive state — if deactivated mid-sleep, interrupt early
// so the user doesn't have to wait for the full duration.
const proactiveCheck =
feature('PROACTIVE') || feature('KAIROS')
? setInterval(() => {
const mod =
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
if (!mod.isProactiveActive()) {
clearTimeout(timer)
clearInterval(proactiveCheck)
reject(new Error('interrupted'))
}
}, 500)
: (null as unknown as ReturnType<typeof setInterval>)
timer = null
}
if (wakeCheck !== null) {
clearInterval(wakeCheck)
wakeCheck = null
}
context.abortController.signal.removeEventListener('abort', onAbort)
}

const finish = () => {
if (settled) return
settled = true
cleanup()
resolve()
}

const interrupt = () => {
if (settled) return
settled = true
cleanup()
reject(new Error('interrupted'))
}

const onAbort = () => {
interrupt()
}

timer = setTimeout(finish, duration_seconds * 1000)

// Abort via user interrupt
if (context.abortController.signal.aborted) {
interrupt()
return
}
context.abortController.signal.addEventListener('abort', onAbort, {
once: true,
})

// Poll proactive state and the shared command queue so new work can
// wake Sleep without waiting for the full duration.
wakeCheck = setInterval(() => {
if (shouldInterruptSleep()) {
interrupt()
}
}, SLEEP_WAKE_CHECK_INTERVAL_MS)
})
return {
data: {
Expand All @@ -129,6 +196,17 @@ export const SleepTool = buildTool({
interrupted: true,
},
}
} finally {
notifyAutomationStateChanged(
isProactiveAutomationEnabled()
? {
enabled: true,
phase: null,
next_tick_at: null,
sleep_until: null,
}
: null,
)
}
},
})
Loading
Loading