@open-mercato/cezar 0.9.1 → 0.9.2
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/README.md +19 -11
- package/dist/agent-config/catalog.d.ts +1 -1
- package/dist/agent-config/files.d.ts +1 -1
- package/dist/agent-config/service.d.ts +1 -1
- package/dist/agent-config/validate.d.ts +1 -1
- 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 +251 -0
- package/dist/automations/types.js +158 -0
- package/dist/automations/types.js.map +1 -0
- package/dist/config.d.ts +9 -111
- package/dist/contract/agent-config.d.ts +151 -0
- package/dist/contract/automations.d.ts +911 -0
- package/dist/contract/events.d.ts +45 -0
- package/dist/contract/github.d.ts +467 -0
- package/dist/contract/health.d.ts +96 -0
- package/dist/contract/index.d.ts +15 -0
- package/dist/contract/index.js +1421 -0
- package/dist/contract/projects.d.ts +182 -0
- package/dist/contract/repo.d.ts +234 -0
- package/dist/contract/runs.d.ts +1226 -0
- package/dist/contract/skills.d.ts +225 -0
- package/dist/contract/workflows.d.ts +212 -0
- package/dist/contract/workspace.d.ts +442 -0
- package/dist/core/agent-env.d.ts +1 -1
- package/dist/core/agent-runner.d.ts +12 -1
- package/dist/core/agent-runner.js +13 -0
- package/dist/core/agent-runner.js.map +1 -1
- package/dist/core/ask.d.ts +45 -124
- package/dist/core/ask.js +103 -13
- package/dist/core/ask.js.map +1 -1
- package/dist/core/claude-cli-runner.d.ts +2 -2
- package/dist/core/claude-cli-runner.js +52 -9
- package/dist/core/claude-cli-runner.js.map +1 -1
- package/dist/core/claude-ui-mapper.d.ts +1 -1
- package/dist/core/codex-app-server-runner.d.ts +1 -1
- package/dist/core/codex-app-server-runner.js +55 -3
- package/dist/core/codex-app-server-runner.js.map +1 -1
- package/dist/core/codex-app-server-transport.d.ts +6 -1
- package/dist/core/codex-app-server-transport.js +12 -3
- package/dist/core/codex-app-server-transport.js.map +1 -1
- package/dist/core/codex-model-catalog.d.ts +1 -1
- package/dist/core/codex-ui-mapper.d.ts +5 -1
- package/dist/core/codex-ui-mapper.js +74 -17
- package/dist/core/codex-ui-mapper.js.map +1 -1
- package/dist/core/model-identity.d.ts +1 -1
- package/dist/core/model-presets.d.ts +2 -2
- package/dist/core/model-presets.js +1 -1
- package/dist/core/opencode-server-runner.d.ts +2 -2
- package/dist/core/opencode-ui-mapper.d.ts +4 -1
- package/dist/core/opencode-ui-mapper.js +45 -7
- package/dist/core/opencode-ui-mapper.js.map +1 -1
- package/dist/core/provider-auth.d.ts +62 -0
- package/dist/core/provider-auth.js +290 -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 +13 -0
- package/dist/core/provider-availability.js.map +1 -0
- package/dist/core/runner-factory.d.ts +1 -1
- package/dist/core/runner-model-catalog.d.ts +1 -1
- package/dist/core/tool-display.d.ts +1 -1
- package/dist/core/ui-events.d.ts +1 -1
- package/dist/core/ui-events.js +1 -1
- package/dist/git-diff-base.d.ts +60 -0
- package/dist/git-diff-base.js +54 -0
- package/dist/git-diff-base.js.map +1 -0
- package/dist/git-worktree.d.ts +37 -6
- package/dist/git-worktree.js +39 -9
- package/dist/git-worktree.js.map +1 -1
- package/dist/handoff.d.ts +2 -2
- package/dist/handoff.js +1 -1
- package/dist/index.js +65 -8
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +16 -1
- package/dist/paths.js +24 -1
- package/dist/paths.js.map +1 -1
- package/dist/planner.d.ts +2 -2
- package/dist/planner.js +1 -1
- package/dist/planner.js.map +1 -1
- 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 +9 -23
- package/dist/release/snapshot.js +9 -22
- package/dist/release/snapshot.js.map +1 -1
- package/dist/release/stable.d.ts +15 -29
- package/dist/release/stable.js +14 -27
- package/dist/release/stable.js.map +1 -1
- package/dist/runs/auto-name.d.ts +1 -1
- package/dist/runs/retention.d.ts +1 -1
- package/dist/runs/store.d.ts +142 -336
- package/dist/runs/store.js +136 -9
- package/dist/runs/store.js.map +1 -1
- package/dist/runs/ui-event-sink.d.ts +1 -1
- 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 +6 -5
- package/dist/server/capabilities.js +11 -0
- package/dist/server/capabilities.js.map +1 -1
- package/dist/server/forge/github.d.ts +58 -151
- package/dist/server/forge/github.js +530 -6
- package/dist/server/forge/github.js.map +1 -1
- package/dist/server/forge/index.d.ts +10 -3
- package/dist/server/forge/index.js +14 -1
- package/dist/server/forge/index.js.map +1 -1
- package/dist/server/forge/types.d.ts +86 -1
- package/dist/server/git-changes.d.ts +8 -8
- package/dist/server/git-changes.js +14 -17
- package/dist/server/git-changes.js.map +1 -1
- package/dist/server/git.d.ts +2 -0
- package/dist/server/git.js +9 -0
- package/dist/server/git.js.map +1 -1
- package/dist/server/github.d.ts +3 -3
- package/dist/server/github.js +1 -1
- package/dist/server/github.js.map +1 -1
- package/dist/server/pr.d.ts +2 -2
- package/dist/server/project-context.d.ts +20 -3
- package/dist/server/project-context.js +30 -1
- package/dist/server/project-context.js.map +1 -1
- package/dist/server/provider-action-gate.d.ts +8 -0
- package/dist/server/provider-action-gate.js +54 -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 +11142 -47
- package/dist/server/server.js +1714 -806
- package/dist/server/server.js.map +1 -1
- 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 +2 -2
- package/dist/server/ws.js +2 -2
- package/dist/server/ws.js.map +1 -1
- package/dist/server-install/engine.d.ts +1 -1
- package/dist/server-install/platforms/macosx-ngrok.d.ts +1 -1
- package/dist/server-install/platforms/ubuntu-vps.d.ts +16 -1
- package/dist/server-install/platforms/ubuntu-vps.js +83 -1
- package/dist/server-install/platforms/ubuntu-vps.js.map +1 -1
- package/dist/server-install/state.d.ts +1 -1
- package/dist/server-install/steps.d.ts +2 -2
- package/dist/server-install/strategies.d.ts +1 -1
- package/dist/server-install/types.d.ts +38 -424
- package/dist/server-install/ui.d.ts +1 -1
- package/dist/skills-banner.d.ts +7 -6
- package/dist/skills-banner.js +7 -6
- package/dist/skills-banner.js.map +1 -1
- package/dist/skills-remote.d.ts +2 -2
- 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 +2 -0
- package/dist/skills.js +3 -2
- package/dist/skills.js.map +1 -1
- package/dist/todos.d.ts +2 -28
- package/dist/todos.js +1 -1
- package/dist/ui-state.d.ts +16 -2
- package/dist/ui-state.js +14 -1
- package/dist/ui-state.js.map +1 -1
- package/dist/workflows/load.d.ts +1 -1
- package/dist/workflows/run.d.ts +53 -12
- package/dist/workflows/run.js +436 -64
- package/dist/workflows/run.js.map +1 -1
- package/dist/workflows/types.d.ts +59 -224
- package/dist/workflows/types.js +14 -0
- package/dist/workflows/types.js.map +1 -1
- package/dist/workspace/config.d.ts +68 -206
- package/dist/workspace/config.js +145 -14
- package/dist/workspace/config.js.map +1 -1
- package/dist/workspace/migrations.d.ts +1 -1
- package/dist/workspace/migrations.js +19 -4
- package/dist/workspace/migrations.js.map +1 -1
- package/dist/workspace/projects.d.ts +8 -2
- package/dist/workspace/projects.js +9 -3
- package/dist/workspace/projects.js.map +1 -1
- package/dist/workspace/semaphore.d.ts +6 -0
- package/dist/workspace/semaphore.js +14 -1
- package/dist/workspace/semaphore.js.map +1 -1
- package/dist/workspace/ui-state.d.ts +13 -3
- package/dist/workspace/ui-state.js +15 -5
- package/dist/workspace/ui-state.js.map +1 -1
- package/package.json +17 -39
- package/scripts/inline-contract.mjs +112 -0
- package/scripts/mock-claude.mjs +39 -1
- package/scripts/sync-readme.mjs +20 -0
- package/web/dist/assets/alert-dialog-ghffK6g9.js +1 -0
- package/web/dist/assets/arrow-down-Cz8qb71e.js +1 -0
- package/web/dist/assets/arrow-left-wcYKQXI_.js +1 -0
- package/web/dist/assets/bundle-mjs-BT31bpU6.js +1 -0
- package/web/dist/assets/centered-state-ZtnFHA-n.js +43 -0
- package/web/dist/assets/chevron-right-G4caF2QV.js +1 -0
- package/web/dist/assets/{chunk-BO2N2NFS-DtrdTCWa.js → chunk-BO2N2NFS-DE6qKn3r.js} +8 -8
- package/web/dist/assets/circle-check--KvBxXWp.js +1 -0
- package/web/dist/assets/circle-x-C3EFZ3xf.js +1 -0
- package/web/dist/assets/collapsible-B0JVzd0B.js +1 -0
- package/web/dist/assets/commit-list-wm6DPHNs.js +1 -0
- package/web/dist/assets/compare-variants-DbdP4Y5T.js +1 -0
- package/web/dist/assets/{core-DePtBHcl.js → core-BCsw8oQw.js} +1 -1
- package/web/dist/assets/diff-Bizc96qe.js +3 -0
- package/web/dist/assets/diff-stat-Dyfg3ltF.js +1 -0
- package/web/dist/assets/{diff-view-DCeSegKk.js → diff-view-CG8ek6tC.js} +4 -4
- package/web/dist/assets/dropdown-menu-C1Dlf35P.js +1 -0
- package/web/dist/assets/editable-title-5rC_QRBZ.js +1 -0
- package/web/dist/assets/{ellipsis-vertical-CNnSLn5a.js → ellipsis-vertical-DpGTRqC2.js} +1 -1
- package/web/dist/assets/{file-D4GBh1ao.js → file-DlUGpUlb.js} +1 -1
- package/web/dist/assets/folder-BAY6Vddy.js +1 -0
- package/web/dist/assets/{git-pull-request-DnR8Xh4_.js → git-pull-request-agOG0iGM.js} +1 -1
- package/web/dist/assets/git-toolbar-DGiNMmtY.js +1 -0
- package/web/dist/assets/github-Vp7lxYGQ.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/image-preview-BWGk5fPX.js +1 -0
- package/web/dist/assets/index-DSxyJpuK.css +2 -0
- package/web/dist/assets/index-DiawI6RX.js +6 -0
- package/web/dist/assets/lib-BQXq3kEf.js +1 -0
- package/web/dist/assets/lib-BxQXEXDF.js +1 -0
- package/web/dist/assets/markdown-BGBOGCA_.js +2 -0
- package/web/dist/assets/mermaid-GHXKKRXX-C4jxqUUb.js +1 -0
- package/web/dist/assets/new-task-form-BpAw_Dks.js +1 -0
- package/web/dist/assets/pill-Dvs-P0xy.js +1 -0
- package/web/dist/assets/project-router-Cx8NPoqn.js +1 -0
- package/web/dist/assets/prompt-templates-CSBYBR1I.js +15 -0
- package/web/dist/assets/react-runtime-CCIEwYL0.js +9 -0
- package/web/dist/assets/{refresh-cw-BT8E96Tm.js → refresh-cw-BoGdAykg.js} +1 -1
- package/web/dist/assets/repo-git-PmX-BNMo.js +1 -0
- package/web/dist/assets/rolldown-runtime-QTnfLwEv.js +1 -0
- package/web/dist/assets/run-diff-EcOkf3ti.js +3 -0
- package/web/dist/assets/run-header-BGiv6_qX.js +1 -0
- package/web/dist/assets/{search-x-BDjaMrzG.js → search-x-X3EpVhgs.js} +1 -1
- package/web/dist/assets/skill-empty-hint-CZLALx6l.js +1 -0
- package/web/dist/assets/skills-BiWfHmCR.js +1 -0
- package/web/dist/assets/skills-CC91ouGH.js +1 -0
- package/web/dist/assets/sparkles-Dt4q7pbG.js +1 -0
- package/web/dist/assets/{square-terminal-Ckjjn0vb.js → square-terminal-hwBL8XsC.js} +1 -1
- package/web/dist/assets/tab-link-DIlR5T07.js +1 -0
- package/web/dist/assets/task-changes-XkHDxzQA.js +1 -0
- package/web/dist/assets/task-commits-BTTA5k5T.js +1 -0
- package/web/dist/assets/task-files-Da8_4C9g.js +2 -0
- package/web/dist/assets/task-thread-DoNXUsHr.js +9 -0
- package/web/dist/assets/textarea-CUdokhkv.js +1 -0
- package/web/dist/assets/thread-loading-B8q8ukXJ.js +1 -0
- package/web/dist/assets/{trash-2-DAyZ0VSi.js → trash-2-DhVIdWv8.js} +1 -1
- package/web/dist/assets/{triangle-alert-Uy8rokUh.js → triangle-alert-XYT0DGCI.js} +1 -1
- package/web/dist/assets/{upload-CDdyPPo_.js → upload-BKjN59-1.js} +1 -1
- package/web/dist/assets/use-desktop-Bf5LS_P4.js +1 -0
- package/web/dist/assets/use-submit-shortcut-CzVyoAMd.js +1 -0
- package/web/dist/assets/utils-DsLuRXwV.js +64 -0
- package/web/dist/assets/workflows-o2QU_pvU.js +11 -0
- package/web/dist/assets/zoomable-image-7WyUiaVD.js +1 -0
- package/web/dist/index.html +32 -13
- package/scripts/dev.mjs +0 -74
- package/scripts/release-snapshot.mjs +0 -130
- package/scripts/release.mjs +0 -119
- package/web/dist/assets/arrow-left-CnSdp92h.js +0 -1
- package/web/dist/assets/bundle-mjs-BegPhL5c.js +0 -1
- package/web/dist/assets/centered-state-DOsTcJZF.js +0 -43
- package/web/dist/assets/commit-list-BrxBEzRK.js +0 -1
- package/web/dist/assets/compare-variants-Bv2c9ORF.js +0 -1
- package/web/dist/assets/dist-B6XQOhgM.js +0 -1
- package/web/dist/assets/git-toolbar-pIC1ubPM.js +0 -1
- package/web/dist/assets/github-DTTSG0ht.js +0 -1
- package/web/dist/assets/highlighted-body-OFNGDK62-BuFJwunK.js +0 -1
- package/web/dist/assets/image-preview-DEhW0TFz.js +0 -1
- package/web/dist/assets/index-Cijmlz7q.js +0 -30
- package/web/dist/assets/index-DzroxqBG.css +0 -2
- package/web/dist/assets/lib-DPEZDBUN.js +0 -1
- package/web/dist/assets/lib-uHpeUPzK.js +0 -1
- package/web/dist/assets/mermaid-GHXKKRXX-CN2zQue0.js +0 -1
- package/web/dist/assets/open-mercato-toBr6SOa.svg +0 -11
- package/web/dist/assets/project-router-BMx4e8m-.js +0 -1
- package/web/dist/assets/repo-git-BusYfa3t.js +0 -1
- package/web/dist/assets/run-diff-wOJ3odA-.js +0 -3
- package/web/dist/assets/run-header-Cg5incil.js +0 -1
- package/web/dist/assets/skill-empty-hint-DTUSp6Vl.js +0 -1
- package/web/dist/assets/skills-PpP2owbM.js +0 -1
- package/web/dist/assets/tab-link-F3uQrdm2.js +0 -1
- package/web/dist/assets/task-changes-iD9kS79I.js +0 -1
- package/web/dist/assets/task-commits-COkLaUri.js +0 -1
- package/web/dist/assets/task-files-Ckttz7XH.js +0 -2
- package/web/dist/assets/task-thread-CQD91VPt.js +0 -9
- package/web/dist/assets/textarea-Bj_LCreg.js +0 -1
- package/web/dist/assets/use-desktop-DUh0STzq.js +0 -3
- package/web/dist/assets/utils-B8s5qIcK.js +0 -1
- package/web/dist/assets/workflows-BFk8C0co.js +0 -11
- package/web/dist/assets/zoomable-image-PEDOqyLQ.js +0 -1
- /package/web/{open-mercato.svg → dist/open-mercato.svg} +0 -0
package/dist/workflows/run.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { parseAskMarkerResult, stripAskMarker, } from '../core/ask.js';
|
|
6
6
|
import { onUsage, registerRunProcess, unregisterRunProcess } from '../core/process-usage.js';
|
|
7
7
|
import { createRunner } from '../core/runner-factory.js';
|
|
8
8
|
import { modelConflictsWithRunner } from '../core/model-presets.js';
|
|
@@ -14,7 +14,7 @@ import { materializeSkillDir } from '../skills-remote.js';
|
|
|
14
14
|
import { seedAgentConfigLocalLayer } from '../agent-config/seed.js';
|
|
15
15
|
import { loadConfig, resolveWorktreeRetention } from '../config.js';
|
|
16
16
|
import { autosaveCommit, createWorktree, resolveBaseRef, worktreeDiff, worktreeShortstat } from '../git-worktree.js';
|
|
17
|
-
import { getRepoInfo } from '../server/git.js';
|
|
17
|
+
import { getHeadCommit, getRepoInfo } from '../server/git.js';
|
|
18
18
|
import { loadWorkflows } from './load.js';
|
|
19
19
|
import { reclaimWorktrees, rematerializeReclaimedWorktree } from '../runs/retention.js';
|
|
20
20
|
import { extractTaskRefs, refineTaskRefs, titleRefNumber } from '../runs/task-refs.js';
|
|
@@ -77,6 +77,18 @@ function emitAskRequested(sink, ask) {
|
|
|
77
77
|
sink.handle({ type: 'ask.requested', requestId, questions: ask.questions });
|
|
78
78
|
return requestId;
|
|
79
79
|
}
|
|
80
|
+
/** A persisted, non-fatal explanation for protocol-shaped text that could not
|
|
81
|
+
* become an ask card. Never include the raw payload in this diagnostic. */
|
|
82
|
+
function askMarkerRejection(result) {
|
|
83
|
+
if (result.kind === 'invalid-json') {
|
|
84
|
+
return 'structured question ignored — CEZ:ASK payload is not valid JSON';
|
|
85
|
+
}
|
|
86
|
+
if (result.kind !== 'invalid-structure')
|
|
87
|
+
return undefined;
|
|
88
|
+
const issue = result.issues[0];
|
|
89
|
+
const location = issue?.path.length ? ` at ${issue.path.join('.')}` : '';
|
|
90
|
+
return `structured question ignored — CEZ:ASK payload failed validation${location}${issue ? `: ${issue.message}` : ''}`;
|
|
91
|
+
}
|
|
80
92
|
/** Periodic "cezar autosave" commit in the task worktree (spec 006). */
|
|
81
93
|
export const AUTOSAVE_INTERVAL_MS = 90_000;
|
|
82
94
|
/** The periodic autosave timer is opt-in (#471): off, a task branch carries only the
|
|
@@ -89,6 +101,7 @@ export function periodicAutosaveEnabled(env = process.env) {
|
|
|
89
101
|
/** Safety cap on autonomous auto-continues per run — stops a stuck agent from nudging forever. */
|
|
90
102
|
const MAX_AUTO_CONTINUES = 40;
|
|
91
103
|
const AUTONOMOUS_NUDGE = 'Continue working autonomously until the task is fully complete. Do not ask me for confirmation or clarification — make reasonable assumptions and proceed. When everything is done, end the session with your done signal.';
|
|
104
|
+
const MONITORING_WAKE_NUDGE = 'Re-check the downstream work you were monitoring. Continue toward the task goal; emit CEZ:MONITORING again only if it is still pending.';
|
|
92
105
|
/**
|
|
93
106
|
* The effective "extra" system prompt for a run (spec §protocol v2, R2 2.3):
|
|
94
107
|
* the per-run override (`POST /api/runs` `systemPrompt`) REPLACES the
|
|
@@ -171,6 +184,7 @@ const VARIANT_HINTS = {
|
|
|
171
184
|
B: 'Approach hint: prefer the minimal, surgical change.',
|
|
172
185
|
C: 'Approach hint: prefer a thorough, structural approach.',
|
|
173
186
|
};
|
|
187
|
+
const RESTART_CONTINUATION_PROMPT = 'The cezar process restarted while you were working on this task. Read the handoff file (CEZ_HANDOFF_FILE) to recover context, then continue the task from where you left off.';
|
|
174
188
|
/**
|
|
175
189
|
* The mini workflow engine: executes a `WorkflowDef` against a repo, one step
|
|
176
190
|
* at a time, persisting every event to the RunStore (which the SSE endpoints
|
|
@@ -200,7 +214,13 @@ export class RunManager {
|
|
|
200
214
|
// timeout already bounds how long a session can sit open. Invariant:
|
|
201
215
|
// `waiting ⊆ active` — always cleared together via dropActive().
|
|
202
216
|
waiting = new Set();
|
|
217
|
+
/** Durable monitoring subset. Only the configured number receives the waiting-slot exemption. */
|
|
218
|
+
monitoring = new Set();
|
|
203
219
|
pendingJobs = new Map();
|
|
220
|
+
/** Interrupted agent turns recovered after a process restart. Unlike an
|
|
221
|
+
* explicit user Continue, these are bulk scheduler work and must re-enter
|
|
222
|
+
* through `pump()` so both workspace and per-project caps are honored. */
|
|
223
|
+
pendingContinuations = new Map();
|
|
204
224
|
/** Per-run image counter behind `pasted-<n>` / `screenshot-<n>` (#472). Lives on
|
|
205
225
|
* the manager rather than the `ActiveRun` so a *queued* run — which has no
|
|
206
226
|
* `ActiveRun` at all — can persist attachments. Seeded lazily from disk. */
|
|
@@ -260,8 +280,9 @@ export class RunManager {
|
|
|
260
280
|
dispose() {
|
|
261
281
|
this.offUsage();
|
|
262
282
|
this.offSemaphore();
|
|
263
|
-
for (const state of this.active
|
|
283
|
+
for (const [runId, state] of this.active) {
|
|
264
284
|
this.clearIdleTimer(state);
|
|
285
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
265
286
|
this.clearAutosaveTimer(state);
|
|
266
287
|
state.releaseRepoRoot?.();
|
|
267
288
|
state.releaseRepoRoot = undefined;
|
|
@@ -271,6 +292,7 @@ export class RunManager {
|
|
|
271
292
|
this.starting.clear();
|
|
272
293
|
this.queue.length = 0;
|
|
273
294
|
this.pendingJobs.clear();
|
|
295
|
+
this.pendingContinuations.clear();
|
|
274
296
|
this.memoryPausing.clear();
|
|
275
297
|
this.lastNamerKey.clear();
|
|
276
298
|
}
|
|
@@ -354,6 +376,9 @@ export class RunManager {
|
|
|
354
376
|
// auto-nudge reads `input.autonomous` (`execute`), but the record is the
|
|
355
377
|
// only source those after-the-fact consumers have.
|
|
356
378
|
autonomous: input.autonomous === true,
|
|
379
|
+
// Persist the explicit opt-out so queued-run restart recovery and the
|
|
380
|
+
// session Git routes can distinguish it from a removed isolated worktree.
|
|
381
|
+
worktree: !group && input.worktree === false ? false : undefined,
|
|
357
382
|
groupId: group?.groupId,
|
|
358
383
|
variant: group?.variant,
|
|
359
384
|
steps: workflow.steps.map((s) => ({ id: s.id, name: s.name ?? s.id, kind: stepKind(s) })),
|
|
@@ -405,7 +430,7 @@ export class RunManager {
|
|
|
405
430
|
return VARIANT_LETTERS.slice(0, Math.min(Math.max(count, 1), VARIANT_LETTERS.length)).map((variant) => {
|
|
406
431
|
const hint = VARIANT_HINTS[variant];
|
|
407
432
|
const task = hint ? `${input.task}\n\n${hint}` : input.task;
|
|
408
|
-
return this.startRun(workflow, { ...input, task }, { groupId, variant });
|
|
433
|
+
return this.startRun(workflow, { ...input, task, worktree: undefined }, { groupId, variant });
|
|
409
434
|
});
|
|
410
435
|
}
|
|
411
436
|
/**
|
|
@@ -418,7 +443,9 @@ export class RunManager {
|
|
|
418
443
|
* `maxParallel`, including when other projects saturate the cap.
|
|
419
444
|
*/
|
|
420
445
|
busySlots() {
|
|
421
|
-
|
|
446
|
+
const ordinaryWaiting = this.waiting.size - this.monitoring.size;
|
|
447
|
+
const exemptMonitoring = Math.min(this.monitoring.size, this.semaphore.maxMonitoringSessions());
|
|
448
|
+
return this.active.size + this.starting.size - ordinaryWaiting - exemptMonitoring;
|
|
422
449
|
}
|
|
423
450
|
/** Epoch ms of this manager's oldest queued run (the semaphore's fairness
|
|
424
451
|
* key when a freed slot is broadcast), or null when nothing is queued.
|
|
@@ -454,6 +481,7 @@ export class RunManager {
|
|
|
454
481
|
* root (spec 006 degradation rule), which is always the tighter bound.
|
|
455
482
|
*/
|
|
456
483
|
async pump() {
|
|
484
|
+
this.reconcileMonitoringWakeTimers();
|
|
457
485
|
// A pump requested while one is in flight can't just be dropped: the
|
|
458
486
|
// in-flight pass may already have read capacity (it awaits `getRepoInfo`
|
|
459
487
|
// before the first check), so a slot freed in that window would be lost
|
|
@@ -483,10 +511,28 @@ export class RunManager {
|
|
|
483
511
|
if (!runId)
|
|
484
512
|
break;
|
|
485
513
|
const job = this.pendingJobs.get(runId);
|
|
514
|
+
const continuation = this.pendingContinuations.get(runId);
|
|
486
515
|
this.pendingJobs.delete(runId);
|
|
487
|
-
|
|
516
|
+
this.pendingContinuations.delete(runId);
|
|
517
|
+
if (!job && !continuation)
|
|
488
518
|
continue;
|
|
489
519
|
this.starting.add(runId);
|
|
520
|
+
if (continuation) {
|
|
521
|
+
const hydrated = this.hydrateQueuedContinuation(runId, continuation);
|
|
522
|
+
void this.runContinuation(runId, hydrated.stepId, hydrated.sessionId, hydrated.backend, hydrated.prompt, hydrated.images, hydrated.persistedImages, hydrated.persistedAttachments).catch((err) => {
|
|
523
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
524
|
+
this.store.updateRun(runId, {
|
|
525
|
+
status: 'failed',
|
|
526
|
+
error: `continue crashed: ${message}`,
|
|
527
|
+
finishedAt: new Date().toISOString(),
|
|
528
|
+
});
|
|
529
|
+
this.starting.delete(runId);
|
|
530
|
+
this.dropActive(runId);
|
|
531
|
+
});
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (!job)
|
|
535
|
+
continue;
|
|
490
536
|
// Rebuild the prompt from the store at the last instant (#472), so an edit
|
|
491
537
|
// or a stacked message that landed while the run waited is honored. Entered
|
|
492
538
|
// in the same synchronous tick as the `pendingJobs.delete` above, so no
|
|
@@ -532,6 +578,35 @@ export class RunManager {
|
|
|
532
578
|
.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
533
579
|
for (const run of live) {
|
|
534
580
|
if (run.status === 'queued') {
|
|
581
|
+
// A second restart can find a continuation that the first restart put
|
|
582
|
+
// behind a capacity gate. Its executable details are process-local,
|
|
583
|
+
// but the queued continue step and preceding provider session are
|
|
584
|
+
// durable, so reconstruct that job before considering workflow revival.
|
|
585
|
+
const queuedContinuation = [...run.steps]
|
|
586
|
+
.reverse()
|
|
587
|
+
.find((step) => step.status === 'pending' && step.id.startsWith('continue-'));
|
|
588
|
+
const sessionStep = queuedContinuation
|
|
589
|
+
? [...run.steps]
|
|
590
|
+
.reverse()
|
|
591
|
+
.find((step) => step.id !== queuedContinuation.id && step.sessionId)
|
|
592
|
+
: undefined;
|
|
593
|
+
if (queuedContinuation && sessionStep?.sessionId) {
|
|
594
|
+
const backend = run.runner ?? 'claude';
|
|
595
|
+
const sessionBackend = sessionStep.backend ?? backend;
|
|
596
|
+
this.pendingContinuations.set(run.id, {
|
|
597
|
+
stepId: queuedContinuation.id,
|
|
598
|
+
sessionId: sessionBackend === backend ? sessionStep.sessionId : undefined,
|
|
599
|
+
backend,
|
|
600
|
+
prompt: RESTART_CONTINUATION_PROMPT,
|
|
601
|
+
images: [],
|
|
602
|
+
});
|
|
603
|
+
this.queue.push(run.id);
|
|
604
|
+
this.store.appendEvent(run.id, {
|
|
605
|
+
type: 'lifecycle',
|
|
606
|
+
message: 'cezar restarted — interrupted continuation re-queued',
|
|
607
|
+
});
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
535
610
|
const workflow = await this.reviveWorkflow(run);
|
|
536
611
|
if (workflow) {
|
|
537
612
|
// Re-apply the inbox ceiling (#471). `execute()` gates again at spawn time, so the
|
|
@@ -559,6 +634,9 @@ export class RunManager {
|
|
|
559
634
|
// recovered queued autonomous run would run non-autonomously (no
|
|
560
635
|
// auto-nudge) and later wrongly park at `review`.
|
|
561
636
|
autonomous: run.autonomous,
|
|
637
|
+
// Preserve an explicit worktree opt-out across a queued restart.
|
|
638
|
+
// Missing on older records means the default isolated mode.
|
|
639
|
+
worktree: run.worktree,
|
|
562
640
|
}),
|
|
563
641
|
});
|
|
564
642
|
this.queue.push(run.id);
|
|
@@ -605,8 +683,8 @@ export class RunManager {
|
|
|
605
683
|
currentStepId: undefined,
|
|
606
684
|
});
|
|
607
685
|
const resumed = this.continueRun(run.id, {
|
|
608
|
-
text:
|
|
609
|
-
});
|
|
686
|
+
text: RESTART_CONTINUATION_PROMPT,
|
|
687
|
+
}, true);
|
|
610
688
|
this.store.appendEvent(run.id, {
|
|
611
689
|
type: 'lifecycle',
|
|
612
690
|
message: resumed.ok
|
|
@@ -618,10 +696,12 @@ export class RunManager {
|
|
|
618
696
|
}
|
|
619
697
|
/** The persisted definition when it looks sane, else the catalog by name. */
|
|
620
698
|
async reviveWorkflow(run) {
|
|
699
|
+
// "Looks sane" is the STORE's job now: it parses `workflowDef` against the definition schema
|
|
700
|
+
// and `.catch`es a def that no longer fits to `undefined`, so anything present here already
|
|
701
|
+
// has the `steps` array the old inline `Array.isArray` check was asking for.
|
|
621
702
|
const def = run.workflowDef;
|
|
622
|
-
if (def
|
|
703
|
+
if (def)
|
|
623
704
|
return def;
|
|
624
|
-
}
|
|
625
705
|
const { workflows } = await loadWorkflows(this.repoRoot);
|
|
626
706
|
return workflows.find((w) => w.name === run.workflow) ?? null;
|
|
627
707
|
}
|
|
@@ -632,6 +712,9 @@ export class RunManager {
|
|
|
632
712
|
if (state)
|
|
633
713
|
state.releaseRepoRoot = undefined;
|
|
634
714
|
this.waiting.delete(runId);
|
|
715
|
+
this.monitoring.delete(runId);
|
|
716
|
+
if (state)
|
|
717
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
635
718
|
this.active.delete(runId);
|
|
636
719
|
this.memoryPausing.delete(runId);
|
|
637
720
|
this.lastNamerKey.delete(runId);
|
|
@@ -723,6 +806,7 @@ export class RunManager {
|
|
|
723
806
|
if (queuedAt >= 0) {
|
|
724
807
|
this.queue.splice(queuedAt, 1);
|
|
725
808
|
this.pendingJobs.delete(runId);
|
|
809
|
+
this.pendingContinuations.delete(runId);
|
|
726
810
|
this.store.updateRun(runId, { status: 'cancelled', finishedAt: new Date().toISOString() });
|
|
727
811
|
this.store.appendEvent(runId, { type: 'lifecycle', message: 'cancelled while queued' });
|
|
728
812
|
return true;
|
|
@@ -763,35 +847,13 @@ export class RunManager {
|
|
|
763
847
|
.map((part) => part.trim())
|
|
764
848
|
.filter((part) => part.length > 0)
|
|
765
849
|
.join('\n\n');
|
|
766
|
-
const readImages = (urls, kind) => {
|
|
767
|
-
const images = [];
|
|
768
|
-
for (const url of urls) {
|
|
769
|
-
const name = url.split('/').pop();
|
|
770
|
-
if (!name || name.includes('..') || name.includes('/') || name.includes('\\'))
|
|
771
|
-
continue;
|
|
772
|
-
try {
|
|
773
|
-
const data = readFileSync(join(this.dataDir, 'runs', `${runId}-images`, name));
|
|
774
|
-
images.push({
|
|
775
|
-
type: 'image',
|
|
776
|
-
source: { type: 'base64', media_type: mediaTypeFor(name), data: data.toString('base64') },
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
catch {
|
|
780
|
-
// Degrade, never fail the boot (AGENTS.md): the user deleted `.ai/cezar/`
|
|
781
|
-
// or the file is unreadable — start with the text and say which image went.
|
|
782
|
-
this.store.appendEvent(runId, {
|
|
783
|
-
type: 'note',
|
|
784
|
-
message: `${kind} attachment ${name} could not be read — starting without it`,
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
return images;
|
|
789
|
-
};
|
|
790
850
|
// Keep the original in-memory blocks for a live process (including the
|
|
791
851
|
// best-effort case where persistence failed). Recovery has no such copy,
|
|
792
852
|
// so rebuild it from the durable task-image URLs.
|
|
793
|
-
const images = input.images?.length
|
|
794
|
-
|
|
853
|
+
const images = input.images?.length
|
|
854
|
+
? input.images
|
|
855
|
+
: this.readPersistedImages(runId, run.taskImages ?? [], 'task').blocks;
|
|
856
|
+
const stackedImages = this.readPersistedImages(runId, stack.flatMap((m) => m.images ?? []), 'queued').blocks;
|
|
795
857
|
return {
|
|
796
858
|
...input,
|
|
797
859
|
task,
|
|
@@ -799,15 +861,67 @@ export class RunManager {
|
|
|
799
861
|
...(stackedImages.length ? { stackedImages } : { stackedImages: undefined }),
|
|
800
862
|
};
|
|
801
863
|
}
|
|
864
|
+
/** Apply edits and messages made while a restart continuation waits for
|
|
865
|
+
* capacity. The durable record remains the source of truth, just as it is for
|
|
866
|
+
* an ordinary queued workflow (#472), so a second restart reconstructs and
|
|
867
|
+
* hydrates the same amendments instead of dropping them. */
|
|
868
|
+
hydrateQueuedContinuation(runId, continuation) {
|
|
869
|
+
const run = this.store.getRun(runId);
|
|
870
|
+
if (!run) {
|
|
871
|
+
return { ...continuation, persistedImages: [], persistedAttachments: [] };
|
|
872
|
+
}
|
|
873
|
+
const stack = run.queuedMessages ?? [];
|
|
874
|
+
const amendedTask = [run.task, ...stack.map((message) => message.text)]
|
|
875
|
+
.map((part) => part.trim())
|
|
876
|
+
.filter((part) => part.length > 0)
|
|
877
|
+
.join('\n\n');
|
|
878
|
+
const prompt = amendedTask
|
|
879
|
+
? `${continuation.prompt}\n\nCurrent task and queued updates:\n\n${amendedTask}`
|
|
880
|
+
: continuation.prompt;
|
|
881
|
+
const persisted = this.readPersistedImages(runId, stack.flatMap((message) => message.images ?? []), 'queued');
|
|
882
|
+
return {
|
|
883
|
+
...continuation,
|
|
884
|
+
prompt,
|
|
885
|
+
persistedImages: persisted.blocks,
|
|
886
|
+
persistedAttachments: persisted.attachments,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
readPersistedImages(runId, urls, kind) {
|
|
890
|
+
const blocks = [];
|
|
891
|
+
const attachments = [];
|
|
892
|
+
for (const url of urls) {
|
|
893
|
+
const name = url.split('/').pop();
|
|
894
|
+
if (!name || name.includes('..') || name.includes('/') || name.includes('\\'))
|
|
895
|
+
continue;
|
|
896
|
+
const path = join(this.dataDir, 'runs', `${runId}-images`, name);
|
|
897
|
+
try {
|
|
898
|
+
const data = readFileSync(path);
|
|
899
|
+
blocks.push({
|
|
900
|
+
type: 'image',
|
|
901
|
+
source: { type: 'base64', media_type: mediaTypeFor(name), data: data.toString('base64') },
|
|
902
|
+
});
|
|
903
|
+
attachments.push({ name, url, path });
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
// Degrade, never fail the boot (AGENTS.md): the user deleted `.ai/cezar/`
|
|
907
|
+
// or the file is unreadable — start with the text and say which image went.
|
|
908
|
+
this.store.appendEvent(runId, {
|
|
909
|
+
type: 'note',
|
|
910
|
+
message: `${kind} attachment ${name} could not be read — starting without it`,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return { blocks, attachments };
|
|
915
|
+
}
|
|
802
916
|
/**
|
|
803
917
|
* Still waiting for a slot? Checked against the engine's own queue rather than
|
|
804
918
|
* the record's `status` (#472): the record is written by `execute()` a tick
|
|
805
919
|
* after `pump()` dequeues, so a status read can see `queued` for a run that has
|
|
806
|
-
* already started.
|
|
807
|
-
* the authoritative answer for "can this prompt still be amended".
|
|
920
|
+
* already started. The pending maps are deleted synchronously at dequeue, so
|
|
921
|
+
* they are the authoritative answer for "can this prompt still be amended".
|
|
808
922
|
*/
|
|
809
923
|
isQueued(runId) {
|
|
810
|
-
return this.pendingJobs.has(runId);
|
|
924
|
+
return this.pendingJobs.has(runId) || this.pendingContinuations.has(runId);
|
|
811
925
|
}
|
|
812
926
|
/** Split `ContentBlock[]` into the persisted shape a stacked message holds. */
|
|
813
927
|
toQueuedMessage(runId, content) {
|
|
@@ -987,6 +1101,18 @@ export class RunManager {
|
|
|
987
1101
|
* then offers "Continue" instead.
|
|
988
1102
|
*/
|
|
989
1103
|
sendMessage(runId, content) {
|
|
1104
|
+
const delivered = this.deliverMessage(runId, content, true);
|
|
1105
|
+
if (delivered) {
|
|
1106
|
+
const state = this.active.get(runId);
|
|
1107
|
+
if (state)
|
|
1108
|
+
state.monitoringWakeups = 0;
|
|
1109
|
+
this.store.updateRun(runId, { monitoringWakeCapReached: undefined });
|
|
1110
|
+
}
|
|
1111
|
+
return delivered;
|
|
1112
|
+
}
|
|
1113
|
+
/** Shared live-session delivery. Synthetic scheduler prompts reuse lifecycle
|
|
1114
|
+
* bookkeeping without masquerading as user-authored transcript messages. */
|
|
1115
|
+
deliverMessage(runId, content, userAuthored) {
|
|
990
1116
|
const state = this.active.get(runId);
|
|
991
1117
|
if (!state?.session?.open || state.cancelled)
|
|
992
1118
|
return false;
|
|
@@ -997,27 +1123,32 @@ export class RunManager {
|
|
|
997
1123
|
// Persist the attached images so the thread can render them (not just count them) — the same
|
|
998
1124
|
// on-disk store + `/images/` route the agent's own screenshots use. `pasted` prefix marks
|
|
999
1125
|
// these as user attachments (vs. agent tool screenshots) on disk (#357).
|
|
1000
|
-
const persisted = content
|
|
1126
|
+
const persisted = userAuthored ? content
|
|
1001
1127
|
.filter((b) => b.type === 'image')
|
|
1002
1128
|
.map((b) => this.persistImage(runId, b.source.media_type, b.source.data, 'pasted'))
|
|
1003
|
-
.filter((saved) => saved !== null);
|
|
1129
|
+
.filter((saved) => saved !== null) : [];
|
|
1004
1130
|
const images = persisted.map((saved) => saved.url);
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1131
|
+
if (userAuthored) {
|
|
1132
|
+
this.store.appendEvent(runId, {
|
|
1133
|
+
type: 'user-message',
|
|
1134
|
+
stepId: state.currentStepId,
|
|
1135
|
+
text,
|
|
1136
|
+
imageCount: content.filter((b) => b.type === 'image').length,
|
|
1137
|
+
images,
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1012
1140
|
// Tell the agent where the pasted files live on disk (#357): the base64 blocks below still
|
|
1013
1141
|
// ride along so the model can *view* them, but a real path is what lets it *operate* on them
|
|
1014
1142
|
// (save, `cp`, attach to a GitHub issue/PR) — and it's the only usable reference on backends
|
|
1015
1143
|
// (codex, opencode) that drop image blocks entirely before reaching the model.
|
|
1016
|
-
const
|
|
1144
|
+
const expanded = userAuthored ? expandRegistrySlashSkill(content, state.skills ?? []) : content;
|
|
1145
|
+
const deliverable = persisted.length ? [...expanded, pastedAttachmentsNote(persisted)] : expanded;
|
|
1017
1146
|
const delivered = state.session.sendMessage(deliverable);
|
|
1018
1147
|
if (delivered) {
|
|
1019
1148
|
this.clearIdleTimer(state);
|
|
1149
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
1020
1150
|
this.waiting.delete(runId); // resumed — the run counts against slots again
|
|
1151
|
+
this.monitoring.delete(runId);
|
|
1021
1152
|
// Clear any `monitoring` activity — the agent is actively working again
|
|
1022
1153
|
// (spec 2026-07-18-subagent-monitoring-status, #490).
|
|
1023
1154
|
this.store.updateRun(runId, { status: 'running', activity: undefined });
|
|
@@ -1054,7 +1185,10 @@ export class RunManager {
|
|
|
1054
1185
|
* behaves exactly like an interactive step: `waiting` after each turn,
|
|
1055
1186
|
* messages via sendMessage, closed by finish/idle/cancel.
|
|
1056
1187
|
*/
|
|
1057
|
-
continueRun(runId, opts = {}
|
|
1188
|
+
continueRun(runId, opts = {},
|
|
1189
|
+
/** Restart recovery may discover several interrupted tasks at once. Those
|
|
1190
|
+
* continuations are queued; an explicit user Continue remains immediate. */
|
|
1191
|
+
deferForCapacity = false) {
|
|
1058
1192
|
if (this.active.has(runId))
|
|
1059
1193
|
return { ok: false, error: 'run is still active' };
|
|
1060
1194
|
const run = this.store.getRun(runId);
|
|
@@ -1108,7 +1242,26 @@ export class RunManager {
|
|
|
1108
1242
|
const continuations = run.steps.filter((s) => s.id.startsWith('continue-')).length;
|
|
1109
1243
|
const stepId = `continue-${continuations + 1}`;
|
|
1110
1244
|
this.store.addStep(runId, { id: stepId, name: 'Continue', kind: 'agent' });
|
|
1111
|
-
|
|
1245
|
+
const prompt = opts.text?.trim() || 'Continue.';
|
|
1246
|
+
const images = opts.images ?? [];
|
|
1247
|
+
if (deferForCapacity) {
|
|
1248
|
+
this.pendingContinuations.set(runId, {
|
|
1249
|
+
stepId,
|
|
1250
|
+
sessionId: resume ? sessionStep.sessionId : undefined,
|
|
1251
|
+
backend: targetRunner,
|
|
1252
|
+
prompt,
|
|
1253
|
+
images,
|
|
1254
|
+
});
|
|
1255
|
+
this.queue.push(runId);
|
|
1256
|
+
this.store.updateRun(runId, {
|
|
1257
|
+
status: 'queued',
|
|
1258
|
+
error: undefined,
|
|
1259
|
+
finishedAt: undefined,
|
|
1260
|
+
currentStepId: undefined,
|
|
1261
|
+
});
|
|
1262
|
+
return { ok: true };
|
|
1263
|
+
}
|
|
1264
|
+
void this.runContinuation(runId, stepId, resume ? sessionStep.sessionId : undefined, targetRunner, prompt, images).catch((err) => {
|
|
1112
1265
|
const message = err instanceof Error ? err.message : String(err);
|
|
1113
1266
|
this.store.updateRun(runId, {
|
|
1114
1267
|
status: 'failed',
|
|
@@ -1123,7 +1276,11 @@ export class RunManager {
|
|
|
1123
1276
|
/** Screenshots pasted into the follow-up composer — delivered with the
|
|
1124
1277
|
* reopened session's opening message, exactly like a live-session
|
|
1125
1278
|
* message's attachments. */
|
|
1126
|
-
images = []
|
|
1279
|
+
images = [],
|
|
1280
|
+
/** Queued-message screenshots were persisted when they were enqueued and
|
|
1281
|
+
* reconstructed at dequeue. Keep them separate from fresh `images` so
|
|
1282
|
+
* opening a recovered continuation does not persist duplicate files. */
|
|
1283
|
+
persistedImages = [], persistedAttachments = []) {
|
|
1127
1284
|
// Continuation runs in the task's worktree when it still exists (spec
|
|
1128
1285
|
// 006) — the resumed session sees exactly what the original run left.
|
|
1129
1286
|
// Retention (#483) may have reclaimed this run's worktree directory while
|
|
@@ -1141,6 +1298,7 @@ export class RunManager {
|
|
|
1141
1298
|
: this.repoRoot;
|
|
1142
1299
|
const state = { cancelled: false, interrupt: () => undefined, cwd };
|
|
1143
1300
|
this.active.set(runId, state);
|
|
1301
|
+
this.starting.delete(runId);
|
|
1144
1302
|
if (state.cwd === this.repoRoot) {
|
|
1145
1303
|
this.store.appendEvent(runId, {
|
|
1146
1304
|
type: 'note',
|
|
@@ -1180,15 +1338,17 @@ export class RunManager {
|
|
|
1180
1338
|
// images rather than a bare count, and handed to the agent BOTH as base64 blocks (so it can
|
|
1181
1339
|
// view them) and as absolute paths appended to the prompt (so it can operate on them — and
|
|
1182
1340
|
// because codex/opencode drop image blocks before they reach the model).
|
|
1183
|
-
const
|
|
1341
|
+
const freshAttachments = images
|
|
1184
1342
|
.filter((b) => b.type === 'image')
|
|
1185
1343
|
.map((b) => this.persistImage(runId, b.source.media_type, b.source.data, 'pasted'))
|
|
1186
1344
|
.filter((saved) => saved !== null);
|
|
1345
|
+
const openingImages = [...images, ...persistedImages];
|
|
1346
|
+
const attachments = [...freshAttachments, ...persistedAttachments];
|
|
1187
1347
|
this.store.appendEvent(runId, {
|
|
1188
1348
|
type: 'user-message',
|
|
1189
1349
|
stepId,
|
|
1190
1350
|
text: prompt,
|
|
1191
|
-
imageCount:
|
|
1351
|
+
imageCount: openingImages.filter((b) => b.type === 'image').length,
|
|
1192
1352
|
...(attachments.length ? { images: attachments.map((saved) => saved.url) } : {}),
|
|
1193
1353
|
});
|
|
1194
1354
|
let stepCost = 0;
|
|
@@ -1229,9 +1389,13 @@ export class RunManager {
|
|
|
1229
1389
|
const done = sessionOpen && DONE_MARKER_RE.test(turnText.trimEnd());
|
|
1230
1390
|
// `CEZ:ASK` → the user is genuinely blocked; wins over `CEZ:MONITORING`
|
|
1231
1391
|
// (a pending question is always attention), loses to `CEZ:DONE` (#473).
|
|
1232
|
-
const
|
|
1392
|
+
const askResult = sessionOpen && !done ? parseAskMarkerResult(turnText) : undefined;
|
|
1393
|
+
const ask = askResult?.kind === 'valid' ? askResult.request : null;
|
|
1394
|
+
const askRejection = askResult ? askMarkerRejection(askResult) : undefined;
|
|
1233
1395
|
const monitoring = sessionOpen && !done && !ask && MONITORING_MARKER_RE.test(turnText.trimEnd());
|
|
1234
1396
|
turnText = '';
|
|
1397
|
+
if (askRejection)
|
|
1398
|
+
this.store.appendEvent(runId, { type: 'note', message: askRejection, stepId });
|
|
1235
1399
|
if (done) {
|
|
1236
1400
|
// Goal achieved (agent contract, #347) — same as in runAgentStep.
|
|
1237
1401
|
this.store.appendEvent(runId, { type: 'lifecycle', message: 'goal achieved — session closed' });
|
|
@@ -1267,13 +1431,19 @@ export class RunManager {
|
|
|
1267
1431
|
if (monitoring) {
|
|
1268
1432
|
this.store.updateRun(runId, { status: 'running', activity: 'monitoring' });
|
|
1269
1433
|
this.store.updateStep(runId, stepId, { status: 'running' });
|
|
1434
|
+
this.monitoring.add(runId);
|
|
1435
|
+
this.clearIdleTimer(state);
|
|
1436
|
+
this.armMonitoringWakeTimer(runId, state);
|
|
1270
1437
|
}
|
|
1271
1438
|
else {
|
|
1272
1439
|
this.store.updateRun(runId, { status: 'waiting', activity: undefined });
|
|
1273
1440
|
this.store.updateStep(runId, stepId, { status: 'waiting' });
|
|
1441
|
+
this.monitoring.delete(runId);
|
|
1442
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
1274
1443
|
}
|
|
1275
1444
|
this.waiting.add(runId);
|
|
1276
|
-
|
|
1445
|
+
if (!monitoring)
|
|
1446
|
+
this.armIdleTimer(runId, state);
|
|
1277
1447
|
this.releaseSlot();
|
|
1278
1448
|
}
|
|
1279
1449
|
}
|
|
@@ -1321,13 +1491,15 @@ export class RunManager {
|
|
|
1321
1491
|
return;
|
|
1322
1492
|
}
|
|
1323
1493
|
const runner = createRunner(continueBackend);
|
|
1494
|
+
state.currentStepId = stepId;
|
|
1495
|
+
this.beginUsageInvocation(runId, state, stepId);
|
|
1324
1496
|
const session = runner.startSession({
|
|
1325
1497
|
// The Continue step is a fresh agent session on the same run — the
|
|
1326
1498
|
// run's extra system prompt (already resolved at execute time and
|
|
1327
1499
|
// echoed on the record) rides along with the handoff contract.
|
|
1328
1500
|
systemPrompt: composeSystemPrompt(record?.systemPrompt, generateFollowups ? HANDOFF_INSTRUCTIONS : HANDOFF_ONLY_INSTRUCTIONS),
|
|
1329
1501
|
userPrompt: attachments.length ? `${prompt}\n\n${pastedAttachmentsText(attachments)}` : prompt,
|
|
1330
|
-
...(
|
|
1502
|
+
...(openingImages.length ? { images: openingImages } : {}),
|
|
1331
1503
|
cwd: state.cwd,
|
|
1332
1504
|
allowedTools: DEFAULT_ALLOWED_TOOLS,
|
|
1333
1505
|
additionalDirectories: [join(this.dataDir, 'runs')],
|
|
@@ -1340,7 +1512,6 @@ export class RunManager {
|
|
|
1340
1512
|
state.session = session;
|
|
1341
1513
|
state.sessionEverOpened = true;
|
|
1342
1514
|
this.flushDeferred(runId);
|
|
1343
|
-
state.currentStepId = stepId;
|
|
1344
1515
|
state.interrupt = () => session.interrupt();
|
|
1345
1516
|
if (session.pid !== undefined)
|
|
1346
1517
|
registerRunProcess(runId, session.pid);
|
|
@@ -1429,11 +1600,20 @@ export class RunManager {
|
|
|
1429
1600
|
// established; only explicit opt-out and non-Git modes run in place.
|
|
1430
1601
|
const repo = await getRepoInfo(this.repoRoot);
|
|
1431
1602
|
if (repo && input.worktree === false) {
|
|
1432
|
-
// Composer opt-out: run in the repo working tree, no branch/worktree
|
|
1433
|
-
//
|
|
1603
|
+
// Composer opt-out: run in the repo working tree, no branch/worktree. The
|
|
1604
|
+
// repository-root lease serializes these runs so workflows cannot overlap.
|
|
1605
|
+
// Pin the starting commit: the session's Changes and Commits views use it
|
|
1606
|
+
// as their stable lower bound while reading the current working copy.
|
|
1607
|
+
const startingCommit = await getHeadCommit(repo.root);
|
|
1608
|
+
if (startingCommit)
|
|
1609
|
+
this.store.updateRun(runId, { baseBranch: startingCommit });
|
|
1434
1610
|
emit({ type: 'note', message: 'worktree off — running in the repo working tree' });
|
|
1435
1611
|
}
|
|
1436
1612
|
else if (repo) {
|
|
1613
|
+
emit({
|
|
1614
|
+
type: 'note',
|
|
1615
|
+
message: `worktree on — using an isolated task worktree (${input.worktree === true ? 'explicit request' : 'default'})`,
|
|
1616
|
+
});
|
|
1437
1617
|
// Fork from the configured base branch (config.json `baseBranch`, e.g.
|
|
1438
1618
|
// `develop`) — also the target of the eventual draft PR. Unresolvable
|
|
1439
1619
|
// (typo, not fetched) → note + the currently checked-out branch.
|
|
@@ -1508,6 +1688,7 @@ export class RunManager {
|
|
|
1508
1688
|
if (seeded)
|
|
1509
1689
|
seedHandoffFile(this.dataDir, seeded);
|
|
1510
1690
|
const skills = await discoverSkills(this.repoRoot);
|
|
1691
|
+
state.skills = skills;
|
|
1511
1692
|
const retriesUsed = new Map();
|
|
1512
1693
|
let checkFailure = null;
|
|
1513
1694
|
let runError = null;
|
|
@@ -1723,13 +1904,17 @@ export class RunManager {
|
|
|
1723
1904
|
const done = interactive && sessionOpen && DONE_MARKER_RE.test(turnText.trimEnd());
|
|
1724
1905
|
// `CEZ:ASK` → the user is blocked; wins over `CEZ:MONITORING`, loses to
|
|
1725
1906
|
// `CEZ:DONE` (#473).
|
|
1726
|
-
const
|
|
1907
|
+
const askResult = interactive && sessionOpen && !done ? parseAskMarkerResult(turnText) : undefined;
|
|
1908
|
+
const ask = askResult?.kind === 'valid' ? askResult.request : null;
|
|
1909
|
+
const askRejection = askResult ? askMarkerRejection(askResult) : undefined;
|
|
1727
1910
|
const monitoring = interactive &&
|
|
1728
1911
|
sessionOpen &&
|
|
1729
1912
|
!done &&
|
|
1730
1913
|
!ask &&
|
|
1731
1914
|
MONITORING_MARKER_RE.test(turnText.trimEnd());
|
|
1732
1915
|
turnText = '';
|
|
1916
|
+
if (askRejection)
|
|
1917
|
+
emit({ type: 'note', stepId: step.id, message: askRejection });
|
|
1733
1918
|
if (done) {
|
|
1734
1919
|
// Goal achieved (agent contract, #347): close the session instead
|
|
1735
1920
|
// of parking at `waiting` — the run completes and frees its slot.
|
|
@@ -1752,13 +1937,19 @@ export class RunManager {
|
|
|
1752
1937
|
if (monitoring) {
|
|
1753
1938
|
this.store.updateRun(runId, { status: 'running', activity: 'monitoring' });
|
|
1754
1939
|
this.store.updateStep(runId, step.id, { status: 'running' });
|
|
1940
|
+
this.monitoring.add(runId);
|
|
1941
|
+
this.clearIdleTimer(state);
|
|
1942
|
+
this.armMonitoringWakeTimer(runId, state);
|
|
1755
1943
|
}
|
|
1756
1944
|
else {
|
|
1757
1945
|
this.store.updateRun(runId, { status: 'waiting', activity: undefined });
|
|
1758
1946
|
this.store.updateStep(runId, step.id, { status: 'waiting' });
|
|
1947
|
+
this.monitoring.delete(runId);
|
|
1948
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
1759
1949
|
}
|
|
1760
1950
|
this.waiting.add(runId);
|
|
1761
|
-
|
|
1951
|
+
if (!monitoring)
|
|
1952
|
+
this.armIdleTimer(runId, state);
|
|
1762
1953
|
this.releaseSlot(); // the freed slot can start a queued run right away — in any project
|
|
1763
1954
|
}
|
|
1764
1955
|
// Cez's own heartbeat — the handoff stays current even when the
|
|
@@ -1790,6 +1981,8 @@ export class RunManager {
|
|
|
1790
1981
|
}
|
|
1791
1982
|
const runner = createRunner(stepBackend);
|
|
1792
1983
|
let session;
|
|
1984
|
+
state.currentStepId = step.id;
|
|
1985
|
+
this.beginUsageInvocation(runId, state, step.id);
|
|
1793
1986
|
try {
|
|
1794
1987
|
session = runner.startSession({
|
|
1795
1988
|
// Skill body, then the run's extra prompt (POST override or config
|
|
@@ -1815,6 +2008,7 @@ export class RunManager {
|
|
|
1815
2008
|
});
|
|
1816
2009
|
}
|
|
1817
2010
|
catch (err) {
|
|
2011
|
+
state.currentStepId = undefined;
|
|
1818
2012
|
return err instanceof Error ? err.message : String(err);
|
|
1819
2013
|
}
|
|
1820
2014
|
state.session = session;
|
|
@@ -1840,6 +2034,9 @@ export class RunManager {
|
|
|
1840
2034
|
finally {
|
|
1841
2035
|
this.recordUsagePeaks(runId);
|
|
1842
2036
|
this.clearIdleTimer(state);
|
|
2037
|
+
this.monitoring.delete(runId);
|
|
2038
|
+
this.waiting.delete(runId);
|
|
2039
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
1843
2040
|
state.session = undefined;
|
|
1844
2041
|
state.currentStepId = undefined;
|
|
1845
2042
|
state.interrupt = () => undefined;
|
|
@@ -1863,16 +2060,90 @@ export class RunManager {
|
|
|
1863
2060
|
/** Native backend asks arrive before turn-end. Persist and park immediately
|
|
1864
2061
|
* so the cockpit shows attention and the run releases its workspace slot. */
|
|
1865
2062
|
handleRunnerUiEvent(runId, state, sink, event) {
|
|
2063
|
+
this.recordUsageUiEvent(runId, state, event);
|
|
1866
2064
|
sink.handle(event);
|
|
1867
2065
|
if (event.type !== 'ask.requested' || state.cancelled)
|
|
1868
2066
|
return;
|
|
1869
2067
|
this.clearIdleTimer(state);
|
|
2068
|
+
this.monitoring.delete(runId);
|
|
2069
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
1870
2070
|
this.waiting.add(runId);
|
|
1871
2071
|
this.store.updateRun(runId, { status: 'waiting', activity: undefined });
|
|
1872
2072
|
if (state.currentStepId)
|
|
1873
2073
|
this.store.updateStep(runId, state.currentStepId, { status: 'waiting' });
|
|
1874
2074
|
this.releaseSlot();
|
|
1875
2075
|
}
|
|
2076
|
+
/** Persist the invocation checkpoint before launching a runner. A throw or
|
|
2077
|
+
* process exit before `turn.started` therefore leaves a durable mismatch. */
|
|
2078
|
+
beginUsageInvocation(runId, state, stepId) {
|
|
2079
|
+
const step = this.store.getRun(runId)?.steps.find((candidate) => candidate.id === stepId);
|
|
2080
|
+
if (!step)
|
|
2081
|
+
return;
|
|
2082
|
+
const epoch = (step.usageInvocationEpoch ?? 0) + 1;
|
|
2083
|
+
this.persistUsageCheckpoint(runId, stepId, {
|
|
2084
|
+
usageInvocationEpoch: epoch,
|
|
2085
|
+
usageInvocationsStarted: (step.usageInvocationsStarted ?? 0) + 1,
|
|
2086
|
+
});
|
|
2087
|
+
state.usageInvocation = {
|
|
2088
|
+
stepId,
|
|
2089
|
+
epoch,
|
|
2090
|
+
observed: false,
|
|
2091
|
+
startedTurns: new Set(),
|
|
2092
|
+
recordedTurns: new Set(),
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
/** Fold backend-neutral completed-turn usage into the current step exactly
|
|
2096
|
+
* once. Invocation/turn counters are written before the event reaches the
|
|
2097
|
+
* NDJSON sink so crashes cannot preserve a falsely complete subtotal. */
|
|
2098
|
+
recordUsageUiEvent(runId, state, event) {
|
|
2099
|
+
const invocation = state.usageInvocation;
|
|
2100
|
+
if (!invocation)
|
|
2101
|
+
return;
|
|
2102
|
+
const step = this.store.getRun(runId)?.steps.find((candidate) => candidate.id === invocation.stepId);
|
|
2103
|
+
if (!step)
|
|
2104
|
+
return;
|
|
2105
|
+
if (event.type === 'turn.started') {
|
|
2106
|
+
if (invocation.startedTurns.has(event.turnId))
|
|
2107
|
+
return;
|
|
2108
|
+
invocation.startedTurns.add(event.turnId);
|
|
2109
|
+
const firstObservedTurn = !invocation.observed;
|
|
2110
|
+
invocation.observed = true;
|
|
2111
|
+
this.persistUsageCheckpoint(runId, invocation.stepId, {
|
|
2112
|
+
usageTurnsStarted: (step.usageTurnsStarted ?? 0) + 1,
|
|
2113
|
+
...(firstObservedTurn
|
|
2114
|
+
? { usageInvocationsObserved: (step.usageInvocationsObserved ?? 0) + 1 }
|
|
2115
|
+
: {}),
|
|
2116
|
+
});
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
if (event.type !== 'turn.completed')
|
|
2120
|
+
return;
|
|
2121
|
+
if (!invocation.startedTurns.has(event.turnId) || invocation.recordedTurns.has(event.turnId))
|
|
2122
|
+
return;
|
|
2123
|
+
const input = event.usage?.input;
|
|
2124
|
+
const output = event.usage?.output;
|
|
2125
|
+
if (typeof input !== 'number' ||
|
|
2126
|
+
!Number.isFinite(input) ||
|
|
2127
|
+
input < 0 ||
|
|
2128
|
+
typeof output !== 'number' ||
|
|
2129
|
+
!Number.isFinite(output) ||
|
|
2130
|
+
output < 0) {
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
invocation.recordedTurns.add(event.turnId);
|
|
2134
|
+
this.persistUsageCheckpoint(runId, invocation.stepId, {
|
|
2135
|
+
inputTokens: (step.inputTokens ?? 0) + input,
|
|
2136
|
+
outputTokens: (step.outputTokens ?? 0) + output,
|
|
2137
|
+
usageTurnsRecorded: (step.usageTurnsRecorded ?? 0) + 1,
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
/** Usage completeness is a crash boundary, unlike high-frequency token
|
|
2141
|
+
* snapshots: the checkpoint must reach `runs.json` before the runner starts
|
|
2142
|
+
* or the matching UI event is persisted and forwarded. */
|
|
2143
|
+
persistUsageCheckpoint(runId, stepId, patch) {
|
|
2144
|
+
this.store.updateStep(runId, stepId, patch);
|
|
2145
|
+
this.store.flush();
|
|
2146
|
+
}
|
|
1876
2147
|
/**
|
|
1877
2148
|
* Turn-end bookkeeping (#389), shared by `runAgentStep` and
|
|
1878
2149
|
* `runContinuation` — called (fire-and-forget) from every `turn-end` event:
|
|
@@ -1939,7 +2210,12 @@ export class RunManager {
|
|
|
1939
2210
|
// deliberately NEVER a title source; see maybeRefreshTitle below. The
|
|
1940
2211
|
// one exception is an explicit CEZ:TITLE declaration (applied above).
|
|
1941
2212
|
if (run.worktreePath && existsSync(run.worktreePath)) {
|
|
1942
|
-
|
|
2213
|
+
// `taskBranch` is what keeps this number *this task's* (#751): a review/QA run
|
|
2214
|
+
// repoints the worktree onto the branch under review, and without the branch to
|
|
2215
|
+
// compare HEAD against, the stat would claim that whole branch's diff.
|
|
2216
|
+
const stat = await worktreeShortstat(run.worktreePath, run.baseBranch ?? 'HEAD', {
|
|
2217
|
+
taskBranch: run.branch,
|
|
2218
|
+
});
|
|
1943
2219
|
if (stat)
|
|
1944
2220
|
this.store.updateRun(runId, { diffStat: stat });
|
|
1945
2221
|
else
|
|
@@ -2096,7 +2372,10 @@ export class RunManager {
|
|
|
2096
2372
|
throw err;
|
|
2097
2373
|
}
|
|
2098
2374
|
this.queuedImageSeq.set(runId, seq);
|
|
2099
|
-
|
|
2375
|
+
// Versioned, because that is the only surface served now. The cockpit still upgrades
|
|
2376
|
+
// the unversioned URLs sitting in OLD transcripts when it renders them
|
|
2377
|
+
// (`resolveApiUrl`), but a URL minted today must be fetchable as written.
|
|
2378
|
+
return { name, url: `/api/v1/runs/${runId}/images/${name}`, path };
|
|
2100
2379
|
}
|
|
2101
2380
|
return null;
|
|
2102
2381
|
}
|
|
@@ -2123,6 +2402,68 @@ export class RunManager {
|
|
|
2123
2402
|
state.idleTimer = undefined;
|
|
2124
2403
|
}
|
|
2125
2404
|
}
|
|
2405
|
+
reconcileMonitoringWakeTimers() {
|
|
2406
|
+
for (const runId of this.monitoring) {
|
|
2407
|
+
const state = this.active.get(runId);
|
|
2408
|
+
if (state)
|
|
2409
|
+
this.armMonitoringWakeTimer(runId, state);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
armMonitoringWakeTimer(runId, state) {
|
|
2413
|
+
const minutes = this.semaphore.monitoringWakeIntervalMinutes();
|
|
2414
|
+
if (minutes === null) {
|
|
2415
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
if ((state.monitoringWakeups ?? 0) >= MAX_AUTO_CONTINUES) {
|
|
2419
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
2420
|
+
if (!this.store.getRun(runId)?.monitoringWakeCapReached) {
|
|
2421
|
+
this.store.updateRun(runId, { monitoringWakeCapReached: true });
|
|
2422
|
+
this.store.appendEvent(runId, {
|
|
2423
|
+
type: 'note',
|
|
2424
|
+
message: `automatic monitoring wake-up cap reached (${MAX_AUTO_CONTINUES}); session remains parked`,
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
if (state.monitoringWakeTimer && state.monitoringWakeIntervalMinutes === minutes)
|
|
2430
|
+
return;
|
|
2431
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
2432
|
+
state.monitoringWakeIntervalMinutes = minutes;
|
|
2433
|
+
this.store.updateRun(runId, { monitoringWakeCapReached: undefined });
|
|
2434
|
+
const deadline = Date.now() + minutes * 60_000;
|
|
2435
|
+
this.store.updateRun(runId, { monitoringWakeAt: new Date(deadline).toISOString() });
|
|
2436
|
+
state.monitoringWakeTimer = setTimeout(() => {
|
|
2437
|
+
state.monitoringWakeTimer = undefined;
|
|
2438
|
+
this.store.updateRun(runId, { monitoringWakeAt: undefined });
|
|
2439
|
+
if (!this.monitoring.has(runId) || !state.session?.open || state.cancelled)
|
|
2440
|
+
return;
|
|
2441
|
+
const wakeups = state.monitoringWakeups ?? 0;
|
|
2442
|
+
if (wakeups >= MAX_AUTO_CONTINUES) {
|
|
2443
|
+
this.store.updateRun(runId, { monitoringWakeCapReached: true });
|
|
2444
|
+
this.store.appendEvent(runId, {
|
|
2445
|
+
type: 'note',
|
|
2446
|
+
message: `automatic monitoring wake-up cap reached (${MAX_AUTO_CONTINUES}); session remains parked`,
|
|
2447
|
+
});
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
state.monitoringWakeups = wakeups + 1;
|
|
2451
|
+
this.store.appendEvent(runId, {
|
|
2452
|
+
type: 'note',
|
|
2453
|
+
message: `automatic monitoring wake-up (${state.monitoringWakeups}/${MAX_AUTO_CONTINUES})`,
|
|
2454
|
+
});
|
|
2455
|
+
this.deliverMessage(runId, [{ type: 'text', text: MONITORING_WAKE_NUDGE }], false);
|
|
2456
|
+
}, Math.max(0, deadline - Date.now()));
|
|
2457
|
+
state.monitoringWakeTimer.unref?.();
|
|
2458
|
+
}
|
|
2459
|
+
clearMonitoringWakeTimer(state, runId) {
|
|
2460
|
+
if (state.monitoringWakeTimer)
|
|
2461
|
+
clearTimeout(state.monitoringWakeTimer);
|
|
2462
|
+
state.monitoringWakeTimer = undefined;
|
|
2463
|
+
state.monitoringWakeIntervalMinutes = undefined;
|
|
2464
|
+
if (runId)
|
|
2465
|
+
this.store.updateRun(runId, { monitoringWakeAt: undefined });
|
|
2466
|
+
}
|
|
2126
2467
|
/** Autosave-commit the worktree every 90 s while the run lives (spec 006).
|
|
2127
2468
|
* Opt-in via CEZ_AUTOSAVE=1 (#471) — see periodicAutosaveEnabled. */
|
|
2128
2469
|
armAutosave(state) {
|
|
@@ -2244,4 +2585,35 @@ export function skillSystemPrompt(skill) {
|
|
|
2244
2585
|
lines.push('', 'Skill instructions:', skill.body.trim());
|
|
2245
2586
|
return lines.join('\n');
|
|
2246
2587
|
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Expand a registry-backed slash skill in a live chat message before it reaches
|
|
2590
|
+
* a backend. Claude otherwise intercepts an unknown leading slash command, and
|
|
2591
|
+
* Codex/OpenCode have no native slash-skill lookup at all (#676).
|
|
2592
|
+
*
|
|
2593
|
+
* Only the first text block is eligible, only at character zero, and unknown
|
|
2594
|
+
* commands pass through byte-for-byte. The caller persists the original user
|
|
2595
|
+
* content before applying this delivery-only rewrite.
|
|
2596
|
+
*/
|
|
2597
|
+
export function expandRegistrySlashSkill(content, skills) {
|
|
2598
|
+
const textIndex = content.findIndex((block) => block.type === 'text');
|
|
2599
|
+
if (textIndex < 0)
|
|
2600
|
+
return content;
|
|
2601
|
+
const block = content[textIndex];
|
|
2602
|
+
if (!block || block.type !== 'text')
|
|
2603
|
+
return content;
|
|
2604
|
+
const match = /^\/([A-Za-z0-9][A-Za-z0-9._-]*)(?=\s|$)/.exec(block.text);
|
|
2605
|
+
if (!match)
|
|
2606
|
+
return content;
|
|
2607
|
+
const skill = skills.find((candidate) => candidate.name === match[1]);
|
|
2608
|
+
if (!skill)
|
|
2609
|
+
return content;
|
|
2610
|
+
const request = block.text.slice(match[0].length).trim();
|
|
2611
|
+
const replacement = {
|
|
2612
|
+
type: 'text',
|
|
2613
|
+
text: request ? `${skillSystemPrompt(skill)}\n\nUser request:\n${request}` : skillSystemPrompt(skill),
|
|
2614
|
+
};
|
|
2615
|
+
const expanded = [...content];
|
|
2616
|
+
expanded[textIndex] = replacement;
|
|
2617
|
+
return expanded;
|
|
2618
|
+
}
|
|
2247
2619
|
//# sourceMappingURL=run.js.map
|