Skip to content

Commit 65b4dc6

Browse files
author
SqlRush
committed
Add daemon tick control
1 parent 06ad0b0 commit 65b4dc6

3 files changed

Lines changed: 84 additions & 5 deletions

File tree

cmd/claude/main.go

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type daemonControlOptions struct {
6868
StatePath string
6969
Status bool
7070
Stop bool
71+
Tick bool
7172
}
7273

7374
type repeatedStringFlag []string
@@ -93,6 +94,7 @@ func run(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int
9394
daemonHeartbeat := flags.Duration("daemon-heartbeat", 5*time.Second, "daemon heartbeat interval")
9495
daemonStatus := flags.Bool("daemon-status", false, "print daemon status and exit")
9596
daemonStop := flags.Bool("daemon-stop", false, "stop the daemon recorded in daemon state")
97+
daemonTick := flags.Bool("daemon-tick", false, "trigger one daemon schedule tick")
9698
daemonStatePath := flags.String("daemon-state", "", "daemon state path")
9799
cwd := flags.String("cwd", "", "working directory")
98100
printMode := flags.Bool("print", false, "print response and exit")
@@ -152,11 +154,12 @@ func run(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int
152154
fmt.Fprintf(stderr, "ccgo: %v\n", err)
153155
return 1
154156
}
155-
if *daemonStatus || *daemonStop {
157+
if *daemonStatus || *daemonStop || *daemonTick {
156158
return runDaemonControl(context.Background(), state, daemonControlOptions{
157159
StatePath: strings.TrimSpace(*daemonStatePath),
158160
Status: *daemonStatus,
159161
Stop: *daemonStop,
162+
Tick: *daemonTick,
160163
}, stdout, stderr)
161164
}
162165
if *daemonMode || *daemonOnce {
@@ -263,8 +266,14 @@ func runChromeNativeHost(stdin io.Reader, stdout io.Writer, stderr io.Writer) in
263266
}
264267

265268
func runDaemonControl(ctx context.Context, state *bootstrap.State, options daemonControlOptions, stdout io.Writer, stderr io.Writer) int {
266-
if options.Status && options.Stop {
267-
fmt.Fprintf(stderr, "ccgo daemon: --daemon-status and --daemon-stop are mutually exclusive\n")
269+
actionCount := 0
270+
for _, enabled := range []bool{options.Status, options.Stop, options.Tick} {
271+
if enabled {
272+
actionCount++
273+
}
274+
}
275+
if actionCount > 1 {
276+
fmt.Fprintf(stderr, "ccgo daemon: daemon control actions are mutually exclusive\n")
268277
return 2
269278
}
270279
statePath, err := resolveDaemonStatePath(state, options.StatePath)
@@ -279,6 +288,13 @@ func runDaemonControl(ctx context.Context, state *bootstrap.State, options daemo
279288
}
280289
return 0
281290
}
291+
if options.Tick {
292+
if err := tickDaemon(ctx, stdout, statePath); err != nil {
293+
fmt.Fprintf(stderr, "ccgo daemon: %v\n", err)
294+
return 1
295+
}
296+
return 0
297+
}
282298
if options.Stop {
283299
if err := stopDaemon(ctx, stdout, statePath); err != nil {
284300
fmt.Fprintf(stderr, "ccgo daemon: %v\n", err)
@@ -383,6 +399,39 @@ func stopDaemon(ctx context.Context, stdout io.Writer, statePath string) error {
383399
return err
384400
}
385401

402+
func tickDaemon(ctx context.Context, stdout io.Writer, statePath string) error {
403+
state, err := daemonpkg.LoadState(statePath)
404+
if err != nil {
405+
return err
406+
}
407+
if state.GeneratedAt == "" {
408+
return fmt.Errorf("daemon state not found: %s", statePath)
409+
}
410+
if strings.TrimSpace(state.Endpoint) == "" {
411+
return fmt.Errorf("daemon endpoint is unavailable in %s", statePath)
412+
}
413+
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
414+
defer cancel()
415+
response, err := postDaemonTick(ctx, state.Endpoint)
416+
if err != nil {
417+
return err
418+
}
419+
lines := []string{
420+
"ccgo daemon tick",
421+
"state_path=" + statePath,
422+
"ok=true",
423+
}
424+
if response.CheckedAt != "" {
425+
lines = append(lines, "checked_at="+response.CheckedAt)
426+
}
427+
lines = append(lines,
428+
fmt.Sprintf("triggered_count=%d", response.TriggeredCount),
429+
fmt.Sprintf("error_count=%d", response.ErrorCount),
430+
)
431+
_, err = fmt.Fprintln(stdout, strings.Join(lines, "\n"))
432+
return err
433+
}
434+
386435
func postDaemonStop(ctx context.Context, endpoint string) (daemonpkg.StopResponse, error) {
387436
req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(endpoint, "/")+"/stop", nil)
388437
if err != nil {
@@ -406,6 +455,29 @@ func postDaemonStop(ctx context.Context, endpoint string) (daemonpkg.StopRespons
406455
return response, nil
407456
}
408457

458+
func postDaemonTick(ctx context.Context, endpoint string) (daemonpkg.TickResponse, error) {
459+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(endpoint, "/")+"/tick", nil)
460+
if err != nil {
461+
return daemonpkg.TickResponse{}, err
462+
}
463+
resp, err := http.DefaultClient.Do(req)
464+
if err != nil {
465+
return daemonpkg.TickResponse{}, err
466+
}
467+
defer resp.Body.Close()
468+
var response daemonpkg.TickResponse
469+
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
470+
return daemonpkg.TickResponse{}, err
471+
}
472+
if resp.StatusCode < 200 || resp.StatusCode > 299 || !response.OK {
473+
if response.Error == "" {
474+
response.Error = resp.Status
475+
}
476+
return daemonpkg.TickResponse{}, errors.New(response.Error)
477+
}
478+
return response, nil
479+
}
480+
409481
func waitForDaemonRuntimeState(ctx context.Context, statePath string, runtimeState string) daemonpkg.State {
410482
deadline := time.NewTimer(time.Second)
411483
defer deadline.Stop()

cmd/claude/main_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,13 @@ func TestRunDaemonServesHealthEndpoint(t *testing.T) {
251251
if !strings.Contains(statusOut.String(), "runtime_state=running") || !strings.Contains(statusOut.String(), "endpoint="+daemonState.Endpoint) {
252252
t.Fatalf("daemon status stdout = %q", statusOut.String())
253253
}
254+
var tickOut, tickErr bytes.Buffer
255+
if code := run([]string{"--daemon-tick", "--daemon-state", statePath}, strings.NewReader(""), &tickOut, &tickErr); code != 0 {
256+
t.Fatalf("daemon tick exit = %d stderr=%s", code, tickErr.String())
257+
}
258+
if !strings.Contains(tickOut.String(), "ccgo daemon tick") || !strings.Contains(tickOut.String(), "error_count=0") {
259+
t.Fatalf("daemon tick stdout = %q", tickOut.String())
260+
}
254261
var stopOut, stopErr bytes.Buffer
255262
if code := run([]string{"--daemon-stop", "--daemon-state", statePath}, strings.NewReader(""), &stopOut, &stopErr); code != 0 {
256263
t.Fatalf("daemon stop exit = %d stderr=%s", code, stopErr.String())

docs/cc-100-roadmap.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
| 文件工具 M5 初版 | 已完成文本版 `Read`、image Read 初版、notebook Read 初版、Read 大文本预算截断/落盘、`Write``Edit`,含读前写、mtime stale guard、`replace_all`、structured diff、`.claude/settings*.json` 写前校验、team-memory secret guard、Read 去重 |
3131
| M6 初始上下文层 | 已完成 CLAUDE.md/memdir 扫描、memory manifest、team-memory secret guard、compact threshold/prompt/runner/boundary plan、conversation auto-compact 接入、失败熔断、microcompact/cache 初版、persistent cached microcompact 初版、cache digest structural/rich-content metadata 覆盖、cache version/TTL/prune、in-memory micro cache prune、memory-cache write-through 到磁盘、atomic cache write、坏缓存默认 fail-open、session memory summary/frontmatter aliases/recall 初版、model-ranked recall session-id selection 和 invalid-selection fallback 初版、recall agent alternate/camel response keys/fenced-prose JSON extraction/scalar id parsing/nested/wrapped/collection-alias selection parsing、resume context model-assisted recall 接入、session memory rollup/prune compaction、rollup archive exclusion/merge、rune-safe rollup truncation、resume context + session memory recall、conversation recall 注入、deterministic/model-backed memory extraction 初版、extraction agent fenced-prose JSON/wrapped facts/provider-style response wrapper/alternate/structured field/nested source object/nested response/fact kind alias parsing、turn-end memory extraction 落盘、prompt history lock/buffered flush/field aliases、official subagent transcript layout、agent metadata sidecar/field aliases、sidechain runtime start/append/finish/cancel/fail 和 parent-chain append/finish 初版、sidechain manager orchestration 初版、sidechain manifest 聚合、sidechain state/list/resume/content-field aliases 初版、sidechain resume context builder、sidechain conversation/content-replacement reconstruction、transcript tail/window/metadata/index loaders、byte-budget transcript tail loader、agent-scoped content replacement metadata/record field-alias loading、session-scoped metadata reappend including AI-title/last-prompt/task-summary、transcript line offset index/window/byte-budget-window/parent-chain/resume/tail/byte-budget-tail loaders、extended transcript metadata entries/type/field aliases、transcript message/session UUID field aliases including top-level `messageUuid`/`messageId`/`id` record IDs and `role`/`entry_type`/`messageType`/`createdAt` timestamp aliases、transcript tombstone metadata delete/relink、transcript resume conversation builder、index 文本预览和 AI-title/last-prompt/task metadata 字段、流式 transcript 搜索、session list pagination/search/title、remote history token refresh、remote history 全量分页抓取/page-field/event-list/records/entries/eventList/sessionEvents/last-id/cursor/event-id/has-next alias/wrapped-data/links/paging/bare-array/keyed event map/connection/eventConnection/sessionEventsConnection wrapper response/edge-cursor fallback/max-pages 截断状态/before_id 续抓、remote event transcript materialization/fallback field fill/去重追加/duplicate parent guard、remote history 一步 sync 到 transcript |
3232
| M7 初始 TUI 层 | 已完成轻量 terminal frame renderer、PromptInput 状态机、history 导航、ctrl-p/ctrl-n history navigation、shift-enter 多行输入、多行 prompt 行内 ctrl-a/ctrl-e/ctrl-u/ctrl-k 和 wrap/render/cursor、共享 kill ring、ctrl-b/ctrl-f/ctrl-u/ctrl-k/ctrl-w 行编辑、alt-b/alt-f/alt-d/alt-backspace word 编辑、ctrl-left/ctrl-right/alt-left/alt-right word motion、ctrl-y yank 和 alt-y yank-pop 初版、reverse-search cursor/word 编辑/kill/yank/yank-pop 初版、ctrl-c interrupt/双击退出事件、ctrl-d delete-forward/空输入双击退出事件、ctrl-l 重绘事件、ctrl-o/ctrl-t 全局切换事件、ctrl-g/ctrl-s/ctrl-x chord chat 事件、reverse-search 状态/渲染/脚本断言/空结果/选择回填/cursor 断言、paste/image hint 输入和 OSC ST/base64 filename 兼容、text/image pasted-content 引用/metadata 脚本断言/提交展开/history entry restoration、SGR mouse 解析、alternate terminal navigation key sequences including modified Home/End/Delete/PageUp/PageDown、滚轮滚动、修饰键滚轮/左键、左键拖动选择、viewport 半页/顶部/底部可配置滚动、viewport 点击选择和 dialog action 点击、focus/blur 事件、resize 视口保持、keybinding resolver/config/chord pending/null-unbind/key/action camelCase alias、JSON config loader 和 focus/mouse/paste/image key name 覆盖、vim insert/normal/j/k/word/WORD/ge/gE/line-local ^/$/0/|/I/A/D/quote/bracket text-object/yank/register/paste/delete/count/replace/undo/find/till/repeat/matching-pair %/dot-repeat/G/gg/toggle/join/open-line/indent/substitute 动作、normal-mode arrow/backspace/delete 映射和 operator linewise/字符范围、REPL screen、permission/task dialog builder、dialog kind/id routing/runtime/status line、runtime 到 REPL screen 的 dialog/status 同步、runtime-aware interaction script runner、prompt text/cursor/expanded/vim mode/register/task state/dialog result/runtime mutation/task bulk-cancel/permission cancel/keybinding mutation/status negative/snapshot negative/screen size/event-sequence/event-count/no-event/dialog-result-count/no-dialog-result 脚本断言、viewport 脚本断言、named-key 脚本输入、script JSON/JSONL/wrapper loader、script file runner 和 runtime/task camel field aliases、stale dialog race guard、cancel active、permission id/all cancellation、queued permission promotion、active task dialog refresh、task lifecycle/bulk-cancel 初版、idempotent alternate screen lifecycle/reset/reassert interactive 初版、mouse/focus/bracketed-paste terminal mode lifecycle/reconciliation、ANSI snapshot 基础、snapshot corpus write/compare/script-file compare/missing-baseline/diff/batch/strict unexpected-baseline 状态、scripted interaction runner/assertions/multi-key/text/paste/image/pasted-content metadata 初版、status/dialog/message components、viewport/selection |
33-
| M10 Agents/tasks/worktree/remote | 已有 Task/TaskOutput/KillTask/SendMessage/TeamCreate/TeamDelete/TeamOutput/TeamSendMessage/TeamDispatch/TeamSchedule/TeamAutoSchedule/TeamCoordinate/ResumeTask/Sleep/Brief/ScheduleCron/RemoteTrigger 入口、sidechain metadata/lifecycle、task progress event、显式与 settings 默认 owned worktree 创建/清理、sparse/symlink settings 应用、`run:true` subagent nested tool loop、agent permission mode/allowlist 过滤,以及 session-scoped team/schedule/remote trigger manifest、daemon heartbeat CLI/state/status/stop 控制面审计、ScheduleCron manual trigger/run_due/turn-start due tick、team coordinator_task_id 元数据、TeamOutput coordinator status、TeamSendMessage target routing、TeamDispatch individualized assignments、TeamSchedule deterministic member assignments、TeamAutoSchedule coordinator briefing + member assignments、coordinator briefing、structured handoff brief、remote trigger injection/event_id dedupe、bridge direct `/remote-trigger` HTTP endpoint、WebSocket `remote_trigger`/`hello`/`health`/`manifest` action 和 remote_trigger/websocket_protocol manifest capability;完整远端连接服务、daemon detached start/restart 托管、多 agent 后台调度循环和模型驱动团队自动调度仍未完成 |
33+
| M10 Agents/tasks/worktree/remote | 已有 Task/TaskOutput/KillTask/SendMessage/TeamCreate/TeamDelete/TeamOutput/TeamSendMessage/TeamDispatch/TeamSchedule/TeamAutoSchedule/TeamCoordinate/ResumeTask/Sleep/Brief/ScheduleCron/RemoteTrigger 入口、sidechain metadata/lifecycle、task progress event、显式与 settings 默认 owned worktree 创建/清理、sparse/symlink settings 应用、`run:true` subagent nested tool loop、agent permission mode/allowlist 过滤,以及 session-scoped team/schedule/remote trigger manifest、daemon heartbeat CLI/state/status/stop/tick 控制面审计、ScheduleCron manual trigger/run_due/turn-start due tick、team coordinator_task_id 元数据、TeamOutput coordinator status、TeamSendMessage target routing、TeamDispatch individualized assignments、TeamSchedule deterministic member assignments、TeamAutoSchedule coordinator briefing + member assignments、coordinator briefing、structured handoff brief、remote trigger injection/event_id dedupe、bridge direct `/remote-trigger` HTTP endpoint、WebSocket `remote_trigger`/`hello`/`health`/`manifest` action 和 remote_trigger/websocket_protocol manifest capability;完整远端连接服务、daemon detached start/restart 托管、多 agent 后台调度循环和模型驱动团队自动调度仍未完成 |
3434
| 全量测试 | 当前 `go test ./...` 通过 |
3535

3636
M10 补充:plugin command/agent 的 allowed tool frontmatter 解析现在只在顶层逗号或空白处分隔,保留括号、方括号和引号内的逗号/空白,避免 `Bash(git commit -m "x,y")` 这类 tool pattern 被误拆。
@@ -91,7 +91,7 @@ M10 补充:conversation runner 在每轮主请求前会执行一次 `ScheduleC
9191

9292
M10 补充:新增 session-scoped `daemon-state.json` 状态契约和 `/status show daemon` 审计 section,可记录 runtime_state、PID、endpoint、started_at、heartbeat_at 和错误,并按 heartbeat 超时把 running 状态判定为 stale;CLI 新增 `--daemon` heartbeat loop 和 `--daemon-once` 单次写入模式,`--daemon` 会启动 loopback `/health`/`/status`/`/tick` HTTP endpoint,定时 tick 和 `POST /tick` 都复用 `ScheduleCron run_due` 执行到期 schedule。完整 daemon 进程管理/远端托管仍未完成。
9393

94-
M10 补充:daemon loopback endpoint 新增 `POST /stop`,CLI 新增 `--daemon-status``--daemon-stop``--daemon-state <path>`,可基于 session-scoped state 文件查询 runtime_state/endpoint/heartbeat 并优雅停止正在运行的 daemon;完整 detached start/restart、跨 session discovery 和远端托管仍未完成。
94+
M10 补充:daemon loopback endpoint 新增 `POST /stop`,CLI 新增 `--daemon-status``--daemon-stop``--daemon-tick``--daemon-state <path>`,可基于 session-scoped state 文件查询 runtime_state/endpoint/heartbeat、手动触发一次 schedule due tick,并优雅停止正在运行的 daemon;完整 detached start/restart、跨 session discovery 和远端托管仍未完成。
9595

9696
M10 补充:新增 `RemoteTrigger` 工具入口,可把 source/event/message 作为远端触发事件注入到 running team recipients;默认优先发给 coordinator,消息正文保留远端来源和事件类型。`event_id` 可选,提供后会写入 session-scoped `remote_triggers.json` receipt,重复投递会 no-op 并记录 duplicate_count,避免远端重试重复注入。完整 remote websocket/CCR 服务接入仍未完成。
9797

0 commit comments

Comments
 (0)