@wjarka/cezarion 0.10.0-pr30.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +838 -0
- package/dist/agent-config/account-identity.d.ts +44 -0
- package/dist/agent-config/account-identity.js +128 -0
- package/dist/agent-config/account-identity.js.map +1 -0
- package/dist/agent-config/catalog.d.ts +72 -0
- package/dist/agent-config/catalog.js +223 -0
- package/dist/agent-config/catalog.js.map +1 -0
- package/dist/agent-config/files.d.ts +46 -0
- package/dist/agent-config/files.js +119 -0
- package/dist/agent-config/files.js.map +1 -0
- package/dist/agent-config/model-settings/claude.d.ts +2 -0
- package/dist/agent-config/model-settings/claude.js +11 -0
- package/dist/agent-config/model-settings/claude.js.map +1 -0
- package/dist/agent-config/model-settings/codex.d.ts +2 -0
- package/dist/agent-config/model-settings/codex.js +16 -0
- package/dist/agent-config/model-settings/codex.js.map +1 -0
- package/dist/agent-config/model-settings/opencode.d.ts +2 -0
- package/dist/agent-config/model-settings/opencode.js +8 -0
- package/dist/agent-config/model-settings/opencode.js.map +1 -0
- package/dist/agent-config/model-settings/pi.d.ts +12 -0
- package/dist/agent-config/model-settings/pi.js +18 -0
- package/dist/agent-config/model-settings/pi.js.map +1 -0
- package/dist/agent-config/model-settings/shared.d.ts +9 -0
- package/dist/agent-config/model-settings/shared.js +93 -0
- package/dist/agent-config/model-settings/shared.js.map +1 -0
- package/dist/agent-config/model-settings/types.d.ts +9 -0
- package/dist/agent-config/model-settings/types.js +2 -0
- package/dist/agent-config/model-settings/types.js.map +1 -0
- package/dist/agent-config/models.d.ts +9 -0
- package/dist/agent-config/models.js +32 -0
- package/dist/agent-config/models.js.map +1 -0
- package/dist/agent-config/seed.d.ts +7 -0
- package/dist/agent-config/seed.js +90 -0
- package/dist/agent-config/seed.js.map +1 -0
- package/dist/agent-config/service.d.ts +44 -0
- package/dist/agent-config/service.js +71 -0
- package/dist/agent-config/service.js.map +1 -0
- package/dist/agent-config/validate.d.ts +21 -0
- package/dist/agent-config/validate.js +76 -0
- package/dist/agent-config/validate.js.map +1 -0
- package/dist/automations/coordinator.d.ts +26 -0
- package/dist/automations/coordinator.js +66 -0
- package/dist/automations/coordinator.js.map +1 -0
- package/dist/automations/github-poller.d.ts +74 -0
- package/dist/automations/github-poller.js +234 -0
- package/dist/automations/github-poller.js.map +1 -0
- package/dist/automations/scheduler.d.ts +42 -0
- package/dist/automations/scheduler.js +192 -0
- package/dist/automations/scheduler.js.map +1 -0
- package/dist/automations/store.d.ts +65 -0
- package/dist/automations/store.js +298 -0
- package/dist/automations/store.js.map +1 -0
- package/dist/automations/task-template.d.ts +18 -0
- package/dist/automations/task-template.js +95 -0
- package/dist/automations/task-template.js.map +1 -0
- package/dist/automations/types.d.ts +255 -0
- package/dist/automations/types.js +159 -0
- package/dist/automations/types.js.map +1 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.js +242 -0
- package/dist/config.js.map +1 -0
- package/dist/contract/agent-config.d.ts +153 -0
- package/dist/contract/agent-profiles.d.ts +347 -0
- package/dist/contract/automations.d.ts +935 -0
- package/dist/contract/events.d.ts +104 -0
- package/dist/contract/github.d.ts +556 -0
- package/dist/contract/health.d.ts +102 -0
- package/dist/contract/index.d.ts +16 -0
- package/dist/contract/index.js +1895 -0
- package/dist/contract/projects.d.ts +210 -0
- package/dist/contract/repo.d.ts +234 -0
- package/dist/contract/runs.d.ts +1473 -0
- package/dist/contract/skills.d.ts +232 -0
- package/dist/contract/workflows.d.ts +218 -0
- package/dist/contract/workspace.d.ts +579 -0
- package/dist/core/agent-env.d.ts +31 -0
- package/dist/core/agent-env.js +371 -0
- package/dist/core/agent-env.js.map +1 -0
- package/dist/core/agent-model-policy.d.ts +10 -0
- package/dist/core/agent-model-policy.js +36 -0
- package/dist/core/agent-model-policy.js.map +1 -0
- package/dist/core/agent-profiles.d.ts +62 -0
- package/dist/core/agent-profiles.js +90 -0
- package/dist/core/agent-profiles.js.map +1 -0
- package/dist/core/agent-runner.d.ts +211 -0
- package/dist/core/agent-runner.js +68 -0
- package/dist/core/agent-runner.js.map +1 -0
- package/dist/core/ask.d.ts +105 -0
- package/dist/core/ask.js +187 -0
- package/dist/core/ask.js.map +1 -0
- package/dist/core/backend-detect.d.ts +16 -0
- package/dist/core/backend-detect.js +155 -0
- package/dist/core/backend-detect.js.map +1 -0
- package/dist/core/claude-cli-runner.d.ts +59 -0
- package/dist/core/claude-cli-runner.js +483 -0
- package/dist/core/claude-cli-runner.js.map +1 -0
- package/dist/core/claude-ui-mapper.d.ts +80 -0
- package/dist/core/claude-ui-mapper.js +598 -0
- package/dist/core/claude-ui-mapper.js.map +1 -0
- package/dist/core/codex-app-server-runner.d.ts +41 -0
- package/dist/core/codex-app-server-runner.js +584 -0
- package/dist/core/codex-app-server-runner.js.map +1 -0
- package/dist/core/codex-app-server-transport.d.ts +35 -0
- package/dist/core/codex-app-server-transport.js +150 -0
- package/dist/core/codex-app-server-transport.js.map +1 -0
- package/dist/core/codex-model-catalog.d.ts +10 -0
- package/dist/core/codex-model-catalog.js +110 -0
- package/dist/core/codex-model-catalog.js.map +1 -0
- package/dist/core/codex-ui-mapper.d.ts +112 -0
- package/dist/core/codex-ui-mapper.js +766 -0
- package/dist/core/codex-ui-mapper.js.map +1 -0
- package/dist/core/model-identity.d.ts +125 -0
- package/dist/core/model-identity.js +132 -0
- package/dist/core/model-identity.js.map +1 -0
- package/dist/core/model-presets.d.ts +21 -0
- package/dist/core/model-presets.js +81 -0
- package/dist/core/model-presets.js.map +1 -0
- package/dist/core/ndjson.d.ts +6 -0
- package/dist/core/ndjson.js +23 -0
- package/dist/core/ndjson.js.map +1 -0
- package/dist/core/opencode-model-catalog.d.ts +32 -0
- package/dist/core/opencode-model-catalog.js +153 -0
- package/dist/core/opencode-model-catalog.js.map +1 -0
- package/dist/core/opencode-server-runner.d.ts +32 -0
- package/dist/core/opencode-server-runner.js +882 -0
- package/dist/core/opencode-server-runner.js.map +1 -0
- package/dist/core/opencode-ui-mapper.d.ts +137 -0
- package/dist/core/opencode-ui-mapper.js +800 -0
- package/dist/core/opencode-ui-mapper.js.map +1 -0
- package/dist/core/pi-model-catalog.d.ts +31 -0
- package/dist/core/pi-model-catalog.js +152 -0
- package/dist/core/pi-model-catalog.js.map +1 -0
- package/dist/core/pi-runner.d.ts +24 -0
- package/dist/core/pi-runner.js +403 -0
- package/dist/core/pi-runner.js.map +1 -0
- package/dist/core/pi-ui-mapper.d.ts +36 -0
- package/dist/core/pi-ui-mapper.js +304 -0
- package/dist/core/pi-ui-mapper.js.map +1 -0
- package/dist/core/process-usage.d.ts +66 -0
- package/dist/core/process-usage.js +193 -0
- package/dist/core/process-usage.js.map +1 -0
- package/dist/core/provider-auth.d.ts +154 -0
- package/dist/core/provider-auth.js +509 -0
- package/dist/core/provider-auth.js.map +1 -0
- package/dist/core/provider-availability.d.ts +3 -0
- package/dist/core/provider-availability.js +14 -0
- package/dist/core/provider-availability.js.map +1 -0
- package/dist/core/runner-factory.d.ts +8 -0
- package/dist/core/runner-factory.js +25 -0
- package/dist/core/runner-factory.js.map +1 -0
- package/dist/core/runner-model-catalog.d.ts +29 -0
- package/dist/core/runner-model-catalog.js +78 -0
- package/dist/core/runner-model-catalog.js.map +1 -0
- package/dist/core/secret-redaction.d.ts +37 -0
- package/dist/core/secret-redaction.js +115 -0
- package/dist/core/secret-redaction.js.map +1 -0
- package/dist/core/shell-env.d.ts +28 -0
- package/dist/core/shell-env.js +56 -0
- package/dist/core/shell-env.js.map +1 -0
- package/dist/core/tool-display.d.ts +31 -0
- package/dist/core/tool-display.js +179 -0
- package/dist/core/tool-display.js.map +1 -0
- package/dist/core/ui-events.d.ts +335 -0
- package/dist/core/ui-events.js +25 -0
- package/dist/core/ui-events.js.map +1 -0
- package/dist/core/usage-limit.d.ts +42 -0
- package/dist/core/usage-limit.js +209 -0
- package/dist/core/usage-limit.js.map +1 -0
- package/dist/core/usage.d.ts +11 -0
- package/dist/core/usage.js +15 -0
- package/dist/core/usage.js.map +1 -0
- package/dist/core/v1-text-coalescer.d.ts +36 -0
- package/dist/core/v1-text-coalescer.js +72 -0
- package/dist/core/v1-text-coalescer.js.map +1 -0
- package/dist/git-diff-base.d.ts +85 -0
- package/dist/git-diff-base.js +180 -0
- package/dist/git-diff-base.js.map +1 -0
- package/dist/git-refs.d.ts +11 -0
- package/dist/git-refs.js +14 -0
- package/dist/git-refs.js.map +1 -0
- package/dist/git-worktree.d.ts +161 -0
- package/dist/git-worktree.js +508 -0
- package/dist/git-worktree.js.map +1 -0
- package/dist/handoff.d.ts +62 -0
- package/dist/handoff.js +154 -0
- package/dist/handoff.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +680 -0
- package/dist/index.js.map +1 -0
- package/dist/install-as-command.d.ts +40 -0
- package/dist/install-as-command.js +52 -0
- package/dist/install-as-command.js.map +1 -0
- package/dist/pack-check.d.ts +17 -0
- package/dist/pack-check.js +27 -0
- package/dist/pack-check.js.map +1 -0
- package/dist/paths.d.ts +140 -0
- package/dist/paths.js +192 -0
- package/dist/paths.js.map +1 -0
- package/dist/planner.d.ts +27 -0
- package/dist/planner.js +268 -0
- package/dist/planner.js.map +1 -0
- package/dist/release/manifests.d.ts +81 -0
- package/dist/release/manifests.js +74 -0
- package/dist/release/manifests.js.map +1 -0
- package/dist/release/snapshot.d.ts +67 -0
- package/dist/release/snapshot.js +97 -0
- package/dist/release/snapshot.js.map +1 -0
- package/dist/release/stable.d.ts +38 -0
- package/dist/release/stable.js +55 -0
- package/dist/release/stable.js.map +1 -0
- package/dist/runs/agent-tmpdir.d.ts +41 -0
- package/dist/runs/agent-tmpdir.js +183 -0
- package/dist/runs/agent-tmpdir.js.map +1 -0
- package/dist/runs/auto-name.d.ts +75 -0
- package/dist/runs/auto-name.js +179 -0
- package/dist/runs/auto-name.js.map +1 -0
- package/dist/runs/event-history.d.ts +50 -0
- package/dist/runs/event-history.js +613 -0
- package/dist/runs/event-history.js.map +1 -0
- package/dist/runs/retention.d.ts +60 -0
- package/dist/runs/retention.js +86 -0
- package/dist/runs/retention.js.map +1 -0
- package/dist/runs/review-gate.d.ts +18 -0
- package/dist/runs/review-gate.js +21 -0
- package/dist/runs/review-gate.js.map +1 -0
- package/dist/runs/run-index.d.ts +19 -0
- package/dist/runs/run-index.js +43 -0
- package/dist/runs/run-index.js.map +1 -0
- package/dist/runs/store.d.ts +446 -0
- package/dist/runs/store.js +1118 -0
- package/dist/runs/store.js.map +1 -0
- package/dist/runs/task-markers.d.ts +28 -0
- package/dist/runs/task-markers.js +74 -0
- package/dist/runs/task-markers.js.map +1 -0
- package/dist/runs/task-refs.d.ts +25 -0
- package/dist/runs/task-refs.js +74 -0
- package/dist/runs/task-refs.js.map +1 -0
- package/dist/runs/ui-event-sink.d.ts +81 -0
- package/dist/runs/ui-event-sink.js +210 -0
- package/dist/runs/ui-event-sink.js.map +1 -0
- package/dist/server/app-type.d.ts +21 -0
- package/dist/server/app-type.js +2 -0
- package/dist/server/app-type.js.map +1 -0
- package/dist/server/capabilities.d.ts +70 -0
- package/dist/server/capabilities.js +152 -0
- package/dist/server/capabilities.js.map +1 -0
- package/dist/server/checkout.d.ts +128 -0
- package/dist/server/checkout.js +296 -0
- package/dist/server/checkout.js.map +1 -0
- package/dist/server/forge/github.d.ts +513 -0
- package/dist/server/forge/github.js +2364 -0
- package/dist/server/forge/github.js.map +1 -0
- package/dist/server/forge/index.d.ts +40 -0
- package/dist/server/forge/index.js +72 -0
- package/dist/server/forge/index.js.map +1 -0
- package/dist/server/forge/types.d.ts +242 -0
- package/dist/server/forge/types.js +2 -0
- package/dist/server/forge/types.js.map +1 -0
- package/dist/server/fs-browse.d.ts +105 -0
- package/dist/server/fs-browse.js +182 -0
- package/dist/server/fs-browse.js.map +1 -0
- package/dist/server/git-changes.d.ts +213 -0
- package/dist/server/git-changes.js +555 -0
- package/dist/server/git-changes.js.map +1 -0
- package/dist/server/git.d.ts +28 -0
- package/dist/server/git.js +112 -0
- package/dist/server/git.js.map +1 -0
- package/dist/server/github.d.ts +9 -0
- package/dist/server/github.js +8 -0
- package/dist/server/github.js.map +1 -0
- package/dist/server/launch-key.d.ts +7 -0
- package/dist/server/launch-key.js +33 -0
- package/dist/server/launch-key.js.map +1 -0
- package/dist/server/open-in-app.d.ts +66 -0
- package/dist/server/open-in-app.js +211 -0
- package/dist/server/open-in-app.js.map +1 -0
- package/dist/server/open-in-terminal.d.ts +70 -0
- package/dist/server/open-in-terminal.js +179 -0
- package/dist/server/open-in-terminal.js.map +1 -0
- package/dist/server/pr.d.ts +7 -0
- package/dist/server/pr.js +7 -0
- package/dist/server/pr.js.map +1 -0
- package/dist/server/project-context.d.ts +120 -0
- package/dist/server/project-context.js +182 -0
- package/dist/server/project-context.js.map +1 -0
- package/dist/server/provider-action-gate.d.ts +8 -0
- package/dist/server/provider-action-gate.js +56 -0
- package/dist/server/provider-action-gate.js.map +1 -0
- package/dist/server/provider-auth-runtime.d.ts +21 -0
- package/dist/server/provider-auth-runtime.js +66 -0
- package/dist/server/provider-auth-runtime.js.map +1 -0
- package/dist/server/server.d.ts +13138 -0
- package/dist/server/server.js +5263 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/static-ui.d.ts +49 -0
- package/dist/server/static-ui.js +92 -0
- package/dist/server/static-ui.js.map +1 -0
- package/dist/server/validators.d.ts +97 -0
- package/dist/server/validators.js +86 -0
- package/dist/server/validators.js.map +1 -0
- package/dist/server/ws.d.ts +85 -0
- package/dist/server/ws.js +218 -0
- package/dist/server/ws.js.map +1 -0
- package/dist/server/wsl.d.ts +25 -0
- package/dist/server/wsl.js +92 -0
- package/dist/server/wsl.js.map +1 -0
- package/dist/server-install/engine.d.ts +50 -0
- package/dist/server-install/engine.js +324 -0
- package/dist/server-install/engine.js.map +1 -0
- package/dist/server-install/platforms/macosx-ngrok.d.ts +6 -0
- package/dist/server-install/platforms/macosx-ngrok.js +345 -0
- package/dist/server-install/platforms/macosx-ngrok.js.map +1 -0
- package/dist/server-install/platforms/ubuntu-vps.d.ts +78 -0
- package/dist/server-install/platforms/ubuntu-vps.js +1016 -0
- package/dist/server-install/platforms/ubuntu-vps.js.map +1 -0
- package/dist/server-install/state.d.ts +52 -0
- package/dist/server-install/state.js +182 -0
- package/dist/server-install/state.js.map +1 -0
- package/dist/server-install/steps.d.ts +109 -0
- package/dist/server-install/steps.js +334 -0
- package/dist/server-install/steps.js.map +1 -0
- package/dist/server-install/strategies.d.ts +4 -0
- package/dist/server-install/strategies.js +20 -0
- package/dist/server-install/strategies.js.map +1 -0
- package/dist/server-install/types.d.ts +287 -0
- package/dist/server-install/types.js +121 -0
- package/dist/server-install/types.js.map +1 -0
- package/dist/server-install/ui.d.ts +42 -0
- package/dist/server-install/ui.js +130 -0
- package/dist/server-install/ui.js.map +1 -0
- package/dist/skills-banner.d.ts +22 -0
- package/dist/skills-banner.js +41 -0
- package/dist/skills-banner.js.map +1 -0
- package/dist/skills-remote.d.ts +84 -0
- package/dist/skills-remote.js +468 -0
- package/dist/skills-remote.js.map +1 -0
- package/dist/skills-update.d.ts +88 -0
- package/dist/skills-update.js +392 -0
- package/dist/skills-update.js.map +1 -0
- package/dist/skills.d.ts +82 -0
- package/dist/skills.js +251 -0
- package/dist/skills.js.map +1 -0
- package/dist/todos.d.ts +48 -0
- package/dist/todos.js +201 -0
- package/dist/todos.js.map +1 -0
- package/dist/ui-state.d.ts +26 -0
- package/dist/ui-state.js +38 -0
- package/dist/ui-state.js.map +1 -0
- package/dist/update-check.d.ts +12 -0
- package/dist/update-check.js +41 -0
- package/dist/update-check.js.map +1 -0
- package/dist/workflows/load.d.ts +15 -0
- package/dist/workflows/load.js +56 -0
- package/dist/workflows/load.js.map +1 -0
- package/dist/workflows/run.d.ts +758 -0
- package/dist/workflows/run.js +3389 -0
- package/dist/workflows/run.js.map +1 -0
- package/dist/workflows/types.d.ts +153 -0
- package/dist/workflows/types.js +212 -0
- package/dist/workflows/types.js.map +1 -0
- package/dist/workspace/agent-accounts.d.ts +153 -0
- package/dist/workspace/agent-accounts.js +304 -0
- package/dist/workspace/agent-accounts.js.map +1 -0
- package/dist/workspace/agent-profiles.d.ts +78 -0
- package/dist/workspace/agent-profiles.js +115 -0
- package/dist/workspace/agent-profiles.js.map +1 -0
- package/dist/workspace/config.d.ts +169 -0
- package/dist/workspace/config.js +379 -0
- package/dist/workspace/config.js.map +1 -0
- package/dist/workspace/migrations.d.ts +45 -0
- package/dist/workspace/migrations.js +147 -0
- package/dist/workspace/migrations.js.map +1 -0
- package/dist/workspace/projects-cli.d.ts +25 -0
- package/dist/workspace/projects-cli.js +172 -0
- package/dist/workspace/projects-cli.js.map +1 -0
- package/dist/workspace/projects.d.ts +109 -0
- package/dist/workspace/projects.js +250 -0
- package/dist/workspace/projects.js.map +1 -0
- package/dist/workspace/semaphore.d.ts +179 -0
- package/dist/workspace/semaphore.js +195 -0
- package/dist/workspace/semaphore.js.map +1 -0
- package/dist/workspace/ui-state.d.ts +34 -0
- package/dist/workspace/ui-state.js +53 -0
- package/dist/workspace/ui-state.js.map +1 -0
- package/package.json +70 -0
- package/scripts/check-pack.mjs +46 -0
- package/scripts/inline-contract.mjs +112 -0
- package/scripts/install-as-command.mjs +120 -0
- package/scripts/mock-claude.mjs +520 -0
- package/scripts/mock-pi-rpc.mjs +83 -0
- package/scripts/sync-readme.mjs +20 -0
- package/scripts/test-process-usage.mjs +47 -0
- package/web/dist/assets/alert-dialog-B4Ofsgv7.js +1 -0
- package/web/dist/assets/arrow-down-Dy8dup7j.js +1 -0
- package/web/dist/assets/arrow-left-BJaucbhn.js +1 -0
- package/web/dist/assets/bundle-mjs-BT31bpU6.js +1 -0
- package/web/dist/assets/centered-state-duPW0phQ.js +43 -0
- package/web/dist/assets/chevron-right-Be4A5TCS.js +1 -0
- package/web/dist/assets/chunk-BO2N2NFS-DE6qKn3r.js +131 -0
- package/web/dist/assets/collapsible-DdcNBdSk.js +1 -0
- package/web/dist/assets/commit-list-CgvN9vec.js +1 -0
- package/web/dist/assets/compare-variants-CQA2nd65.js +1 -0
- package/web/dist/assets/core-BCsw8oQw.js +12 -0
- package/web/dist/assets/css-BsVw1vtW.js +1 -0
- package/web/dist/assets/diff-B6Agp5Hl.js +3 -0
- package/web/dist/assets/diff-stat-BoXuGkgq.js +1 -0
- package/web/dist/assets/diff-view-CD8XI-fj.js +6 -0
- package/web/dist/assets/diff-woXpYk--.js +1 -0
- package/web/dist/assets/dropdown-menu-B3SYw-k5.js +1 -0
- package/web/dist/assets/editable-title-B-hjp5wl.js +1 -0
- package/web/dist/assets/ellipsis-vertical-BYwzw0xl.js +1 -0
- package/web/dist/assets/engine-javascript-FQvzRocB.js +141 -0
- package/web/dist/assets/file-BRj5eISx.js +1 -0
- package/web/dist/assets/folder-z4oT5981.js +1 -0
- package/web/dist/assets/git-toolbar-owSEkYld.js +1 -0
- package/web/dist/assets/github-Bz2VgRX3.js +1 -0
- package/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/web/dist/assets/highlighted-body-OFNGDK62-BL-MfFgl.js +1 -0
- package/web/dist/assets/highlighter-CdHqIwFr.js +3 -0
- package/web/dist/assets/html-BY76lXLH.js +1 -0
- package/web/dist/assets/image-preview-h3WFluuy.js +1 -0
- package/web/dist/assets/index-BbcQNT92.css +2 -0
- package/web/dist/assets/index-EtQQQ6Kj.js +7 -0
- package/web/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/web/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/web/dist/assets/javascript-BgS3c2Ky.js +1 -0
- package/web/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/web/dist/assets/json-qhed-kSA.js +1 -0
- package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/web/dist/assets/jsx-Bz0zcwM4.js +1 -0
- package/web/dist/assets/lib-BQXq3kEf.js +1 -0
- package/web/dist/assets/lib-BxQXEXDF.js +1 -0
- package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/web/dist/assets/markdown-DqBKYPiB.js +2 -0
- package/web/dist/assets/mermaid-GHXKKRXX-C4jxqUUb.js +1 -0
- package/web/dist/assets/new-task-form-BdJKRBJX.js +1 -0
- package/web/dist/assets/pill-BQfmfpYp.js +1 -0
- package/web/dist/assets/project-router-DxgFZY1l.js +1 -0
- package/web/dist/assets/prompt-templates-B6m16gq-.js +15 -0
- package/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/web/dist/assets/react-runtime-CCIEwYL0.js +9 -0
- package/web/dist/assets/refresh-cw-mhSpxf2a.js +1 -0
- package/web/dist/assets/repo-git-CUNRxR3_.js +1 -0
- package/web/dist/assets/rolldown-runtime-QTnfLwEv.js +1 -0
- package/web/dist/assets/run-diff-DsmmDVfx.js +3 -0
- package/web/dist/assets/run-header-BkhrseLJ.js +1 -0
- package/web/dist/assets/rust-Cfkwpbl8.js +1 -0
- package/web/dist/assets/search-x-BqDMB2g8.js +1 -0
- package/web/dist/assets/shellscript-CLZ0U2zV.js +1 -0
- package/web/dist/assets/skill-empty-hint-BWEvTggc.js +1 -0
- package/web/dist/assets/skills-9oR_oHse.js +1 -0
- package/web/dist/assets/skills-DwIxnnWO.js +1 -0
- package/web/dist/assets/sparkles-DIoTQpyx.js +1 -0
- package/web/dist/assets/sql-BsFa4tDR.js +1 -0
- package/web/dist/assets/square-terminal-Cc7CuQxc.js +1 -0
- package/web/dist/assets/tab-link-D1fQgpmO.js +1 -0
- package/web/dist/assets/task-changes-BKeLlDpK.js +1 -0
- package/web/dist/assets/task-commits-gcEFZIa5.js +1 -0
- package/web/dist/assets/task-files-BGAqoXTW.js +2 -0
- package/web/dist/assets/task-thread-DT8aUYTS.js +9 -0
- package/web/dist/assets/textarea-BOVbfy5C.js +1 -0
- package/web/dist/assets/thread-loading-CypW73KD.js +1 -0
- package/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/web/dist/assets/trash-2-CJq6LHWn.js +1 -0
- package/web/dist/assets/triangle-alert-CjDFvU27.js +1 -0
- package/web/dist/assets/tsx-udAQXfEw.js +1 -0
- package/web/dist/assets/typescript-bsJCZSQ-.js +1 -0
- package/web/dist/assets/upload-CPDZlJeK.js +1 -0
- package/web/dist/assets/use-desktop-5JsLmi0a.js +1 -0
- package/web/dist/assets/use-submit-shortcut-CHVFevty.js +1 -0
- package/web/dist/assets/utils-DiO6zyJd.js +64 -0
- package/web/dist/assets/workflows-rQDFzWRt.js +11 -0
- package/web/dist/assets/yaml-rwi0_p6S.js +1 -0
- package/web/dist/assets/zoomable-image-COWqb11v.js +1 -0
- package/web/dist/index.html +80 -0
- package/web/dist/open-mercato.svg +11 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { collectSecretValues, redactDeep, redactSecrets } from '../core/secret-redaction.js';
|
|
7
|
+
// Pure, dependency-free reference helpers — the same sanity bound the marker parser applies.
|
|
8
|
+
import { MAX_REF } from './task-refs.js';
|
|
9
|
+
// Type-only module (zod + nothing else), so this cannot cycle back into the store.
|
|
10
|
+
import { workflowDefSchema } from '../workflows/types.js';
|
|
11
|
+
import { RUNNER_IDS } from '../core/agent-runner.js';
|
|
12
|
+
const usageCounterSchema = z.number().finite().nonnegative();
|
|
13
|
+
/**
|
|
14
|
+
* A runner id as it may appear in a PERSISTED record, normalized to the three
|
|
15
|
+
* ids the rest of cezar speaks (#547).
|
|
16
|
+
*
|
|
17
|
+
* `claude-cli` is the legacy spelling of `claude` — still a member of
|
|
18
|
+
* `AgentBackend` and still accepted by `createRunner`, and named by
|
|
19
|
+
* `BACKWARD_COMPATIBILITY.md` §3 as an id `runs.json` keeps parseable. The enum
|
|
20
|
+
* here did not accept it, so that promise was false: the loader `safeParse`s the
|
|
21
|
+
* WHOLE array, so one record carrying it would have dropped every run in the
|
|
22
|
+
* file — the exact failure mode §3 exists to warn about.
|
|
23
|
+
*
|
|
24
|
+
* Parse-and-fold rather than widen: the legacy id is accepted on the way in and
|
|
25
|
+
* collapsed to `claude`, so no consumer, wire type or contract schema ever sees
|
|
26
|
+
* a fourth runner. The narrowing is one-way and permanent (the index is
|
|
27
|
+
* re-serialized from the parsed records), which is what "old run records
|
|
28
|
+
* normalise identically to `claude`" in `core/model-identity.ts` has always
|
|
29
|
+
* claimed. Use ONLY for read-back of stored state — request bodies, settings and
|
|
30
|
+
* workflow step defs stay the three selectable ids (`RunnerId`), because nothing
|
|
31
|
+
* should be able to ASK for the legacy spelling.
|
|
32
|
+
*/
|
|
33
|
+
const storedRunnerSchema = z
|
|
34
|
+
.enum([...RUNNER_IDS, 'claude-cli'])
|
|
35
|
+
.transform((id) => (id === 'claude-cli' ? 'claude' : id));
|
|
36
|
+
const stepStateSchema = z.object({
|
|
37
|
+
id: z.string(),
|
|
38
|
+
name: z.string(),
|
|
39
|
+
kind: z.enum(['agent', 'check']),
|
|
40
|
+
status: z.enum(['pending', 'running', 'waiting', 'review', 'done', 'failed', 'cancelled', 'skipped']),
|
|
41
|
+
iterations: z.number(),
|
|
42
|
+
tokensUsed: z.number(),
|
|
43
|
+
inputTokens: usageCounterSchema.optional(),
|
|
44
|
+
outputTokens: usageCounterSchema.optional(),
|
|
45
|
+
usageInvocationsStarted: usageCounterSchema.optional(),
|
|
46
|
+
usageInvocationsObserved: usageCounterSchema.optional(),
|
|
47
|
+
usageTurnsStarted: usageCounterSchema.optional(),
|
|
48
|
+
usageTurnsRecorded: usageCounterSchema.optional(),
|
|
49
|
+
usageInvocationEpoch: usageCounterSchema.optional(),
|
|
50
|
+
startedAt: z.string().optional(),
|
|
51
|
+
finishedAt: z.string().optional(),
|
|
52
|
+
error: z.string().optional(),
|
|
53
|
+
/** Latest backend-owned session id, used for same-backend Continue. */
|
|
54
|
+
sessionId: z.string().optional(),
|
|
55
|
+
/** Backend that owns `sessionId`. Optional so pre-affinity runs.json files still parse;
|
|
56
|
+
* `storedRunnerSchema` so a legacy `claude-cli` folds to `claude` instead of failing (#547). */
|
|
57
|
+
backend: storedRunnerSchema.optional(),
|
|
58
|
+
/** Agent profile (account) this step actually spawned under — `default`, or a stored profile
|
|
59
|
+
* id (spec 2026-07-29-agent-profiles). Recorded rather than re-derived because a session id
|
|
60
|
+
* only means something inside the config dir that created it: `sessionId` and `profileId` are
|
|
61
|
+
* a PAIR. Without it, changing the project's account would silently make Continue resume
|
|
62
|
+
* against the wrong account's session store. Absent = the discovered default. */
|
|
63
|
+
profileId: z.string().optional(),
|
|
64
|
+
/** Dollar cost reported by the claude CLI for this step's turns. */
|
|
65
|
+
costUsd: z.number().optional(),
|
|
66
|
+
});
|
|
67
|
+
/** One prompt message stacked onto a run while it waits for a free agent slot
|
|
68
|
+
* (#472). Folded into `{{task}}` at dequeue by `hydrateQueuedInput`; never
|
|
69
|
+
* delivered as its own turn — a follow-up turn would reach only the first step
|
|
70
|
+
* of a chain and would race the opening turn. */
|
|
71
|
+
const queuedMessageSchema = z.object({
|
|
72
|
+
id: z.string(),
|
|
73
|
+
text: z.string(),
|
|
74
|
+
/** `/api/v1/runs/:id/images/…` URLs — the base64 never enters `runs.json`. */
|
|
75
|
+
images: z.array(z.string()).optional(),
|
|
76
|
+
createdAt: z.string(),
|
|
77
|
+
});
|
|
78
|
+
/** Exported for `./run-index.ts`, the read-only reader of the same file. Nothing else should
|
|
79
|
+
* parse `runs.json` — see `reconcileLoadedRun` for why a second parser is a correctness risk. */
|
|
80
|
+
export const runRecordSchema = z.object({
|
|
81
|
+
id: z.string(),
|
|
82
|
+
title: z.string(),
|
|
83
|
+
/** Display title (#389): the auto-derived summary of the first agent turn,
|
|
84
|
+
* or the user's inline edit (`PATCH /api/runs/:id` sets it together with
|
|
85
|
+
* `title` so edits always win). The UI shows `titleSummary ?? title`. */
|
|
86
|
+
titleSummary: z.string().optional(),
|
|
87
|
+
/** `git diff --shortstat` of the worktree vs its base, refreshed on every
|
|
88
|
+
* turn-end (#389) — what the quick list / table shows without a git call.
|
|
89
|
+
* `repointed` (#751) is optional and only ever written as `true`: it marks the
|
|
90
|
+
* runs whose numbers were narrowed to uncommitted work because the agent had
|
|
91
|
+
* checked another branch out into the worktree. Optional is load-bearing here —
|
|
92
|
+
* `runs.json` is `safeParse`d as one array, so a required addition would
|
|
93
|
+
* silently drop every pre-existing run. */
|
|
94
|
+
diffStat: z
|
|
95
|
+
.object({
|
|
96
|
+
adds: z.number(),
|
|
97
|
+
dels: z.number(),
|
|
98
|
+
files: z.number(),
|
|
99
|
+
repointed: z.boolean().optional(),
|
|
100
|
+
})
|
|
101
|
+
.optional(),
|
|
102
|
+
workflow: z.string(),
|
|
103
|
+
task: z.string(),
|
|
104
|
+
/** Prompt messages stacked onto this run while it was queued (#472). Optional:
|
|
105
|
+
* `undefined` on every pre-#472 record reads as an empty stack. Like `task`,
|
|
106
|
+
* `text` is the user's own prompt and is replayed into `{{task}}`, so it is
|
|
107
|
+
* deliberately NOT in `redactPatch`'s field list — scrubbing it would corrupt
|
|
108
|
+
* the run the same way scrubbing `task` would. */
|
|
109
|
+
queuedMessages: z.array(queuedMessageSchema).optional(),
|
|
110
|
+
/** URLs of images attached to the initial task prompt, for the thread's first bubble
|
|
111
|
+
* (#image-display) — persisted like agent screenshots, served from `/images/`. */
|
|
112
|
+
taskImages: z.array(z.string()).optional(),
|
|
113
|
+
model: z.string().optional(),
|
|
114
|
+
/** Canonical provider/model identity (#405) — the normalised `provider/model`
|
|
115
|
+
* (e.g. `anthropic/claude-opus-4-8`) the run actually used, resolved from the
|
|
116
|
+
* free-text `model` against the chosen runner. Additive and optional: pre-#405
|
|
117
|
+
* records carry only `model`, and it stays the human/hand-edit surface; this
|
|
118
|
+
* is the parseable identity cost attribution and reproducible replay key off.
|
|
119
|
+
*
|
|
120
|
+
* Read in production by the session header's agent badge (#546), which shows it
|
|
121
|
+
* whenever it says something `model` does not — so this is no longer a
|
|
122
|
+
* write-only field whose next reader has to guess whether it is load-bearing. */
|
|
123
|
+
modelIdentity: z.string().optional(),
|
|
124
|
+
/** Agent backend this run used — drives "open in CLI" resume command. `storedRunnerSchema`
|
|
125
|
+
* so a legacy `claude-cli` record folds to `claude` instead of failing the whole index (#547). */
|
|
126
|
+
runner: storedRunnerSchema.optional(),
|
|
127
|
+
/** Per-task agent-account override from the composer (spec 2026-07-29-agent-profiles), applying
|
|
128
|
+
* to steps that run on `runner`. Steps on a DIFFERENT backend still resolve from the project's
|
|
129
|
+
* own selection — an override for Claude says nothing about which Codex account a mixed
|
|
130
|
+
* workflow's codex step should use. Absent = follow the project. */
|
|
131
|
+
agentProfile: z.string().optional(),
|
|
132
|
+
/** Echo of the extra system prompt this run actually used (R2): the
|
|
133
|
+
* `POST /api/runs` override, or the `config.json` default it fell back to.
|
|
134
|
+
* Deliberately NOT the full composed prompt — skill bodies and the handoff
|
|
135
|
+
* contract are derivable from the persisted workflow and would bloat the
|
|
136
|
+
* index. Resolved at execute time (a queued run picks up config edits). */
|
|
137
|
+
systemPrompt: z.string().optional(),
|
|
138
|
+
/** Per-task follow-up inbox contract (spec 007, #444). Missing on old runs
|
|
139
|
+
* means enabled — the historical behavior. */
|
|
140
|
+
generateFollowups: z.boolean().optional(),
|
|
141
|
+
/** Autonomous mode (#489): the run was started with the "autonomous" checkbox,
|
|
142
|
+
* so it never parks at `waiting` (auto-nudge) and — once persisted here —
|
|
143
|
+
* never parks at the terminal `review` gate either (`settleSuccess` + the
|
|
144
|
+
* group-pick winner-park read it). Additive-safe: absent = falsy = not
|
|
145
|
+
* autonomous. Set at creation from `WorkflowInput.autonomous`. */
|
|
146
|
+
autonomous: z.boolean().optional(),
|
|
147
|
+
/** Optional provenance for tasks launched by a project GitHub automation. */
|
|
148
|
+
automation: z
|
|
149
|
+
.object({
|
|
150
|
+
automationId: z.string(),
|
|
151
|
+
automationRevision: z.number().int().positive(),
|
|
152
|
+
receiptId: z.string(),
|
|
153
|
+
event: z.string(),
|
|
154
|
+
githubUrl: z.string().url(),
|
|
155
|
+
})
|
|
156
|
+
.optional(),
|
|
157
|
+
status: z.enum(['queued', 'running', 'waiting', 'review', 'done', 'failed', 'cancelled']),
|
|
158
|
+
/** Sub-state of `running` (spec 2026-07-18-subagent-monitoring-status, #490):
|
|
159
|
+
* `monitoring` while the agent is still working on its own downstream work.
|
|
160
|
+
* Optional/absent on old runs; cleared when the run resumes or ends. */
|
|
161
|
+
activity: z.enum(['monitoring']).optional(),
|
|
162
|
+
/** Exact server-computed deadline for the next automatic monitoring check. */
|
|
163
|
+
monitoringWakeAt: z.string().datetime().optional().catch(undefined),
|
|
164
|
+
/** True only for the live epoch that exhausted all automatic monitoring checks. */
|
|
165
|
+
monitoringWakeCapReached: z.boolean().optional(),
|
|
166
|
+
/**
|
|
167
|
+
* Exact deadline at which a run stopped by a provider USAGE LIMIT resumes itself
|
|
168
|
+
* (spec 2026-08-03-auto-resume-after-usage-limit) — the reset instant the provider named plus a
|
|
169
|
+
* short grace. Present only while such a resume is pending: the run is `failed`, the timer is
|
|
170
|
+
* armed, and the cockpit says so. Deliberately survives a restart (`RunStore.open` keeps it) —
|
|
171
|
+
* it is what lets `recover()` re-arm a wait that may be hours long.
|
|
172
|
+
*/
|
|
173
|
+
autoResumeAt: z.string().datetime().optional().catch(undefined),
|
|
174
|
+
/** Consecutive automatic resumes since the last human turn — the safety cap's counter.
|
|
175
|
+
* Persisted so a restart cannot reset a loop back to zero. */
|
|
176
|
+
autoResumeAttempts: z.number().int().min(0).optional().catch(undefined),
|
|
177
|
+
createdAt: z.string(),
|
|
178
|
+
startedAt: z.string().optional(),
|
|
179
|
+
finishedAt: z.string().optional(),
|
|
180
|
+
tokensUsed: z.number(),
|
|
181
|
+
inputTokens: usageCounterSchema.optional(),
|
|
182
|
+
outputTokens: usageCounterSchema.optional(),
|
|
183
|
+
costUsd: z.number().optional(),
|
|
184
|
+
/** First GitHub PR URL spotted in the transcript (the janitor trick). */
|
|
185
|
+
pullRequestUrl: z.string().optional(),
|
|
186
|
+
/** The PR this task is ABOUT (#407, spec 2026-07-16-pr-autodiscovery):
|
|
187
|
+
* auto-discovered from conversation references for tasks that work on an
|
|
188
|
+
* existing PR (review/continue/merge). Display-only tier — `pullRequestUrl`
|
|
189
|
+
* (the PR this task CREATED) always wins, and action gates ignore this. */
|
|
190
|
+
referencedPullRequestUrl: z.string().optional(),
|
|
191
|
+
/** The PR/issue number this task is ABOUT (spec 2026-07-17-task-auto-naming):
|
|
192
|
+
* regex-extracted from the task prompt, upgradable by the namer's
|
|
193
|
+
* cross-checked output. Display tier — never gates actions. */
|
|
194
|
+
prNumber: z.number().optional(),
|
|
195
|
+
issueNumber: z.number().optional(),
|
|
196
|
+
/** Provenance for an `issueNumber` seeded by referenced-issue discovery.
|
|
197
|
+
* Persisted so ambiguity can revoke only the janitor's own value, including
|
|
198
|
+
* after a restart. Any prompt, namer, or marker write clears this flag. */
|
|
199
|
+
referencedIssueNumberSeeded: z.boolean().optional(),
|
|
200
|
+
/** Who owns the display title: `user` (PATCH rename — never auto-overwritten),
|
|
201
|
+
* `marker` (agent-declared via `CEZ:TITLE`, spec 2026-07-18-task-ref-markers —
|
|
202
|
+
* beats the namer, silences live refresh) or `auto` (namer-owned — a later
|
|
203
|
+
* namer result may replace it). Missing on old runs = legacy behavior (auto
|
|
204
|
+
* fills only an unset titleSummary). Precedence: user > marker > auto. */
|
|
205
|
+
titleOrigin: z.enum(['user', 'auto', 'marker']).optional(),
|
|
206
|
+
/** References the agent itself declared via `CEZ:PR=` / `CEZ:ISSUE=` markers
|
|
207
|
+
* (spec 2026-07-18-task-ref-markers). Presence of a kind makes it
|
|
208
|
+
* authoritative: the namer may no longer write that kind, and a declared PR
|
|
209
|
+
* owns the referenced tier's resolution. */
|
|
210
|
+
markerRefs: z.object({ pr: z.number().optional(), issue: z.number().optional() }).optional(),
|
|
211
|
+
/** Distinct PR URLs spotted so far — the referenced tier's working set,
|
|
212
|
+
* persisted so a resumed run keeps disambiguating against the full history
|
|
213
|
+
* instead of re-adopting the next URL as "the only one". Capped. */
|
|
214
|
+
referencedPrCandidates: z.array(z.string()).optional(),
|
|
215
|
+
/** The issue this task is ABOUT (spec 2026-07-21-report-ref-discovery):
|
|
216
|
+
* auto-discovered from `github.com/…/issues/N` links in the conversation,
|
|
217
|
+
* mirroring the referenced-PR tier. Display-only; never gates actions. */
|
|
218
|
+
referencedIssueUrl: z.string().optional(),
|
|
219
|
+
/** Distinct issue URLs spotted so far — the referenced-issue working set,
|
|
220
|
+
* persisted like `referencedPrCandidates`. Capped. */
|
|
221
|
+
referencedIssueCandidates: z.array(z.string()).optional(),
|
|
222
|
+
/** Explicit execution policy. `false` means the run intentionally uses the repo root;
|
|
223
|
+
* absent on older runs and for the default isolated-worktree mode. */
|
|
224
|
+
worktree: z.literal(false).optional(),
|
|
225
|
+
/** Task worktree (spec 006) — absent for in-place runs and after explicit cleanup. */
|
|
226
|
+
worktreePath: z.string().optional(),
|
|
227
|
+
/** The task's own branch (`cez/<id8>`), created off `baseBranch`. */
|
|
228
|
+
branch: z.string().optional(),
|
|
229
|
+
/** Stable baseline for session git views: a worktree's fork ref, or an in-place run's starting commit. */
|
|
230
|
+
baseBranch: z.string().optional(),
|
|
231
|
+
/** Set when count-based retention (#483) reclaimed this run's worktree
|
|
232
|
+
* *directory* (the `cez/<id8>` branch is kept). Presence means "materialized
|
|
233
|
+
* dir gone, recoverable via `git worktree add`"; it excludes the run from the
|
|
234
|
+
* retention budget until the dir is re-materialized (resume clears it). */
|
|
235
|
+
worktreeReclaimedAt: z.string().optional(),
|
|
236
|
+
/** Parallel variants (spec 010): tasks sharing a groupId are one group. */
|
|
237
|
+
groupId: z.string().optional(),
|
|
238
|
+
/** Variant letter within the group — 'A' | 'B' | 'C' (kept as a string). */
|
|
239
|
+
variant: z.string().optional(),
|
|
240
|
+
/** Peak resident memory (bytes) / process count observed across the run's
|
|
241
|
+
* agent process trees (#348) — written when a session's telemetry ends.
|
|
242
|
+
* Optional: old runs.json files and `ps`-less platforms have neither. */
|
|
243
|
+
peakRssBytes: z.number().optional(),
|
|
244
|
+
peakProcCount: z.number().optional(),
|
|
245
|
+
archived: z.boolean().default(false),
|
|
246
|
+
archivedAt: z.string().optional(),
|
|
247
|
+
/** Read receipt (#unread-done-items): the ISO time the cockpit last opened this
|
|
248
|
+
* run's thread. A finished run reads as "unread" until it has been seen since it
|
|
249
|
+
* finished — see `isUnread()` in the cockpit's `lib/read-state.ts`. Absent on old
|
|
250
|
+
* runs, on every run not yet opened, and on one `setUnread` put back to unread
|
|
251
|
+
* (#775) — the unread rule treats all three alike. */
|
|
252
|
+
seenAt: z.string().optional(),
|
|
253
|
+
currentStepId: z.string().optional(),
|
|
254
|
+
error: z.string().optional(),
|
|
255
|
+
steps: z.array(stepStateSchema),
|
|
256
|
+
/** Full workflow definition, persisted so a `queued` run can be re-enqueued
|
|
257
|
+
* after a restart (#367) — including ad-hoc "(planned)" chains that exist
|
|
258
|
+
* nowhere else.
|
|
259
|
+
*
|
|
260
|
+
* Typed, not `z.record(z.string(), z.unknown())`: this key goes out over the
|
|
261
|
+
* wire on every run route, and `unknown` is wider than anything the server
|
|
262
|
+
* can serialize — which made the route's own type (hono's `JSONValue`, whose
|
|
263
|
+
* index signature admits `object | symbol | undefined`) impossible for the
|
|
264
|
+
* contract to describe. `.catch(undefined)` keeps an older or hand-edited
|
|
265
|
+
* entry from failing the whole index parse: a def that no longer fits simply
|
|
266
|
+
* drops, and `reviveWorkflow` falls back to the catalog by name.
|
|
267
|
+
*
|
|
268
|
+
* That drop is PERMANENT, not per-boot — the index is re-serialized from the
|
|
269
|
+
* parsed records (`saveNow`), so the next save writes runs.json back without
|
|
270
|
+
* it. Harmless for a catalog workflow, which re-resolves by name; fatal for
|
|
271
|
+
* the ad-hoc "(planned)" chain this field exists to preserve, which has no
|
|
272
|
+
* catalog entry to fall back to. Nothing written since #367 fails the schema
|
|
273
|
+
* (`name`, `source` and `steps` have been on every persisted def), so tighten
|
|
274
|
+
* `workflowStepSchema` only with that in mind: a narrowing here silently eats
|
|
275
|
+
* queued runs rather than degrading them. */
|
|
276
|
+
workflowDef: workflowDefSchema.optional().catch(undefined),
|
|
277
|
+
});
|
|
278
|
+
const MAX_RUNS_KEPT = 300;
|
|
279
|
+
const MAX_ARCHIVED_KEPT = 500;
|
|
280
|
+
const PR_URL_RE = /https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+/;
|
|
281
|
+
const ISSUE_URL_RE = /https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/issues\/\d+/;
|
|
282
|
+
// The transcript auto-link is convenience only (the cockpit's own `gh pr create` path sets the
|
|
283
|
+
// URL authoritatively). Adopt a PR URL ONLY when the agent actually CREATED one — a task that
|
|
284
|
+
// reviews or merely references an existing PR must not get mislabeled with its number (#fake-pr).
|
|
285
|
+
const CREATED_PR_RE = /\b(?:gh\s+pr\s+create|pull\s*request\s+created|created\s+(?:a\s+)?(?:draft\s+)?(?:pr|pull\s*request)|opened\s+(?:a\s+)?(?:draft\s+)?pull\s*request)\b/i;
|
|
286
|
+
/** Referenced-tier working-set cap (spec 2026-07-16-pr-autodiscovery): past
|
|
287
|
+
* this many distinct PRs the conversation is a survey, not a subject. */
|
|
288
|
+
const MAX_PR_CANDIDATES = 8;
|
|
289
|
+
/**
|
|
290
|
+
* Every scannable string of one persisted event: the v1 top-level fields plus
|
|
291
|
+
* the protocol-v2 `item.*` content (nested — the reason v2 streams were
|
|
292
|
+
* invisible to the janitor, #407). Reasoning items are skipped: thinking text
|
|
293
|
+
* speculates about PRs the task never touches.
|
|
294
|
+
*/
|
|
295
|
+
/**
|
|
296
|
+
* Archiving IS resigning from a task, so an archived run can never carry a pending usage-limit
|
|
297
|
+
* resume (spec 2026-08-03-auto-resume-after-usage-limit). The rule lives HERE rather than in the
|
|
298
|
+
* archive route because the bulk "Archive finished" sweep never goes through that route, and a
|
|
299
|
+
* user who archives fifty finished tasks has resigned from all fifty.
|
|
300
|
+
*
|
|
301
|
+
* The engine needs no telling: its timer re-reads the record before it fires and no sweep re-arms
|
|
302
|
+
* an archived run, so a cleared field is the whole cancellation.
|
|
303
|
+
*/
|
|
304
|
+
function clearPendingAutoResume(run) {
|
|
305
|
+
run.autoResumeAt = undefined;
|
|
306
|
+
run.autoResumeAttempts = undefined;
|
|
307
|
+
}
|
|
308
|
+
function eventTextFragments(event) {
|
|
309
|
+
const fragments = [];
|
|
310
|
+
for (const key of ['text', 'result', 'message']) {
|
|
311
|
+
const value = event[key];
|
|
312
|
+
if (typeof value === 'string')
|
|
313
|
+
fragments.push(value);
|
|
314
|
+
}
|
|
315
|
+
const item = event.item;
|
|
316
|
+
if (item && typeof item === 'object' && item.kind !== 'reasoning') {
|
|
317
|
+
const it = item;
|
|
318
|
+
for (const key of ['text', 'title', 'output']) {
|
|
319
|
+
const value = it[key];
|
|
320
|
+
if (typeof value === 'string')
|
|
321
|
+
fragments.push(value);
|
|
322
|
+
}
|
|
323
|
+
if (typeof it.input === 'string') {
|
|
324
|
+
fragments.push(it.input);
|
|
325
|
+
}
|
|
326
|
+
else if (it.input !== undefined) {
|
|
327
|
+
try {
|
|
328
|
+
fragments.push(JSON.stringify(it.input));
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
// circular input — skip it
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return fragments;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Where a CREATION CLAIM may come from — the trust boundary the created tier was missing.
|
|
339
|
+
*
|
|
340
|
+
* `CREATED_PR_RE` used to be matched against everything an event carried, tool OUTPUT included,
|
|
341
|
+
* so a transcript that merely QUOTES a `gh pr create` line handed the run a PR it never opened.
|
|
342
|
+
* Not hypothetical: the task that fixed the reference chips printed another run's stored events
|
|
343
|
+
* while investigating them, and cezar read `"title": "Ran gh pr create --repo …"` out of that
|
|
344
|
+
* dump and adopted a PR from a DIFFERENT repository as its own — permanently, because the first
|
|
345
|
+
* created URL wins and the real `gh pr create` that followed was never looked at.
|
|
346
|
+
*
|
|
347
|
+
* So the claim must come from the agent's own words, or from the tool title cezar itself renders
|
|
348
|
+
* from the command it saw run. Tool output and tool input are the transcript of the world, not a
|
|
349
|
+
* statement about this run. The URL is still read from the whole event — `gh` prints it in the
|
|
350
|
+
* output — because it is the CLAIM that needs a trustworthy source, not the link.
|
|
351
|
+
*/
|
|
352
|
+
function eventCreationClaimFragments(event) {
|
|
353
|
+
const fragments = [];
|
|
354
|
+
// A `tool-result` event's `result` IS raw command output; on every other event the top-level
|
|
355
|
+
// text is the agent's own.
|
|
356
|
+
if (event.type !== 'tool-result') {
|
|
357
|
+
for (const key of ['text', 'result', 'message']) {
|
|
358
|
+
const value = event[key];
|
|
359
|
+
if (typeof value === 'string')
|
|
360
|
+
fragments.push(value);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const item = event.item;
|
|
364
|
+
if (item && typeof item === 'object') {
|
|
365
|
+
const it = item;
|
|
366
|
+
if (it.kind === 'message' && it.role === 'assistant' && typeof it.text === 'string') {
|
|
367
|
+
fragments.push(it.text);
|
|
368
|
+
}
|
|
369
|
+
if (it.kind === 'tool' && typeof it.title === 'string')
|
|
370
|
+
fragments.push(it.title);
|
|
371
|
+
}
|
|
372
|
+
return fragments;
|
|
373
|
+
}
|
|
374
|
+
/** Agent-authored event text, matching the trust boundary used by task markers.
|
|
375
|
+
* Tool titles, inputs, and outputs remain visible to the referenced-URL tier,
|
|
376
|
+
* but must never promote an issue into the shared `issueNumber` field (#538). */
|
|
377
|
+
function eventAgentTextFragments(event) {
|
|
378
|
+
const fragments = [];
|
|
379
|
+
for (const key of ['text', 'result']) {
|
|
380
|
+
const value = event[key];
|
|
381
|
+
if (typeof value === 'string')
|
|
382
|
+
fragments.push(value);
|
|
383
|
+
}
|
|
384
|
+
const item = event.item;
|
|
385
|
+
if (item && typeof item === 'object') {
|
|
386
|
+
const it = item;
|
|
387
|
+
if (it.kind === 'message' && it.role === 'assistant' && typeof it.text === 'string') {
|
|
388
|
+
fragments.push(it.text);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return fragments;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The referenced tier's resolution rule, shared by the PR and issue janitors:
|
|
395
|
+
* a marker-declared number (spec 2026-07-18-task-ref-markers) owns the answer
|
|
396
|
+
* outright — only a candidate URL ending in that number resolves, and a
|
|
397
|
+
* contradiction clears the chip. Without a declaration: one distinct URL is
|
|
398
|
+
* the subject; among several, the one whose number the task prompt names (and
|
|
399
|
+
* only when exactly one matches); otherwise ambiguous — no chip beats a wrong
|
|
400
|
+
* chip.
|
|
401
|
+
*/
|
|
402
|
+
function resolveReferencedRef(candidates, task, declared) {
|
|
403
|
+
if (declared !== undefined)
|
|
404
|
+
return candidates.find((url) => url.endsWith(`/${declared}`));
|
|
405
|
+
if (candidates.length === 1)
|
|
406
|
+
return candidates[0];
|
|
407
|
+
const named = candidates.filter((url) => {
|
|
408
|
+
const num = url.split('/').pop() ?? '';
|
|
409
|
+
// `\d` boundaries only: they reject `170` inside `4170` yet still match a
|
|
410
|
+
// number written as `#4170`, ` 4170`, or inside a pasted `…/pull/4170`.
|
|
411
|
+
return num !== '' && new RegExp(`(?<!\\d)#?${num}(?!\\d)`).test(task);
|
|
412
|
+
});
|
|
413
|
+
return named.length === 1 ? named[0] : undefined;
|
|
414
|
+
}
|
|
415
|
+
/** The number a forge URL's last segment names (`…/pull/402` → 402), or undefined. */
|
|
416
|
+
function refUrlNumber(url) {
|
|
417
|
+
if (!url)
|
|
418
|
+
return undefined;
|
|
419
|
+
const n = Number(url.split('/').pop());
|
|
420
|
+
return Number.isInteger(n) && n > 0 && n < MAX_REF ? n : undefined;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* The PR declaration the REFERENCED tier is allowed to act on.
|
|
424
|
+
*
|
|
425
|
+
* `CEZ:PR=N` means one of two things depending on when the agent writes it: on the way in it
|
|
426
|
+
* names the PR the task is ABOUT, and once the task has opened a PR of its own the marker
|
|
427
|
+
* contract asks it to re-declare with the new number ("Re-emit with the new number if the subject
|
|
428
|
+
* changes (e.g. you open a PR later in the task)"). A declaration naming the PR this run CREATED
|
|
429
|
+
* is therefore a statement about the CREATED tier, which `pullRequestUrl` already carries — and
|
|
430
|
+
* feeding it to the referenced tier ERASES the about-PR, because `resolveReferencedRef` clears
|
|
431
|
+
* the chip when no candidate matches the declared number (a task on #4326 that opened
|
|
432
|
+
* #5366 dropped from two chips to one the moment it declared #5366).
|
|
433
|
+
*
|
|
434
|
+
* Both tiers stay true instead: the created PR is the created PR, and the reference resolves as
|
|
435
|
+
* if that declaration had not been made — which is exactly what it was before the task opened
|
|
436
|
+
* anything.
|
|
437
|
+
*/
|
|
438
|
+
function referencedPrDeclaration(run) {
|
|
439
|
+
const declared = run.markerRefs?.pr;
|
|
440
|
+
if (declared === undefined)
|
|
441
|
+
return undefined;
|
|
442
|
+
return declared === refUrlNumber(run.pullRequestUrl) ? undefined : declared;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* The PR URL a creation phrase *introduces*, or undefined. The created URL is
|
|
446
|
+
* the first one at or after the `CREATED_PR_RE` phrase — a PR the same event
|
|
447
|
+
* merely referenced *earlier* (e.g. the issue's own linked `…/pull/1`) must not
|
|
448
|
+
* be mistaken for the one just created (#495). Falls back to the last URL
|
|
449
|
+
* *before* the phrase for `gh` orderings that print the URL first. Selection
|
|
450
|
+
* only — the caller decides whether creation phrasing is present.
|
|
451
|
+
*/
|
|
452
|
+
function createdPrUrl(haystack) {
|
|
453
|
+
const phrase = CREATED_PR_RE.exec(haystack);
|
|
454
|
+
if (!phrase)
|
|
455
|
+
return undefined;
|
|
456
|
+
const after = PR_URL_RE.exec(haystack.slice(phrase.index));
|
|
457
|
+
if (after)
|
|
458
|
+
return after[0];
|
|
459
|
+
let before;
|
|
460
|
+
for (const m of haystack.slice(0, phrase.index).matchAll(new RegExp(PR_URL_RE.source, 'g'))) {
|
|
461
|
+
before = m[0];
|
|
462
|
+
}
|
|
463
|
+
return before;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Reconcile one record just read off disk with the fact that whichever process wrote it is gone.
|
|
467
|
+
*
|
|
468
|
+
* Mutates and returns `run`. Extracted from `RunStore.open` so the read-only index reader
|
|
469
|
+
* (`./run-index.ts`) answers the SAME question about a `running` row on disk. Two parsers that
|
|
470
|
+
* disagree here is a visible bug, not an internal one: the cockpit would show a task as running
|
|
471
|
+
* in the ⌘K index and failed the moment you opened it.
|
|
472
|
+
*
|
|
473
|
+
* `keepLive` (#367): leave `queued`/`running`/`waiting` untouched so the caller can recover them
|
|
474
|
+
* (RunManager.recover re-queues queued runs, resumes interrupted ones). Without it — one-shot CLI
|
|
475
|
+
* paths that never recover, and the index reader, which has no manager at all — live-looking runs
|
|
476
|
+
* are marked failed so no ghost stays behind.
|
|
477
|
+
*/
|
|
478
|
+
export function reconcileLoadedRun(run, opts) {
|
|
479
|
+
// A run that was live when the previous process exited can never finish —
|
|
480
|
+
// surface that instead of a forever-"running" ghost. `review` survives
|
|
481
|
+
// restarts on purpose: the gate is pure data (worktree + branch + record)
|
|
482
|
+
// with no live process, so the diff panel, Send back (resume) and Draft PR
|
|
483
|
+
// all still work.
|
|
484
|
+
if (!opts?.keepLive &&
|
|
485
|
+
(run.status === 'running' || run.status === 'queued' || run.status === 'waiting')) {
|
|
486
|
+
run.status = 'failed';
|
|
487
|
+
run.error = 'interrupted — cezar process exited during the run';
|
|
488
|
+
run.finishedAt = run.finishedAt ?? new Date().toISOString();
|
|
489
|
+
for (const step of run.steps) {
|
|
490
|
+
if (step.status === 'running' || step.status === 'waiting')
|
|
491
|
+
step.status = 'failed';
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (!['running', 'waiting', 'queued'].includes(run.status)) {
|
|
495
|
+
run.activity = undefined;
|
|
496
|
+
run.monitoringWakeAt = undefined;
|
|
497
|
+
}
|
|
498
|
+
// A pending usage-limit resume survives the restart on purpose (the wait can be
|
|
499
|
+
// hours) — `RunManager.recover()` re-arms it from this field. It can only mean
|
|
500
|
+
// anything on a `failed` run, so anywhere else it is stale bookkeeping.
|
|
501
|
+
if (run.status !== 'failed')
|
|
502
|
+
run.autoResumeAt = undefined;
|
|
503
|
+
// The wake counter is intentionally process-local, so a restarted process
|
|
504
|
+
// starts a fresh epoch instead of displaying a stale cap.
|
|
505
|
+
run.monitoringWakeCapReached = undefined;
|
|
506
|
+
// Heal a record written before `referencedPrDeclaration` existed: a task that re-declared
|
|
507
|
+
// `CEZ:PR` with the PR it had just CREATED cleared the PR it was ABOUT, because no candidate
|
|
508
|
+
// could match the created number. The evidence is all still on the record — only the
|
|
509
|
+
// conclusion drawn from it was wrong — so re-resolve without that declaration instead of
|
|
510
|
+
// asking for a migration. Deliberately one-directional: it only runs on a record that HAS no
|
|
511
|
+
// referenced PR, so it can never take one away from a record written by an older cezar whose
|
|
512
|
+
// candidate list no longer explains it. `prNumber` is not recoverable this way (the
|
|
513
|
+
// declaration overwrote it) and is left alone — the restored URL is what paints the chip.
|
|
514
|
+
if (run.referencedPullRequestUrl === undefined &&
|
|
515
|
+
run.markerRefs?.pr !== undefined &&
|
|
516
|
+
referencedPrDeclaration(run) === undefined) {
|
|
517
|
+
run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, undefined);
|
|
518
|
+
}
|
|
519
|
+
return run;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* File-backed run store: `runs.json` index (atomic tmp+rename writes, the
|
|
523
|
+
* pattern from @cezar/core's IssueStore) plus one append-only NDJSON event
|
|
524
|
+
* file per run. Also the in-process event bus the SSE endpoints subscribe to:
|
|
525
|
+
* emits `('run', RunRecord)` and `('event', { runId, event: RunEvent })`.
|
|
526
|
+
*/
|
|
527
|
+
export class RunStore extends EventEmitter {
|
|
528
|
+
dataDir;
|
|
529
|
+
runs = new Map();
|
|
530
|
+
saveTimer = null;
|
|
531
|
+
constructor(dataDir) {
|
|
532
|
+
super();
|
|
533
|
+
this.dataDir = dataDir;
|
|
534
|
+
this.setMaxListeners(100);
|
|
535
|
+
}
|
|
536
|
+
/** See `reconcileLoadedRun` for what `keepLive` (#367) decides about live-looking rows. */
|
|
537
|
+
static open(dataDir, opts) {
|
|
538
|
+
mkdirSync(join(dataDir, 'runs'), { recursive: true });
|
|
539
|
+
const store = new RunStore(dataDir);
|
|
540
|
+
const indexPath = join(dataDir, 'runs.json');
|
|
541
|
+
if (existsSync(indexPath)) {
|
|
542
|
+
try {
|
|
543
|
+
const raw = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
544
|
+
const parsed = z.array(runRecordSchema).safeParse(raw);
|
|
545
|
+
if (parsed.success) {
|
|
546
|
+
for (const run of parsed.data) {
|
|
547
|
+
store.runs.set(run.id, reconcileLoadedRun(run, opts));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
// corrupt index — start fresh; event files stay on disk untouched
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return store;
|
|
556
|
+
}
|
|
557
|
+
listRuns() {
|
|
558
|
+
return [...this.runs.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
559
|
+
}
|
|
560
|
+
getRun(id) {
|
|
561
|
+
return this.runs.get(id);
|
|
562
|
+
}
|
|
563
|
+
createRun(input) {
|
|
564
|
+
const run = {
|
|
565
|
+
id: randomUUID(),
|
|
566
|
+
// Scrubbed on the way in, exactly as `updateRun` scrubs it on the way
|
|
567
|
+
// through (#456 review) — a token pasted into the prompt otherwise sat
|
|
568
|
+
// verbatim in `runs.json` from creation. `task` is deliberately NOT
|
|
569
|
+
// scrubbed: it is the user's own prompt and is replayed into `{{task}}`
|
|
570
|
+
// when a queued run is revived after a restart (#367), so redacting it
|
|
571
|
+
// would corrupt the revived run.
|
|
572
|
+
title: this.redactText(input.title),
|
|
573
|
+
workflow: input.workflow,
|
|
574
|
+
task: input.task,
|
|
575
|
+
model: input.model,
|
|
576
|
+
runner: input.runner,
|
|
577
|
+
agentProfile: input.agentProfile,
|
|
578
|
+
generateFollowups: input.generateFollowups,
|
|
579
|
+
autonomous: input.autonomous,
|
|
580
|
+
worktree: input.worktree,
|
|
581
|
+
groupId: input.groupId,
|
|
582
|
+
variant: input.variant,
|
|
583
|
+
status: 'queued',
|
|
584
|
+
createdAt: new Date().toISOString(),
|
|
585
|
+
tokensUsed: 0,
|
|
586
|
+
archived: false,
|
|
587
|
+
steps: input.steps.map((s) => ({
|
|
588
|
+
...s,
|
|
589
|
+
status: 'pending',
|
|
590
|
+
iterations: 0,
|
|
591
|
+
tokensUsed: 0,
|
|
592
|
+
})),
|
|
593
|
+
};
|
|
594
|
+
// A prompt that pastes a PR or issue URL is already about that item — seed
|
|
595
|
+
// both referenced tiers so queued runs can expose the reference before the
|
|
596
|
+
// first agent event (#407, #554).
|
|
597
|
+
this.trackReferencedPrs(run, input.task);
|
|
598
|
+
this.trackReferencedIssues(run, input.task);
|
|
599
|
+
this.runs.set(run.id, run);
|
|
600
|
+
this.pruneOldRuns();
|
|
601
|
+
this.touch(run);
|
|
602
|
+
return run;
|
|
603
|
+
}
|
|
604
|
+
updateRun(id, patch) {
|
|
605
|
+
const run = this.runs.get(id);
|
|
606
|
+
if (!run)
|
|
607
|
+
return undefined;
|
|
608
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'issueNumber')) {
|
|
609
|
+
delete run.referencedIssueNumberSeeded;
|
|
610
|
+
}
|
|
611
|
+
const normalized = { ...patch };
|
|
612
|
+
if (normalized.status && !['running', 'waiting', 'queued'].includes(normalized.status)) {
|
|
613
|
+
normalized.activity = undefined;
|
|
614
|
+
normalized.monitoringWakeAt = undefined;
|
|
615
|
+
normalized.monitoringWakeCapReached = undefined;
|
|
616
|
+
}
|
|
617
|
+
// …and the mirror image for the usage-limit resume (spec
|
|
618
|
+
// 2026-08-03-auto-resume-after-usage-limit): it is a promise made ABOUT a failed run, so a
|
|
619
|
+
// run coming back to life — the resume itself, a user Continue, a re-queue — retires it.
|
|
620
|
+
// The manager's timer re-checks the record before it fires, so a cleared field is enough.
|
|
621
|
+
if (normalized.status && ['running', 'waiting', 'queued'].includes(normalized.status)) {
|
|
622
|
+
normalized.autoResumeAt = undefined;
|
|
623
|
+
}
|
|
624
|
+
Object.assign(run, this.redactPatch(normalized));
|
|
625
|
+
this.touch(run);
|
|
626
|
+
return run;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Scrub the free-text fields of a record patch (#427 review). Redacting only
|
|
630
|
+
* events left a hole: `titleSummary` is derived from the RAW first agent turn
|
|
631
|
+
* and `error` from raw process output, so a token the agent echoed was
|
|
632
|
+
* `[REDACTED]` in the NDJSON yet verbatim in `runs.json` — the file the "no
|
|
633
|
+
* secrets in state files" rule names explicitly. These three are the only
|
|
634
|
+
* patch fields carrying agent/process text; the rest are ids, enums, counters
|
|
635
|
+
* and URLs, and running the scrubber over them would only risk mangling them.
|
|
636
|
+
*
|
|
637
|
+
* `StepState.error` is the step-level counterpart and is scrubbed the same
|
|
638
|
+
* way in `updateStep` — `run.ts` feeds the SAME `err.message` string to both
|
|
639
|
+
* calls, so redacting only the run-level copy left the token verbatim one
|
|
640
|
+
* field away (#456 review).
|
|
641
|
+
*/
|
|
642
|
+
redactPatch(patch) {
|
|
643
|
+
if (process.env.CEZ_REDACT_SECRETS === '0')
|
|
644
|
+
return patch;
|
|
645
|
+
const out = { ...patch };
|
|
646
|
+
for (const field of ['title', 'titleSummary', 'error']) {
|
|
647
|
+
const value = out[field];
|
|
648
|
+
if (typeof value === 'string')
|
|
649
|
+
out[field] = this.redactText(value);
|
|
650
|
+
}
|
|
651
|
+
return out;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Step-level counterpart of `redactPatch` (#456 review). `error` is the only
|
|
655
|
+
* free-text `StepState` field — it is set from raw `err.message` /process
|
|
656
|
+
* output (`run.ts` `finishStep`), and `touch()` fans the whole record out
|
|
657
|
+
* over SSE, so an unscrubbed copy leaked to `runs.json` AND to the browser.
|
|
658
|
+
* The remaining fields are ids, enums, counters and timestamps.
|
|
659
|
+
*/
|
|
660
|
+
redactStepPatch(patch) {
|
|
661
|
+
if (process.env.CEZ_REDACT_SECRETS === '0')
|
|
662
|
+
return patch;
|
|
663
|
+
if (typeof patch.error !== 'string')
|
|
664
|
+
return patch;
|
|
665
|
+
return { ...patch, error: this.redactText(patch.error) };
|
|
666
|
+
}
|
|
667
|
+
/** Append a step to an existing run (used by "Continue" — spec 003). */
|
|
668
|
+
addStep(runId, step) {
|
|
669
|
+
const run = this.runs.get(runId);
|
|
670
|
+
if (!run || run.steps.some((s) => s.id === step.id))
|
|
671
|
+
return;
|
|
672
|
+
run.steps.push({ ...step, status: 'pending', iterations: 0, tokensUsed: 0 });
|
|
673
|
+
this.touch(run);
|
|
674
|
+
}
|
|
675
|
+
updateStep(runId, stepId, patch) {
|
|
676
|
+
const run = this.runs.get(runId);
|
|
677
|
+
const step = run?.steps.find((s) => s.id === stepId);
|
|
678
|
+
if (!run || !step)
|
|
679
|
+
return;
|
|
680
|
+
Object.assign(step, this.redactStepPatch(patch));
|
|
681
|
+
run.tokensUsed = run.steps.reduce((sum, s) => sum + s.tokensUsed, 0);
|
|
682
|
+
const startedAgentSteps = run.steps.filter((candidate) => candidate.kind === 'agent' && candidate.iterations > 0);
|
|
683
|
+
const directionalComplete = startedAgentSteps.length > 0 &&
|
|
684
|
+
startedAgentSteps.every((candidate) => candidate.usageInvocationsStarted !== undefined &&
|
|
685
|
+
candidate.usageInvocationsObserved !== undefined &&
|
|
686
|
+
candidate.usageInvocationsObserved > 0 &&
|
|
687
|
+
candidate.usageInvocationsStarted === candidate.usageInvocationsObserved &&
|
|
688
|
+
candidate.usageTurnsStarted !== undefined &&
|
|
689
|
+
candidate.usageTurnsRecorded !== undefined &&
|
|
690
|
+
candidate.usageTurnsStarted > 0 &&
|
|
691
|
+
candidate.usageTurnsStarted === candidate.usageTurnsRecorded &&
|
|
692
|
+
candidate.inputTokens !== undefined &&
|
|
693
|
+
candidate.outputTokens !== undefined);
|
|
694
|
+
run.inputTokens = directionalComplete
|
|
695
|
+
? startedAgentSteps.reduce((sum, candidate) => sum + (candidate.inputTokens ?? 0), 0)
|
|
696
|
+
: undefined;
|
|
697
|
+
run.outputTokens = directionalComplete
|
|
698
|
+
? startedAgentSteps.reduce((sum, candidate) => sum + (candidate.outputTokens ?? 0), 0)
|
|
699
|
+
: undefined;
|
|
700
|
+
const cost = run.steps.reduce((sum, s) => sum + (s.costUsd ?? 0), 0);
|
|
701
|
+
run.costUsd = cost > 0 ? cost : undefined;
|
|
702
|
+
this.touch(run);
|
|
703
|
+
}
|
|
704
|
+
setArchived(id, archived) {
|
|
705
|
+
const run = this.runs.get(id);
|
|
706
|
+
if (!run)
|
|
707
|
+
return undefined;
|
|
708
|
+
run.archived = archived;
|
|
709
|
+
run.archivedAt = archived ? new Date().toISOString() : undefined;
|
|
710
|
+
if (archived)
|
|
711
|
+
clearPendingAutoResume(run);
|
|
712
|
+
this.touch(run);
|
|
713
|
+
return run;
|
|
714
|
+
}
|
|
715
|
+
/** Bulk-archive every finished run; returns how many were archived. */
|
|
716
|
+
archiveFinished() {
|
|
717
|
+
let count = 0;
|
|
718
|
+
for (const run of this.runs.values()) {
|
|
719
|
+
if (!run.archived && ['done', 'failed', 'cancelled'].includes(run.status)) {
|
|
720
|
+
run.archived = true;
|
|
721
|
+
run.archivedAt = new Date().toISOString();
|
|
722
|
+
clearPendingAutoResume(run);
|
|
723
|
+
this.touch(run);
|
|
724
|
+
count++;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return count;
|
|
728
|
+
}
|
|
729
|
+
/** Mark one run as read (#unread-done-items): stamp the read receipt now. Mirrors
|
|
730
|
+
* `setArchived` — sets the field then persists + broadcasts via `touch`, so the
|
|
731
|
+
* updated record rides the existing `run` SSE with no new event. Idempotent by
|
|
732
|
+
* design: opening an already-read thread just re-stamps a later `seenAt`. */
|
|
733
|
+
setRead(id) {
|
|
734
|
+
const run = this.runs.get(id);
|
|
735
|
+
if (!run)
|
|
736
|
+
return undefined;
|
|
737
|
+
run.seenAt = new Date().toISOString();
|
|
738
|
+
this.touch(run);
|
|
739
|
+
return run;
|
|
740
|
+
}
|
|
741
|
+
/** Mark one run as UNread (#775): drop the read receipt so the run rejoins the unread
|
|
742
|
+
* list. The inverse of `setRead` and, like it, `touch`es so the updated record rides the
|
|
743
|
+
* existing `run` SSE.
|
|
744
|
+
*
|
|
745
|
+
* Deleting the field rather than adding a "manually unread" flag is the whole point:
|
|
746
|
+
* absent `seenAt` is ALREADY what every reader treats as unread (`isUnread` in the
|
|
747
|
+
* cockpit's read-state.ts, and `markAllRead`'s clause-for-clause copy of it below), so
|
|
748
|
+
* clearing needs no new state and writes a shape any older cezar already parses.
|
|
749
|
+
*
|
|
750
|
+
* Deliberately unconditional: clearing a receipt is always a legal write, so this
|
|
751
|
+
* succeeds for an already-unread run (idempotent) and for statuses that can never wear
|
|
752
|
+
* the marker. WHETHER the action means anything for a given run is UI policy, and lives
|
|
753
|
+
* in the cockpit's `runActionFlags` — the same split the rest of the store keeps. */
|
|
754
|
+
setUnread(id) {
|
|
755
|
+
const run = this.runs.get(id);
|
|
756
|
+
if (!run)
|
|
757
|
+
return undefined;
|
|
758
|
+
delete run.seenAt;
|
|
759
|
+
this.touch(run);
|
|
760
|
+
return run;
|
|
761
|
+
}
|
|
762
|
+
/** Bulk mark-read: stamp every currently-unread finished run; returns the count.
|
|
763
|
+
* "Unread" here is the same rule the cockpit paints (`isUnread` in read-state.ts),
|
|
764
|
+
* clause for clause:
|
|
765
|
+
* - a `done` or `failed` run that finished and has not been seen since;
|
|
766
|
+
* - cancelled runs are never unread — you stopped them yourself;
|
|
767
|
+
* - archived ones never are either, since archiving is a stronger "done with this"
|
|
768
|
+
* than reading;
|
|
769
|
+
* - and a `failed` run with a pending `autoResumeAt` is not a done item AT ALL
|
|
770
|
+
* (`isScheduledResume`, spec 2026-08-03-auto-resume-after-usage-limit): it has an
|
|
771
|
+
* appointment to pick the work back up, so there is no outcome to have missed.
|
|
772
|
+
*
|
|
773
|
+
* Keeping the two rules identical is what makes the returned count the number the
|
|
774
|
+
* cockpit's unread badge was showing. The `autoResumeAt` clause is the one that drifted
|
|
775
|
+
* (#803): `isUnread` gained it with auto-resume and this sweep did not, so a task waiting
|
|
776
|
+
* out a usage limit was uncounted by the badge but stamped read by the sweep — and this
|
|
777
|
+
* comment asserted an invariant the code no longer held.
|
|
778
|
+
*
|
|
779
|
+
* This rule lives in two languages of the same repo, which is why it has now drifted
|
|
780
|
+
* once. The cockpit cannot import it (`packages/web` does not depend on the service, and
|
|
781
|
+
* should not), so a single definition would have to move to `packages/contract` — the one
|
|
782
|
+
* package both sides already import. Worth doing; deliberately not done here, because
|
|
783
|
+
* widening the contract package's remit from "shapes" to "behavior" is a design change
|
|
784
|
+
* that deserves its own review rather than riding along in a bug fix. Until then: EDIT
|
|
785
|
+
* BOTH, and the case-table tests on either side are what catch you if you don't. */
|
|
786
|
+
markAllRead() {
|
|
787
|
+
const now = new Date().toISOString();
|
|
788
|
+
let count = 0;
|
|
789
|
+
for (const run of this.runs.values()) {
|
|
790
|
+
const unread = !run.archived &&
|
|
791
|
+
(run.status === 'done' || run.status === 'failed') &&
|
|
792
|
+
!(run.status === 'failed' && run.autoResumeAt !== undefined) &&
|
|
793
|
+
run.finishedAt !== undefined &&
|
|
794
|
+
(run.seenAt === undefined || run.seenAt < run.finishedAt);
|
|
795
|
+
if (!unread)
|
|
796
|
+
continue;
|
|
797
|
+
run.seenAt = now;
|
|
798
|
+
this.touch(run);
|
|
799
|
+
count++;
|
|
800
|
+
}
|
|
801
|
+
return count;
|
|
802
|
+
}
|
|
803
|
+
appendEvent(runId, event) {
|
|
804
|
+
const run = this.runs.get(runId);
|
|
805
|
+
if (!run)
|
|
806
|
+
throw new Error(`unknown run: ${runId}`);
|
|
807
|
+
const seq = this.nextSeq(runId);
|
|
808
|
+
// Scrub credentials before the event touches disk or the live wire (#427):
|
|
809
|
+
// tool-result output is persisted verbatim and served back over the API, so
|
|
810
|
+
// a secret in an agent's command output would otherwise land in `.ai/cezar/`.
|
|
811
|
+
const full = this.redact({ ...event, seq, ts: new Date().toISOString() });
|
|
812
|
+
// Sync append keeps event order without a write queue; local NDJSON
|
|
813
|
+
// appends at agent-event rates are effectively free.
|
|
814
|
+
appendFileSync(this.eventsPath(runId), `${JSON.stringify(full)}\n`, 'utf8');
|
|
815
|
+
this.emit('event', { runId, event: full });
|
|
816
|
+
// The janitor trick: agents print the PR URL after `gh pr create` — the
|
|
817
|
+
// first one spotted in the transcript becomes the run's PR link. Scans v1
|
|
818
|
+
// fields AND nested v2 `item.*` content (#407). A URL without the created
|
|
819
|
+
// phrasing still feeds the referenced tier (the PR the task is about) —
|
|
820
|
+
// and the phrasing itself is only believed from a source that can speak
|
|
821
|
+
// FOR this run (`eventCreationClaimFragments`), never from quoted output.
|
|
822
|
+
const haystack = eventTextFragments(full).join(' ');
|
|
823
|
+
const agentHaystack = eventAgentTextFragments(full).join(' ');
|
|
824
|
+
// The creation CLAIM is read from a narrower source than the URL is
|
|
825
|
+
// (`eventCreationClaimFragments`), which is why the two are searched
|
|
826
|
+
// together rather than the haystack alone: the phrase must land in the
|
|
827
|
+
// trusted prefix, and the link may come from anywhere after it.
|
|
828
|
+
const claim = eventCreationClaimFragments(full).join(' ');
|
|
829
|
+
if (haystack.length > 0) {
|
|
830
|
+
let changed = false;
|
|
831
|
+
if (!run.pullRequestUrl) {
|
|
832
|
+
const created = CREATED_PR_RE.test(claim) ? createdPrUrl(`${claim} ${haystack}`) : undefined;
|
|
833
|
+
if (created) {
|
|
834
|
+
this.updateRun(runId, { pullRequestUrl: created });
|
|
835
|
+
// Adopting the created tier can RELEASE a declaration the referenced tier was holding
|
|
836
|
+
// (see `referencedPrDeclaration`), so re-resolve here too: the about-PR must come back
|
|
837
|
+
// whether the marker arrived before the creation evidence or after it.
|
|
838
|
+
const resolved = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, referencedPrDeclaration(run));
|
|
839
|
+
if (resolved !== run.referencedPullRequestUrl) {
|
|
840
|
+
run.referencedPullRequestUrl = resolved;
|
|
841
|
+
changed = true;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
else if (PR_URL_RE.test(haystack) && this.trackReferencedPrs(run, haystack)) {
|
|
845
|
+
changed = true;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// Issue links feed their own referenced tier regardless of PR state —
|
|
849
|
+
// a task that created a PR can still be ABOUT an issue
|
|
850
|
+
// (spec 2026-07-21-report-ref-discovery).
|
|
851
|
+
if (ISSUE_URL_RE.test(haystack) &&
|
|
852
|
+
this.trackReferencedIssues(run, haystack, agentHaystack)) {
|
|
853
|
+
changed = true;
|
|
854
|
+
}
|
|
855
|
+
if (changed)
|
|
856
|
+
this.touch(run);
|
|
857
|
+
}
|
|
858
|
+
return full;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Fold every PR URL in `haystack` into the run's referenced-tier working
|
|
862
|
+
* set and re-resolve `referencedPullRequestUrl` (spec
|
|
863
|
+
* 2026-07-16-pr-autodiscovery). Mutates the record in place — the caller
|
|
864
|
+
* owns persistence/fan-out — and reports whether anything changed.
|
|
865
|
+
*/
|
|
866
|
+
trackReferencedPrs(run, haystack) {
|
|
867
|
+
const seen = new Set(run.referencedPrCandidates ?? []);
|
|
868
|
+
const before = seen.size;
|
|
869
|
+
for (const match of haystack.matchAll(new RegExp(PR_URL_RE.source, 'g'))) {
|
|
870
|
+
if (seen.size >= MAX_PR_CANDIDATES)
|
|
871
|
+
break;
|
|
872
|
+
seen.add(match[0]);
|
|
873
|
+
}
|
|
874
|
+
if (seen.size === before)
|
|
875
|
+
return false;
|
|
876
|
+
run.referencedPrCandidates = [...seen];
|
|
877
|
+
run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates, run.task, referencedPrDeclaration(run));
|
|
878
|
+
return true;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* The issue-side mirror of `trackReferencedPrs` (spec
|
|
882
|
+
* 2026-07-21-report-ref-discovery): fold every issue URL in `haystack` into
|
|
883
|
+
* the working set and re-resolve `referencedIssueUrl`. An unambiguous
|
|
884
|
+
* resolution also seeds `issueNumber` when nothing owns that field yet —
|
|
885
|
+
* marker and namer both outrank this janitor and overwrite it freely.
|
|
886
|
+
*/
|
|
887
|
+
trackReferencedIssues(run, haystack, seedHaystack = haystack) {
|
|
888
|
+
const seen = new Set(run.referencedIssueCandidates ?? []);
|
|
889
|
+
const before = seen.size;
|
|
890
|
+
for (const match of haystack.matchAll(new RegExp(ISSUE_URL_RE.source, 'g'))) {
|
|
891
|
+
if (seen.size >= MAX_PR_CANDIDATES)
|
|
892
|
+
break;
|
|
893
|
+
seen.add(match[0]);
|
|
894
|
+
}
|
|
895
|
+
const candidatesChanged = seen.size !== before;
|
|
896
|
+
if (candidatesChanged)
|
|
897
|
+
run.referencedIssueCandidates = [...seen];
|
|
898
|
+
const prev = run.referencedIssueUrl;
|
|
899
|
+
run.referencedIssueUrl = resolveReferencedRef(run.referencedIssueCandidates ?? [], run.task, run.markerRefs?.issue);
|
|
900
|
+
let numberChanged = false;
|
|
901
|
+
if (run.markerRefs?.issue === undefined && ISSUE_URL_RE.test(seedHaystack)) {
|
|
902
|
+
if (run.referencedIssueUrl && run.issueNumber === undefined) {
|
|
903
|
+
const n = Number(run.referencedIssueUrl.split('/').pop());
|
|
904
|
+
if (Number.isInteger(n) && n > 0) {
|
|
905
|
+
run.issueNumber = n;
|
|
906
|
+
run.referencedIssueNumberSeeded = true;
|
|
907
|
+
numberChanged = true;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
else if (!run.referencedIssueUrl && prev && run.referencedIssueNumberSeeded) {
|
|
911
|
+
// Ambiguity revoked the resolution — take back the number this janitor
|
|
912
|
+
// seeded from it. No chip beats a wrong chip.
|
|
913
|
+
delete run.issueNumber;
|
|
914
|
+
delete run.referencedIssueNumberSeeded;
|
|
915
|
+
numberChanged = true;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return candidatesChanged || run.referencedIssueUrl !== prev || numberChanged;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Apply agent-declared reference markers (spec 2026-07-18-task-ref-markers).
|
|
922
|
+
* Marker values are authoritative for the display tier: they overwrite the
|
|
923
|
+
* regex/namer numbers, and a declared PR re-resolves the referenced URL
|
|
924
|
+
* against the candidate working set — including down to `undefined` when no
|
|
925
|
+
* candidate matches (a wrong chip is worse than no chip). The created tier
|
|
926
|
+
* (`pullRequestUrl`) is deliberately untouched.
|
|
927
|
+
*
|
|
928
|
+
* One declaration is NOT a statement about the referenced tier: the number of the PR this run
|
|
929
|
+
* itself created. See `referencedPrDeclaration` — the marker contract asks the agent to
|
|
930
|
+
* re-declare after it opens a PR, and taking that literally cost the task the PR it was about.
|
|
931
|
+
*/
|
|
932
|
+
applyMarkerRefs(runId, refs) {
|
|
933
|
+
const run = this.runs.get(runId);
|
|
934
|
+
if (!run || (refs.pr === undefined && refs.issue === undefined))
|
|
935
|
+
return run;
|
|
936
|
+
run.markerRefs = {
|
|
937
|
+
...run.markerRefs,
|
|
938
|
+
...(refs.pr !== undefined ? { pr: refs.pr } : {}),
|
|
939
|
+
...(refs.issue !== undefined ? { issue: refs.issue } : {}),
|
|
940
|
+
};
|
|
941
|
+
// `prNumber` is the about-PR as well (it is what paints a numeric-only chip), so a
|
|
942
|
+
// re-declaration naming the created PR only FILLS it — it never overwrites the number the
|
|
943
|
+
// task came in with, which is still the PR this task is about.
|
|
944
|
+
if (refs.pr !== undefined && (run.prNumber === undefined || refs.pr !== refUrlNumber(run.pullRequestUrl))) {
|
|
945
|
+
run.prNumber = refs.pr;
|
|
946
|
+
}
|
|
947
|
+
if (refs.issue !== undefined) {
|
|
948
|
+
run.issueNumber = refs.issue;
|
|
949
|
+
delete run.referencedIssueNumberSeeded;
|
|
950
|
+
}
|
|
951
|
+
if (run.markerRefs.pr !== undefined) {
|
|
952
|
+
run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, referencedPrDeclaration(run));
|
|
953
|
+
}
|
|
954
|
+
if (run.markerRefs.issue !== undefined) {
|
|
955
|
+
run.referencedIssueUrl = resolveReferencedRef(run.referencedIssueCandidates ?? [], run.task, run.markerRefs.issue);
|
|
956
|
+
}
|
|
957
|
+
this.touch(run);
|
|
958
|
+
return run;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Fan an event out to live subscribers WITHOUT writing it to the NDJSON
|
|
962
|
+
* file — the channel for coalesced `item.delta` flushes (protocol-v2
|
|
963
|
+
* performance guardrail: raw deltas never hit disk; replay = the persisted
|
|
964
|
+
* snapshots). Stamped with `seq`/`ts` like persisted lines so the live
|
|
965
|
+
* wire keeps one ordering axis; the seq simply never appears in a replay
|
|
966
|
+
* (gaps are fine — dedup compares with `>`).
|
|
967
|
+
*/
|
|
968
|
+
emitEphemeral(runId, event) {
|
|
969
|
+
const full = this.redact({ ...event, seq: this.nextSeq(runId), ts: new Date().toISOString() });
|
|
970
|
+
this.emit('event', { runId, event: full });
|
|
971
|
+
return full;
|
|
972
|
+
}
|
|
973
|
+
/** Lazily-collected concrete secret values from the host env (#427). */
|
|
974
|
+
secretValues = null;
|
|
975
|
+
/**
|
|
976
|
+
* Scrub known credential values / token shapes from an event before it is
|
|
977
|
+
* persisted or fanned out. On by default; `CEZ_REDACT_SECRETS=0` opts out.
|
|
978
|
+
*/
|
|
979
|
+
redact(event) {
|
|
980
|
+
if (process.env.CEZ_REDACT_SECRETS === '0')
|
|
981
|
+
return event;
|
|
982
|
+
return redactDeep(event, this.hostSecrets());
|
|
983
|
+
}
|
|
984
|
+
/** Best-effort scrub of one free-text string bound for `runs.json`. Honors
|
|
985
|
+
* the `CEZ_REDACT_SECRETS=0` opt-out itself so every caller inherits it. */
|
|
986
|
+
redactText(text) {
|
|
987
|
+
if (process.env.CEZ_REDACT_SECRETS === '0')
|
|
988
|
+
return text;
|
|
989
|
+
return redactSecrets(text, this.hostSecrets());
|
|
990
|
+
}
|
|
991
|
+
hostSecrets() {
|
|
992
|
+
if (this.secretValues === null)
|
|
993
|
+
this.secretValues = collectSecretValues();
|
|
994
|
+
return this.secretValues;
|
|
995
|
+
}
|
|
996
|
+
readEvents(runId) {
|
|
997
|
+
try {
|
|
998
|
+
const raw = readFileSync(this.eventsPath(runId), 'utf8');
|
|
999
|
+
return raw
|
|
1000
|
+
.split('\n')
|
|
1001
|
+
.filter(Boolean)
|
|
1002
|
+
.map((line) => {
|
|
1003
|
+
try {
|
|
1004
|
+
return JSON.parse(line);
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
})
|
|
1010
|
+
.filter((e) => e !== null);
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
return [];
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
deleteRun(id) {
|
|
1017
|
+
const existed = this.runs.delete(id);
|
|
1018
|
+
if (existed) {
|
|
1019
|
+
try {
|
|
1020
|
+
rmSync(this.eventsPath(id), { force: true });
|
|
1021
|
+
rmSync(this.handoffPath(id), { force: true }); // spec 007: the journal goes with the task
|
|
1022
|
+
rmSync(this.imagesDir(id), { recursive: true, force: true }); // agent screenshots
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
// best effort — the index is authoritative
|
|
1026
|
+
}
|
|
1027
|
+
this.seqs.delete(id);
|
|
1028
|
+
this.scheduleSave();
|
|
1029
|
+
this.emit('deleted', id);
|
|
1030
|
+
}
|
|
1031
|
+
return existed;
|
|
1032
|
+
}
|
|
1033
|
+
/** Write the index out now (used on shutdown). */
|
|
1034
|
+
flush() {
|
|
1035
|
+
if (this.saveTimer) {
|
|
1036
|
+
clearTimeout(this.saveTimer);
|
|
1037
|
+
this.saveTimer = null;
|
|
1038
|
+
}
|
|
1039
|
+
this.saveNow();
|
|
1040
|
+
}
|
|
1041
|
+
// ---- internals -----------------------------------------------------------
|
|
1042
|
+
seqs = new Map();
|
|
1043
|
+
nextSeq(runId) {
|
|
1044
|
+
const next = (this.seqs.get(runId) ?? this.rehydrateSeq(runId)) + 1;
|
|
1045
|
+
this.seqs.set(runId, next);
|
|
1046
|
+
return next;
|
|
1047
|
+
}
|
|
1048
|
+
/** After a restart the in-memory counter is empty while the run's NDJSON file
|
|
1049
|
+
* keeps the history. Restarting from 1 would collide with the seqs a client
|
|
1050
|
+
* already replayed — its `seq > maxSeq` dedup then silently drops every
|
|
1051
|
+
* resumed event, even across a reload (the frozen-transcript symptom class
|
|
1052
|
+
* of #424). One file read on the first post-restart append per run. */
|
|
1053
|
+
rehydrateSeq(runId) {
|
|
1054
|
+
let max = 0;
|
|
1055
|
+
for (const event of this.readEvents(runId)) {
|
|
1056
|
+
if (typeof event.seq === 'number' && event.seq > max)
|
|
1057
|
+
max = event.seq;
|
|
1058
|
+
}
|
|
1059
|
+
return max;
|
|
1060
|
+
}
|
|
1061
|
+
eventsPath(runId) {
|
|
1062
|
+
return join(this.dataDir, 'runs', `${runId}.ndjson`);
|
|
1063
|
+
}
|
|
1064
|
+
/** Same location `handoffPath()` in handoff.ts produces — inlined to keep
|
|
1065
|
+
* the store free of upward imports. */
|
|
1066
|
+
handoffPath(runId) {
|
|
1067
|
+
return join(this.dataDir, 'runs', `${runId}.handoff.md`);
|
|
1068
|
+
}
|
|
1069
|
+
/** Agent screenshots persisted by the run manager (see persistImage). */
|
|
1070
|
+
imagesDir(runId) {
|
|
1071
|
+
return join(this.dataDir, 'runs', `${runId}-images`);
|
|
1072
|
+
}
|
|
1073
|
+
touch(run) {
|
|
1074
|
+
this.scheduleSave();
|
|
1075
|
+
this.emit('run', run);
|
|
1076
|
+
}
|
|
1077
|
+
pruneOldRuns() {
|
|
1078
|
+
const all = this.listRuns();
|
|
1079
|
+
const stalePool = [
|
|
1080
|
+
...all.filter((r) => !r.archived).slice(MAX_RUNS_KEPT),
|
|
1081
|
+
...all.filter((r) => r.archived).slice(MAX_ARCHIVED_KEPT),
|
|
1082
|
+
];
|
|
1083
|
+
for (const stale of stalePool) {
|
|
1084
|
+
this.runs.delete(stale.id);
|
|
1085
|
+
try {
|
|
1086
|
+
rmSync(this.eventsPath(stale.id), { force: true });
|
|
1087
|
+
rmSync(this.handoffPath(stale.id), { force: true });
|
|
1088
|
+
rmSync(this.imagesDir(stale.id), { recursive: true, force: true });
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
// best effort
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
/** Debounced so token-usage updates don't rewrite the index per event. */
|
|
1096
|
+
scheduleSave() {
|
|
1097
|
+
if (this.saveTimer)
|
|
1098
|
+
return;
|
|
1099
|
+
this.saveTimer = setTimeout(() => {
|
|
1100
|
+
this.saveTimer = null;
|
|
1101
|
+
this.saveNow();
|
|
1102
|
+
}, 300);
|
|
1103
|
+
this.saveTimer.unref?.();
|
|
1104
|
+
}
|
|
1105
|
+
saveNow() {
|
|
1106
|
+
const indexPath = join(this.dataDir, 'runs.json');
|
|
1107
|
+
const tmpPath = `${indexPath}.tmp`;
|
|
1108
|
+
try {
|
|
1109
|
+
writeFileSync(tmpPath, JSON.stringify(this.listRuns(), null, 2), 'utf8');
|
|
1110
|
+
renameSync(tmpPath, indexPath);
|
|
1111
|
+
}
|
|
1112
|
+
catch (err) {
|
|
1113
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1114
|
+
console.error(`[cez] failed to save runs.json: ${message}`);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
//# sourceMappingURL=store.js.map
|