@wjarka/cezarion 0.14.6 → 0.14.7
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 +37 -2
- package/dist/agent-config/model-settings/shared.d.ts +2 -1
- package/dist/agent-config/model-settings/shared.js +1 -1
- package/dist/agent-config/model-settings/shared.js.map +1 -1
- package/dist/ci-wait/client.d.ts +4 -0
- package/dist/ci-wait/client.js +67 -0
- package/dist/ci-wait/client.js.map +1 -0
- package/dist/ci-wait/controller.d.ts +78 -0
- package/dist/ci-wait/controller.js +148 -0
- package/dist/ci-wait/controller.js.map +1 -0
- package/dist/ci-wait/github.d.ts +61 -0
- package/dist/ci-wait/github.js +209 -0
- package/dist/ci-wait/github.js.map +1 -0
- package/dist/ci-wait/injection.d.ts +3 -0
- package/dist/ci-wait/injection.js +26 -0
- package/dist/ci-wait/injection.js.map +1 -0
- package/dist/ci-wait/mcp.d.ts +97 -0
- package/dist/ci-wait/mcp.js +35 -0
- package/dist/ci-wait/mcp.js.map +1 -0
- package/dist/ci-wait/resources.d.ts +7 -0
- package/dist/ci-wait/resources.js +32 -0
- package/dist/ci-wait/resources.js.map +1 -0
- package/dist/ci-wait/supervisor.d.ts +21 -0
- package/dist/ci-wait/supervisor.js +163 -0
- package/dist/ci-wait/supervisor.js.map +1 -0
- package/dist/contract/ci-wait.d.ts +151 -0
- package/dist/contract/conversations.d.ts +7 -0
- package/dist/contract/delegation.d.ts +2 -0
- package/dist/contract/events.d.ts +3 -0
- package/dist/contract/index.d.ts +1 -0
- package/dist/contract/index.js +1016 -935
- package/dist/contract/runs.d.ts +461 -0
- package/dist/contract/skills.d.ts +92 -0
- package/dist/core/agent-env.js +2 -0
- package/dist/core/agent-env.js.map +1 -1
- package/dist/core/agent-runner.d.ts +6 -0
- package/dist/core/agent-runner.js +1 -0
- package/dist/core/agent-runner.js.map +1 -1
- package/dist/core/claude-cli-runner.js +10 -0
- package/dist/core/claude-cli-runner.js.map +1 -1
- package/dist/core/codex-app-server-runner.js +5 -1
- package/dist/core/codex-app-server-runner.js.map +1 -1
- package/dist/core/codex-app-server-transport.js +3 -1
- package/dist/core/codex-app-server-transport.js.map +1 -1
- package/dist/core/cursor-acp-runner.js +9 -1
- package/dist/core/cursor-acp-runner.js.map +1 -1
- package/dist/core/opencode-server-runner.js +6 -2
- package/dist/core/opencode-server-runner.js.map +1 -1
- package/dist/core/pi-runner.js +7 -1
- package/dist/core/pi-runner.js.map +1 -1
- package/dist/delegation/cli.js +8 -4
- package/dist/delegation/cli.js.map +1 -1
- package/dist/delegation/conversations.js +2 -1
- package/dist/delegation/conversations.js.map +1 -1
- package/dist/delegation/input.d.ts +6 -0
- package/dist/delegation/input.js +22 -0
- package/dist/delegation/input.js.map +1 -1
- package/dist/delegation/provision.js +3 -1
- package/dist/delegation/provision.js.map +1 -1
- package/dist/delegation/routes.d.ts +11 -0
- package/dist/delegation/service.js +31 -7
- package/dist/delegation/service.js.map +1 -1
- package/dist/delegation/transport.d.ts +11 -0
- package/dist/runs/delegation-state.d.ts +2 -0
- package/dist/runs/run-index.js +2 -3
- package/dist/runs/run-index.js.map +1 -1
- package/dist/runs/store.d.ts +514 -3
- package/dist/runs/store.js +85 -6
- package/dist/runs/store.js.map +1 -1
- package/dist/server/server.d.ts +1376 -12
- package/dist/workflows/run.d.ts +25 -1
- package/dist/workflows/run.js +470 -43
- package/dist/workflows/run.js.map +1 -1
- package/package.json +4 -3
- package/scripts/mock-ci-tool.mjs +53 -0
- package/scripts/mock-claude.mjs +8 -0
- package/scripts/mock-codex-app-server.mjs +212 -0
- package/scripts/mock-cursor-acp.mjs +6 -2
- package/scripts/mock-opencode-serve.mjs +392 -0
- package/scripts/mock-pi-rpc.mjs +7 -0
- package/scripts/pi-ci-wait.mjs +14 -0
- package/web/dist/assets/{agent-accounts-lP_xK83o.js → agent-accounts-BZm_2WAv.js} +1 -1
- package/web/dist/assets/{alert-dialog-CmDuefYh.js → alert-dialog-B422RI-k.js} +1 -1
- package/web/dist/assets/{collapsible-BqX8REmY.js → collapsible-Cy_Fmkrd.js} +1 -1
- package/web/dist/assets/{commit-list-BWvv1H7A.js → commit-list-CLxXMZvP.js} +1 -1
- package/web/dist/assets/{compare-variants-Bg7eLW3z.js → compare-variants-D2dRj6H_.js} +1 -1
- package/web/dist/assets/{diff-controls-HwWHHait.js → diff-controls-BGrXGi7O.js} +1 -1
- package/web/dist/assets/{diff-stat-mNkhAc5m.js → diff-stat-Cgnl_hYF.js} +1 -1
- package/web/dist/assets/{diff-0GBum40Q2.js → diff-tRE59UNb2.js} +2 -2
- package/web/dist/assets/{diff-view-DW64doDY.js → diff-view-DN1ort7Q.js} +1 -1
- package/web/dist/assets/{dropdown-menu-sZjYPcLh.js → dropdown-menu-dwDGT8MM.js} +1 -1
- package/web/dist/assets/{git-toolbar-DBW5EIcY.js → git-toolbar-UiHnGvE0.js} +1 -1
- package/web/dist/assets/{github-CkZ4hWI5.js → github-CaGjGQnW.js} +1 -1
- package/web/dist/assets/{image-preview-DZicGNkU.js → image-preview-C2JGcBlC.js} +1 -1
- package/web/dist/assets/index-CXZukB1O.css +1 -0
- package/web/dist/assets/{index-DWWI-B9n.js → index-D0RwD9-c.js} +3 -3
- package/web/dist/assets/{markdown-AY2K3kIV.js → markdown-C_IcKVQd.js} +1 -1
- package/web/dist/assets/{pill-CcJqA1cL.js → pill-BsbO6G7y.js} +1 -1
- package/web/dist/assets/{project-router-SwsXRKXs.js → project-router-DbLkll25.js} +1 -1
- package/web/dist/assets/{prompt-templates-iV2WD2jy.js → prompt-templates-BlTGJB5W.js} +1 -1
- package/web/dist/assets/{repo-git-BW3TOs3U.js → repo-git-CWD2dO8S.js} +1 -1
- package/web/dist/assets/{run-diff-DPUNj56q.js → run-diff-CyG0Nciv.js} +1 -1
- package/web/dist/assets/{run-header-CSaJIGXl.css → run-header-BJQfFL2l.css} +1 -1
- package/web/dist/assets/run-header-IFyIGCey.js +1 -0
- package/web/dist/assets/{skills-Dv8Vwv5D.js → skills-e7wHnuoc.js} +1 -1
- package/web/dist/assets/{tab-link-DSftvkZS.js → tab-link-BWNp3mLY.js} +1 -1
- package/web/dist/assets/{task-changes-CTCuVls1.js → task-changes-CpJImtq8.js} +1 -1
- package/web/dist/assets/{task-commits-weJQ5GSl.js → task-commits-hujrykPI.js} +1 -1
- package/web/dist/assets/{task-files-B4bUimY8.js → task-files-CjTiGtjt.js} +1 -1
- package/web/dist/assets/{task-thread-BuXrVB9K.js → task-thread-DDPFDS8C.js} +4 -4
- package/web/dist/assets/task-thread-Dc3wplva.css +1 -0
- package/web/dist/assets/{textarea-CxRdTWlq.js → textarea-FjaU370i.js} +1 -1
- package/web/dist/assets/thread-loading-77sJ8Yu_.js +1 -0
- package/web/dist/assets/{token-metrics-Z3UFYbuD.js → token-metrics-Clxh03bE.js} +1 -1
- package/web/dist/assets/utils-PYYcpCFZ.js +64 -0
- package/web/dist/assets/{workflows-vA27Yi8W.js → workflows-DGUQmWy7.js} +1 -1
- package/web/dist/assets/{zoomable-image-DUE1KFJ0.js → zoomable-image-Z3JDnm0b.js} +1 -1
- package/web/dist/index.html +14 -14
- package/web/dist/assets/index-DK0Mkv2m.css +0 -1
- package/web/dist/assets/run-header-DbDluWoV.js +0 -1
- package/web/dist/assets/task-thread-DMRD0IVt.css +0 -1
- package/web/dist/assets/thread-loading-JlP6dUpX.js +0 -1
- package/web/dist/assets/utils-CUnaAifW.js +0 -64
package/dist/workflows/run.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ciWaitRequestSchema, ciWaitResultSchema } from '../contract/index.js';
|
|
2
|
+
import { acquireCiResources } from '../ci-wait/resources.js';
|
|
1
3
|
import { workerContextHash, workerWorkflowHash, workerStepIdentity, captureWorkerAccount, boundWorkerAccountEnv, WorkerIdentityError } from '../delegation/execution-identity.js';
|
|
2
4
|
import { buildChildEnv } from '../core/agent-env.js';
|
|
3
5
|
import { randomUUID } from 'node:crypto';
|
|
@@ -27,7 +29,7 @@ import { autosaveCommit, createWorktree, resolveBaseRef, worktreeDiff, worktreeS
|
|
|
27
29
|
import { getHeadCommit, getRepoInfo } from '../server/git.js';
|
|
28
30
|
import { ensureOwnedWorkspace, verifyOwnedWorkspace } from '../delegation/workspace.js';
|
|
29
31
|
import { verifyWorkerContext } from '../delegation/context.js';
|
|
30
|
-
import { enqueueAgentInput,
|
|
32
|
+
import { enqueueAgentInput, agentInputBatch } from '../delegation/input.js';
|
|
31
33
|
import { parseDelegationEffort } from '../delegation/effort.js';
|
|
32
34
|
import { DelegationPolicyError } from '../delegation/policy.js';
|
|
33
35
|
import { reconcileWorkerWait } from '../delegation/wait.js';
|
|
@@ -477,6 +479,14 @@ export class RunManager {
|
|
|
477
479
|
if (this.executions.get(runId) === execution)
|
|
478
480
|
this.executions.delete(runId);
|
|
479
481
|
execution.resolve();
|
|
482
|
+
if (!this.disposed) {
|
|
483
|
+
try {
|
|
484
|
+
this.queueCiWake(runId);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
this.retryCiAdmission(runId);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
480
490
|
if (!this.disposed && this.store.getRun(runId)?.stopping)
|
|
481
491
|
this.store.updateRun(runId, { stopping: undefined });
|
|
482
492
|
if (!this.disposed)
|
|
@@ -572,6 +582,13 @@ export class RunManager {
|
|
|
572
582
|
/** Durable monitoring subset. Only the configured number receives the waiting-slot exemption. */
|
|
573
583
|
monitoring = new Set();
|
|
574
584
|
/** Separate from human waiting: these resumes MUST reacquire scheduler capacity. */
|
|
585
|
+
ciWakeQueuedAt = new Map();
|
|
586
|
+
ciWakeAdmitted = new Set();
|
|
587
|
+
ciWatches = new Map();
|
|
588
|
+
ciRegistrations = new Map();
|
|
589
|
+
ciRetries = new Map();
|
|
590
|
+
ciResources;
|
|
591
|
+
ciSupervisor;
|
|
575
592
|
workerWaiting = new Set();
|
|
576
593
|
workerWakeAdmitted = new Set();
|
|
577
594
|
workerWakeQueuedAt = new Map();
|
|
@@ -580,6 +597,8 @@ export class RunManager {
|
|
|
580
597
|
recovering = false;
|
|
581
598
|
disposed = false;
|
|
582
599
|
onDelegationRun = (run) => {
|
|
600
|
+
if (!this.disposed && !['queued', 'running', 'waiting'].includes(run.status))
|
|
601
|
+
this.withdrawCiWait(run.id);
|
|
583
602
|
if (run.delegation && !['queued', 'running', 'waiting'].includes(run.status))
|
|
584
603
|
this.active.get(run.id)?.revokeDelegation?.();
|
|
585
604
|
if (this.disposed || this.recovering || this.reconcilingWorkers)
|
|
@@ -647,6 +666,8 @@ export class RunManager {
|
|
|
647
666
|
this.repoRoot = repoRoot;
|
|
648
667
|
this.dataDir = join(repoRoot, '.ai/cezar');
|
|
649
668
|
this.semaphore = options.semaphore ?? new WorkspaceSemaphore();
|
|
669
|
+
this.ciResources = acquireCiResources(this.semaphore);
|
|
670
|
+
this.ciSupervisor = this.ciResources.supervisor;
|
|
650
671
|
this.offSemaphore = this.semaphore.register({
|
|
651
672
|
busySlots: () => this.busySlots(),
|
|
652
673
|
pump: () => this.pump(),
|
|
@@ -672,6 +693,16 @@ export class RunManager {
|
|
|
672
693
|
*/
|
|
673
694
|
dispose() {
|
|
674
695
|
this.disposed = true;
|
|
696
|
+
for (const task of this.ciWatches.values())
|
|
697
|
+
task.abort.abort();
|
|
698
|
+
this.ciWatches.clear();
|
|
699
|
+
for (const task of this.ciRegistrations.values())
|
|
700
|
+
task.abort.abort();
|
|
701
|
+
this.ciRegistrations.clear();
|
|
702
|
+
for (const timer of this.ciRetries.values())
|
|
703
|
+
clearTimeout(timer);
|
|
704
|
+
this.ciRetries.clear();
|
|
705
|
+
this.ciResources.release();
|
|
675
706
|
this.delegationProvisioner = undefined;
|
|
676
707
|
this.finalizedWorkers.clear();
|
|
677
708
|
for (const settle of this.terminationWaiters)
|
|
@@ -687,6 +718,8 @@ export class RunManager {
|
|
|
687
718
|
this.offSemaphore();
|
|
688
719
|
clearInterval(this.queueWatchdog);
|
|
689
720
|
for (const [runId, state] of this.active) {
|
|
721
|
+
state.revokeCiTools?.();
|
|
722
|
+
state.revokeCiTools = undefined;
|
|
690
723
|
state.revokeDelegation?.();
|
|
691
724
|
state.revokeDelegation = undefined;
|
|
692
725
|
this.clearIdleTimer(state);
|
|
@@ -917,6 +950,196 @@ export class RunManager {
|
|
|
917
950
|
state.revokeDelegation = session?.revoke;
|
|
918
951
|
return session;
|
|
919
952
|
}
|
|
953
|
+
/** Provision the same capability and descriptor on every start, Continue and recovered launch. */
|
|
954
|
+
async provisionCiSession(runId, state) {
|
|
955
|
+
state.revokeCiTools?.();
|
|
956
|
+
state.revokeCiTools = undefined;
|
|
957
|
+
state.ciGeneration = randomUUID();
|
|
958
|
+
state.ciTurnId = randomUUID();
|
|
959
|
+
const generation = state.ciGeneration;
|
|
960
|
+
const controller = await this.ciResources.controller();
|
|
961
|
+
if (!controller || this.disposed || state.cancelled || this.active.get(runId) !== state)
|
|
962
|
+
return;
|
|
963
|
+
const provisioned = controller.provision((request, signal) => this.registerCiWait(runId, request, generation, signal));
|
|
964
|
+
state.revokeCiTools = provisioned.revoke;
|
|
965
|
+
return provisioned;
|
|
966
|
+
}
|
|
967
|
+
/** Trusted capability callbacks supply run identity; the model supplies only a PR and deadline. */
|
|
968
|
+
async registerCiWait(runId, request, generation, capabilitySignal) {
|
|
969
|
+
const parsed = ciWaitRequestSchema.parse(request);
|
|
970
|
+
const state = this.active.get(runId);
|
|
971
|
+
const authorized = () => {
|
|
972
|
+
const run = this.store.getRun(runId);
|
|
973
|
+
return !this.disposed && !capabilitySignal?.aborted && !!run && ['running', 'waiting'].includes(run.status) && !run.stopping &&
|
|
974
|
+
this.active.get(runId) === state && !!state?.session?.open && !state.cancelled && !state.finishRequested &&
|
|
975
|
+
state.ciGeneration === generation && !state.pendingHumanAsk && !this.hasUnansweredHumanAsk(runId) &&
|
|
976
|
+
!this.workerWait(runId) && !this.workerExecutionStopped(runId) && !this.executionBlockedByRootFinish(run);
|
|
977
|
+
};
|
|
978
|
+
if (!authorized() || !state)
|
|
979
|
+
throw Object.assign(new Error('Run cannot register a CI wait in its current state'), { code: 'unauthorized' });
|
|
980
|
+
const current = this.store.getRun(runId)?.ciWait;
|
|
981
|
+
if (current) {
|
|
982
|
+
if (current.prUrl.toLowerCase() === parsed.pr.replace(/\/$/, '').toLowerCase() && current.timeoutSeconds === parsed.timeout_seconds)
|
|
983
|
+
return current;
|
|
984
|
+
throw Object.assign(new Error('wait_conflict: a different CI wait is already registered'), { code: 'wait_conflict' });
|
|
985
|
+
}
|
|
986
|
+
const pending = this.ciRegistrations.get(runId);
|
|
987
|
+
if (pending) {
|
|
988
|
+
if (pending.pr === parsed.pr && pending.seconds === parsed.timeout_seconds)
|
|
989
|
+
return pending.promise;
|
|
990
|
+
throw Object.assign(new Error('wait_conflict: a different CI registration is already in progress'), { code: 'wait_conflict' });
|
|
991
|
+
}
|
|
992
|
+
const turnId = state.ciTurnId ??= randomUUID();
|
|
993
|
+
const abort = new AbortController();
|
|
994
|
+
const signal = capabilitySignal ? AbortSignal.any([abort.signal, capabilitySignal]) : abort.signal;
|
|
995
|
+
const promise = (async () => {
|
|
996
|
+
const identity = await this.ciSupervisor.resolve(parsed.pr, signal);
|
|
997
|
+
if (signal.aborted || !authorized() || state.ciTurnId !== turnId)
|
|
998
|
+
throw Object.assign(new Error('Run cannot register a CI wait after interruption'), { code: 'unauthorized' });
|
|
999
|
+
const registeredAt = new Date().toISOString();
|
|
1000
|
+
const wait = { ...identity, id: randomUUID(), generation, turnId, timeoutSeconds: parsed.timeout_seconds,
|
|
1001
|
+
registeredAt, deadline: new Date(Date.now() + parsed.timeout_seconds * 1000).toISOString(), phase: 'registered' };
|
|
1002
|
+
try {
|
|
1003
|
+
this.store.commitCiWait(runId, wait);
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
throw Object.assign(new Error('CI registration could not be saved; retry when storage is writable'), { code: 'persistence' });
|
|
1007
|
+
}
|
|
1008
|
+
this.startCiWatch(runId, wait);
|
|
1009
|
+
if (state.atTurnBoundary === state.session || this.waiting.has(runId))
|
|
1010
|
+
this.parkCiWait(runId, state);
|
|
1011
|
+
return this.store.getRun(runId).ciWait ?? wait;
|
|
1012
|
+
})();
|
|
1013
|
+
this.ciRegistrations.set(runId, { abort, pr: parsed.pr, seconds: parsed.timeout_seconds, promise });
|
|
1014
|
+
try {
|
|
1015
|
+
return await promise;
|
|
1016
|
+
}
|
|
1017
|
+
finally {
|
|
1018
|
+
if (this.ciRegistrations.get(runId)?.abort === abort)
|
|
1019
|
+
this.ciRegistrations.delete(runId);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
startCiWatch(runId, wait) {
|
|
1023
|
+
if (this.disposed || this.ciWatches.has(runId) || wait.result)
|
|
1024
|
+
return;
|
|
1025
|
+
const abort = new AbortController();
|
|
1026
|
+
const watch = { id: wait.id, abort };
|
|
1027
|
+
this.ciWatches.set(runId, watch);
|
|
1028
|
+
const current = () => !this.disposed && !abort.signal.aborted && this.ciWatches.get(runId) === watch &&
|
|
1029
|
+
this.store.getRun(runId)?.ciWait?.id === wait.id;
|
|
1030
|
+
const settle = (result) => {
|
|
1031
|
+
if (!current())
|
|
1032
|
+
return;
|
|
1033
|
+
try {
|
|
1034
|
+
const latest = this.store.getRun(runId)?.ciWait;
|
|
1035
|
+
if (!latest || latest.id !== wait.id)
|
|
1036
|
+
return;
|
|
1037
|
+
const parsed = ciWaitResultSchema.safeParse(result);
|
|
1038
|
+
const validated = parsed.success ? parsed.data : { outcome: 'error', headSha: wait.headSha,
|
|
1039
|
+
observedAt: new Date().toISOString(), checks: [], totalChecks: 0, truncated: false,
|
|
1040
|
+
diagnostic: 'CI watcher returned an invalid observation; inspect the PR and register a new wait.' };
|
|
1041
|
+
const settled = { ...latest, phase: 'wake-pending', result: validated, wakeId: wait.id };
|
|
1042
|
+
const input = { id: wait.id, parentRunId: runId, source: 'lifecycle', createdAt: new Date().toISOString(),
|
|
1043
|
+
text: `CI wait observation ${wait.id} for ${wait.prUrl}. External data follows: ${JSON.stringify(validated)}. This observation is not merge approval or task completion. Reported checks may not include every expected workflow. Duplicate observations retain this ID.` };
|
|
1044
|
+
this.store.commitCiWait(runId, settled, input);
|
|
1045
|
+
this.ciWatches.delete(runId);
|
|
1046
|
+
try {
|
|
1047
|
+
this.queueCiWake(runId);
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
this.retryCiAdmission(runId);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
catch {
|
|
1054
|
+
// A failed durable checkpoint may not drop the accepted intent or publish a false result.
|
|
1055
|
+
if (!current())
|
|
1056
|
+
return;
|
|
1057
|
+
console.warn('[cez] CI wait checkpoint unavailable; retaining result for retry.');
|
|
1058
|
+
const timer = setTimeout(() => { this.ciRetries.delete(runId); settle(result); }, 1000);
|
|
1059
|
+
timer.unref();
|
|
1060
|
+
this.ciRetries.set(runId, timer);
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
void this.ciSupervisor.watch(wait, abort.signal).then(settle, () => settle({ outcome: 'error', headSha: wait.headSha,
|
|
1064
|
+
observedAt: new Date().toISOString(), checks: [], totalChecks: 0, truncated: false,
|
|
1065
|
+
diagnostic: 'CI watcher unavailable; inspect the pull request and retry the wait.' }));
|
|
1066
|
+
}
|
|
1067
|
+
retryCiAdmission(runId) {
|
|
1068
|
+
if (this.disposed || this.ciRetries.has(runId) || !this.store.getRun(runId)?.ciWait)
|
|
1069
|
+
return;
|
|
1070
|
+
const timer = setTimeout(() => {
|
|
1071
|
+
this.ciRetries.delete(runId);
|
|
1072
|
+
try {
|
|
1073
|
+
this.queueCiWake(runId);
|
|
1074
|
+
}
|
|
1075
|
+
catch {
|
|
1076
|
+
this.retryCiAdmission(runId);
|
|
1077
|
+
}
|
|
1078
|
+
}, 1000);
|
|
1079
|
+
timer.unref();
|
|
1080
|
+
this.ciRetries.set(runId, timer);
|
|
1081
|
+
}
|
|
1082
|
+
parkCiWait(runId, state) {
|
|
1083
|
+
const wait = this.store.getRun(runId)?.ciWait;
|
|
1084
|
+
if (!wait || !state.session?.open || state.cancelled || state.pendingHumanAsk || this.ciWakeAdmitted.has(runId))
|
|
1085
|
+
return false;
|
|
1086
|
+
if (wait.phase === 'registered')
|
|
1087
|
+
this.store.commitCiWait(runId, { ...wait, phase: 'parked' });
|
|
1088
|
+
this.clearIdleTimer(state);
|
|
1089
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
1090
|
+
this.waiting.add(runId);
|
|
1091
|
+
this.monitoring.add(runId);
|
|
1092
|
+
this.store.updateRun(runId, { status: 'running', activity: 'monitoring', monitoringWakeAt: undefined });
|
|
1093
|
+
if (state.currentStepId)
|
|
1094
|
+
this.store.updateStep(runId, state.currentStepId, { status: 'running' });
|
|
1095
|
+
this.queueCiWake(runId);
|
|
1096
|
+
this.releaseSlot();
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
1099
|
+
queueCiWake(runId) {
|
|
1100
|
+
const run = this.store.getRun(runId);
|
|
1101
|
+
const wait = run?.ciWait;
|
|
1102
|
+
if (this.disposed || this.recovering || !run || !wait?.result || !wait.wakeId || this.ciWakeQueuedAt.has(runId) ||
|
|
1103
|
+
this.ciWakeAdmitted.has(runId) || !['running', 'waiting', 'queued'].includes(run.status) || run.stopping ||
|
|
1104
|
+
this.hasPendingHumanAsk(runId) || this.workerExecutionStopped(runId) || this.executionBlockedByRootFinish(run))
|
|
1105
|
+
return;
|
|
1106
|
+
const state = this.active.get(runId);
|
|
1107
|
+
if (state) {
|
|
1108
|
+
if (!this.monitoring.has(runId) || state.atTurnBoundary !== state.session || state.pendingHumanAsk)
|
|
1109
|
+
return;
|
|
1110
|
+
this.ciWakeQueuedAt.set(runId, Date.now());
|
|
1111
|
+
if (!this.queue.includes(runId))
|
|
1112
|
+
this.queue.push(runId);
|
|
1113
|
+
}
|
|
1114
|
+
else if (!this.isActive(runId)) {
|
|
1115
|
+
// No live session: create a scheduler-owned continuation, never a direct provider call.
|
|
1116
|
+
this.store.updateRun(runId, { status: 'waiting', activity: undefined });
|
|
1117
|
+
const resumed = this.continueRun(runId, { text: 'Resume to receive the queued CI observation. End this opening turn so Cezar can deliver it; do not poll CI.' }, true);
|
|
1118
|
+
if (!resumed.ok) {
|
|
1119
|
+
this.store.appendEvent(runId, { type: 'note', message: `CI result retained; continuation unavailable: ${resumed.error ?? 'unknown'}` });
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
this.ciWakeQueuedAt.set(runId, Date.now());
|
|
1123
|
+
}
|
|
1124
|
+
this.releaseSlot();
|
|
1125
|
+
}
|
|
1126
|
+
withdrawCiWait(runId, message) {
|
|
1127
|
+
this.ciRegistrations.get(runId)?.abort.abort();
|
|
1128
|
+
this.ciRegistrations.delete(runId);
|
|
1129
|
+
this.store.commitCiWaitWithdrawal(runId, message);
|
|
1130
|
+
this.ciWatches.get(runId)?.abort.abort();
|
|
1131
|
+
this.ciWatches.delete(runId);
|
|
1132
|
+
const timer = this.ciRetries.get(runId);
|
|
1133
|
+
if (timer)
|
|
1134
|
+
clearTimeout(timer);
|
|
1135
|
+
this.ciRetries.delete(runId);
|
|
1136
|
+
if (this.ciWakeQueuedAt.delete(runId) && this.active.has(runId)) {
|
|
1137
|
+
const index = this.queue.indexOf(runId);
|
|
1138
|
+
if (index >= 0)
|
|
1139
|
+
this.queue.splice(index, 1);
|
|
1140
|
+
}
|
|
1141
|
+
this.ciWakeAdmitted.delete(runId);
|
|
1142
|
+
}
|
|
920
1143
|
/** Snapshot of the actual active session, never guessed from partial run settings. */
|
|
921
1144
|
delegationExecutionSettings(runId) {
|
|
922
1145
|
const state = this.active.get(runId);
|
|
@@ -1117,7 +1340,7 @@ export class RunManager {
|
|
|
1117
1340
|
const head = this.queue[0];
|
|
1118
1341
|
if (!head)
|
|
1119
1342
|
return null;
|
|
1120
|
-
const wakeQueuedAt = this.workerWakeQueuedAt.get(head);
|
|
1343
|
+
const wakeQueuedAt = this.workerWakeQueuedAt.get(head) ?? this.ciWakeQueuedAt.get(head);
|
|
1121
1344
|
if (wakeQueuedAt !== undefined)
|
|
1122
1345
|
return wakeQueuedAt;
|
|
1123
1346
|
const createdAt = this.store.getRun(head)?.createdAt;
|
|
@@ -1196,13 +1419,16 @@ export class RunManager {
|
|
|
1196
1419
|
// Only pay for the config read when something is actually held: a queued record may name
|
|
1197
1420
|
// no runner, and then the account it would use is the configured default.
|
|
1198
1421
|
const defaultRunner = anyHold ? (await loadConfig(this.repoRoot)).defaultRunner : undefined;
|
|
1199
|
-
|
|
1422
|
+
// Removing one monitor above the exemption cap needs no new slot: it already holds one.
|
|
1423
|
+
const chargedCiWake = (id) => this.ciWakeQueuedAt.has(id) && this.monitoring.has(id) &&
|
|
1424
|
+
this.monitoring.size > this.semaphore.maxMonitoringSessions();
|
|
1425
|
+
while (this.queue.length > 0) {
|
|
1200
1426
|
// FIFO among the runs that CAN start; a held one keeps its place in the queue rather
|
|
1201
1427
|
// than being dequeued and re-queued (which would churn its position and its record).
|
|
1202
1428
|
const next = this.queue.findIndex((id) => {
|
|
1203
1429
|
const queued = this.store.getRun(id);
|
|
1204
|
-
return !queued || this.historyDeletionPending(id) || ((!this.executions.get(id)?.admitted || this.active.has(id)) && !this.executionBlockedByRootFinish(queued) &&
|
|
1205
|
-
(!anyHold || !accountHeldFor(queued, holds, defaultRunner ?? 'claude')));
|
|
1430
|
+
return (capacity() || chargedCiWake(id)) && (!queued || this.historyDeletionPending(id) || ((!this.executions.get(id)?.admitted || this.active.has(id)) && !this.executionBlockedByRootFinish(queued) &&
|
|
1431
|
+
(!anyHold || !accountHeldFor(queued, holds, defaultRunner ?? 'claude'))));
|
|
1206
1432
|
});
|
|
1207
1433
|
if (next === -1)
|
|
1208
1434
|
break; // every queued run has a durable reason to remain held
|
|
@@ -1226,6 +1452,19 @@ export class RunManager {
|
|
|
1226
1452
|
const runId = this.queue.splice(next, 1)[0];
|
|
1227
1453
|
if (!runId)
|
|
1228
1454
|
break;
|
|
1455
|
+
if (this.ciWakeQueuedAt.has(runId) && this.active.has(runId)) {
|
|
1456
|
+
this.ciWakeQueuedAt.delete(runId);
|
|
1457
|
+
const state = this.active.get(runId);
|
|
1458
|
+
if (!state.pendingHumanAsk && this.store.getRun(runId)?.ciWait?.result && state.atTurnBoundary === state.session) {
|
|
1459
|
+
this.ciWakeAdmitted.add(runId);
|
|
1460
|
+
this.waiting.delete(runId);
|
|
1461
|
+
this.monitoring.delete(runId);
|
|
1462
|
+
this.flushAgentInputs(runId);
|
|
1463
|
+
}
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
if (this.ciWakeQueuedAt.delete(runId))
|
|
1467
|
+
this.ciWakeAdmitted.add(runId);
|
|
1229
1468
|
if (this.workerWakeQueuedAt.has(runId) && this.active.has(runId)) {
|
|
1230
1469
|
this.workerWakeQueuedAt.delete(runId);
|
|
1231
1470
|
const state = this.active.get(runId);
|
|
@@ -1476,6 +1715,15 @@ export class RunManager {
|
|
|
1476
1715
|
this.continueRun(run.id, {}, true);
|
|
1477
1716
|
continue;
|
|
1478
1717
|
}
|
|
1718
|
+
if (run.ciWait && !this.hasPendingHumanAsk(run.id)) {
|
|
1719
|
+
if (run.ciWait.phase === 'registered')
|
|
1720
|
+
this.store.commitCiWait(run.id, { ...run.ciWait, phase: 'parked' });
|
|
1721
|
+
this.store.updateRun(run.id, { status: 'running', activity: 'monitoring', monitoringWakeAt: undefined });
|
|
1722
|
+
this.startCiWatch(run.id, this.store.getRun(run.id).ciWait);
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
if (run.ciWait)
|
|
1726
|
+
this.withdrawCiWait(run.id);
|
|
1479
1727
|
if (run.status === 'queued') {
|
|
1480
1728
|
await this.reviveQueuedRun(run, 'cezar restarted');
|
|
1481
1729
|
continue;
|
|
@@ -1573,6 +1821,12 @@ export class RunManager {
|
|
|
1573
1821
|
this.recovering = false;
|
|
1574
1822
|
}
|
|
1575
1823
|
this.reconcileWorkerWaits();
|
|
1824
|
+
for (const run of this.store.listRuns()) {
|
|
1825
|
+
if (run.ciWait && !['running', 'waiting', 'queued'].includes(run.status))
|
|
1826
|
+
this.withdrawCiWait(run.id);
|
|
1827
|
+
else if (run.ciWait)
|
|
1828
|
+
this.queueCiWake(run.id);
|
|
1829
|
+
}
|
|
1576
1830
|
this.reconcileAutoResumes();
|
|
1577
1831
|
void this.pump();
|
|
1578
1832
|
}
|
|
@@ -1589,7 +1843,7 @@ export class RunManager {
|
|
|
1589
1843
|
}
|
|
1590
1844
|
isDisposedDelegatedRun(runId) {
|
|
1591
1845
|
const role = this.store.getRun(runId)?.delegation?.role;
|
|
1592
|
-
return this.disposed && (role === 'root' || role === 'worker');
|
|
1846
|
+
return this.disposed && (role === 'root' || role === 'worker' || !!this.store.getRun(runId)?.ciWait);
|
|
1593
1847
|
}
|
|
1594
1848
|
/** Shutdown is not completion. Explicit terminal controls retain their precedence. */
|
|
1595
1849
|
preserveRunAfterDisposal(runId, state) {
|
|
@@ -1599,6 +1853,7 @@ export class RunManager {
|
|
|
1599
1853
|
dropActive(runId) {
|
|
1600
1854
|
const state = this.active.get(runId);
|
|
1601
1855
|
state?.releaseRepoRoot?.();
|
|
1856
|
+
state?.revokeCiTools?.();
|
|
1602
1857
|
if (state) {
|
|
1603
1858
|
state.releaseRepoRoot = undefined;
|
|
1604
1859
|
this.clearIdleTimer(state);
|
|
@@ -1608,9 +1863,17 @@ export class RunManager {
|
|
|
1608
1863
|
this.monitoring.delete(runId);
|
|
1609
1864
|
this.workerWaiting.delete(runId);
|
|
1610
1865
|
this.workerWakeAdmitted.delete(runId);
|
|
1866
|
+
this.ciWakeAdmitted.delete(runId);
|
|
1867
|
+
if (this.ciWakeQueuedAt.delete(runId)) {
|
|
1868
|
+
const index = this.queue.indexOf(runId);
|
|
1869
|
+
if (index >= 0)
|
|
1870
|
+
this.queue.splice(index, 1);
|
|
1871
|
+
}
|
|
1611
1872
|
if (state)
|
|
1612
1873
|
this.clearMonitoringWakeTimer(state, runId);
|
|
1613
1874
|
this.active.delete(runId);
|
|
1875
|
+
if (!this.disposed && this.store.getRun(runId)?.ciWait)
|
|
1876
|
+
this.queueCiWake(runId);
|
|
1614
1877
|
if (!this.executions.has(runId) && this.store.getRun(runId)?.stopping)
|
|
1615
1878
|
this.store.updateRun(runId, { stopping: undefined });
|
|
1616
1879
|
this.memoryPausing.delete(runId);
|
|
@@ -2062,6 +2325,8 @@ export class RunManager {
|
|
|
2062
2325
|
this.store.commitWorkerExecutionComplete(runId, proof.generation);
|
|
2063
2326
|
}
|
|
2064
2327
|
}
|
|
2328
|
+
const hadCiWait = !!this.store.getRun(runId)?.ciWait;
|
|
2329
|
+
this.withdrawCiWait(runId);
|
|
2065
2330
|
const cancelledFinish = this.store.commitRootFinishCancellation(runId);
|
|
2066
2331
|
this.withdrawWorkerWait(runId);
|
|
2067
2332
|
// Still waiting in the queue: just drop it there.
|
|
@@ -2087,7 +2352,7 @@ export class RunManager {
|
|
|
2087
2352
|
if (cancelledFinish)
|
|
2088
2353
|
return true;
|
|
2089
2354
|
const run = this.store.getRun(runId);
|
|
2090
|
-
if (run
|
|
2355
|
+
if (run && (run.delegation || hadCiWait) && ['queued', 'running', 'waiting'].includes(run.status)) {
|
|
2091
2356
|
this.store.updateRun(runId, { status: 'cancelled', finishedAt: new Date().toISOString() });
|
|
2092
2357
|
return true;
|
|
2093
2358
|
}
|
|
@@ -2096,6 +2361,8 @@ export class RunManager {
|
|
|
2096
2361
|
state.cancelled = true;
|
|
2097
2362
|
this.store.updateRun(runId, { stopping: true });
|
|
2098
2363
|
this.store.flush();
|
|
2364
|
+
state.revokeCiTools?.();
|
|
2365
|
+
state.revokeCiTools = undefined;
|
|
2099
2366
|
state.revokeDelegation?.();
|
|
2100
2367
|
state.revokeDelegation = undefined;
|
|
2101
2368
|
this.clearIdleTimer(state);
|
|
@@ -2251,7 +2518,9 @@ export class RunManager {
|
|
|
2251
2518
|
if (!run || run.stopping || this.workerExecutionStopped(runId) || this.executionBlockedByRootFinish(run))
|
|
2252
2519
|
return null;
|
|
2253
2520
|
const message = this.toQueuedMessage(runId, content);
|
|
2254
|
-
if (
|
|
2521
|
+
if (run.ciWait)
|
|
2522
|
+
this.withdrawCiWait(runId, message);
|
|
2523
|
+
else if (this.workerWait(runId))
|
|
2255
2524
|
this.withdrawWorkerWait(runId, message);
|
|
2256
2525
|
else
|
|
2257
2526
|
this.store.updateRun(runId, { queuedMessages: [...(run.queuedMessages ?? []), message] });
|
|
@@ -2381,8 +2650,10 @@ export class RunManager {
|
|
|
2381
2650
|
return false;
|
|
2382
2651
|
// The pending maps were removed only AFTER hydration. This entry therefore
|
|
2383
2652
|
// needs live delivery, never a second fold into the already-created opening.
|
|
2384
|
-
const message = this.workerWait(runId) ? this.toQueuedMessage(runId, content) : undefined;
|
|
2385
|
-
if (
|
|
2653
|
+
const message = this.workerWait(runId) || run.ciWait ? this.toQueuedMessage(runId, content) : undefined;
|
|
2654
|
+
if (run.ciWait)
|
|
2655
|
+
this.withdrawCiWait(runId, message);
|
|
2656
|
+
else if (message)
|
|
2386
2657
|
this.withdrawWorkerWait(runId, message);
|
|
2387
2658
|
const pending = this.deferredMessages.get(runId) ?? [];
|
|
2388
2659
|
pending.push({ content, ...(message ? { messageId: message.id } : {}) });
|
|
@@ -2533,10 +2804,9 @@ export class RunManager {
|
|
|
2533
2804
|
const parsed = workerWaitRequestSchema.parse(request);
|
|
2534
2805
|
const run = this.store.getRun(parentId);
|
|
2535
2806
|
const state = this.active.get(parentId);
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
throw new DelegationPolicyError('
|
|
2539
|
-
}
|
|
2807
|
+
this.assertCanRegisterWorkerWait(parentId);
|
|
2808
|
+
if (run?.delegation?.role !== 'root' || !state)
|
|
2809
|
+
throw new DelegationPolicyError('denied_scope', 'Only a parent may wait for owned workers');
|
|
2540
2810
|
for (const id of parsed.workerIds) {
|
|
2541
2811
|
const worker = this.store.getRun(id);
|
|
2542
2812
|
if (worker?.delegation?.role !== 'worker' || worker.delegation.parentRunId !== parentId ||
|
|
@@ -2553,15 +2823,45 @@ export class RunManager {
|
|
|
2553
2823
|
this.parkWorkerWait(parentId, state);
|
|
2554
2824
|
return this.workerWait(parentId);
|
|
2555
2825
|
}
|
|
2826
|
+
assertCanRegisterWorkerWait(runId) {
|
|
2827
|
+
const run = this.store.getRun(runId);
|
|
2828
|
+
const state = this.active.get(runId);
|
|
2829
|
+
const refuse = (message) => { throw new DelegationPolicyError('incompatible_state', message); };
|
|
2830
|
+
if (!run?.delegation || run.delegation.role === 'invalid')
|
|
2831
|
+
refuse('Worker waits require an owned worker or parent run');
|
|
2832
|
+
if (this.historyDeletionPending(runId))
|
|
2833
|
+
refuse('History deletion is pending; finish deletion before registering a wait');
|
|
2834
|
+
if (this.executionBlockedByRootFinish(run))
|
|
2835
|
+
refuse('Parent finish is pending; a finishing parent cannot register a wait');
|
|
2836
|
+
if (state?.pendingHumanAsk || this.hasUnansweredHumanAsk(runId))
|
|
2837
|
+
refuse('A human question is pending; answer it before registering a wait');
|
|
2838
|
+
if (run?.ciWait)
|
|
2839
|
+
refuse('A CI wait is already active; finish or withdraw it before registering a worker wait');
|
|
2840
|
+
const wait = this.workerWait(runId);
|
|
2841
|
+
if (wait)
|
|
2842
|
+
refuse(`Wait ${wait.id} is already active; keep it or run worker cancel-wait ${wait.id} before registering another`);
|
|
2843
|
+
if (!['running', 'waiting'].includes(run.status) || !state?.session?.open || state.cancelled) {
|
|
2844
|
+
refuse(`Run is ${run.status} with no available active session; register a wait from an executing parent or worker`);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
/** An answer already executing in this generation is not an unanswered question.
|
|
2848
|
+
* Keep the durable checkpoint at turn-end for crash recovery, and match its exact
|
|
2849
|
+
* sequence so it can never hide a newer question. */
|
|
2850
|
+
hasUnansweredHumanAsk(runId) {
|
|
2851
|
+
const seq = this.pendingHumanAskSeq(runId);
|
|
2852
|
+
const state = this.active.get(runId);
|
|
2853
|
+
return seq !== undefined && !(state?.session?.open && !state.cancelled && state.openingAnswerAskSeq === seq);
|
|
2854
|
+
}
|
|
2556
2855
|
/** Both roles can select their own requests without acquiring worker-management authority. */
|
|
2557
2856
|
registerRequestWait(runId, request) {
|
|
2558
2857
|
const parsed = requestWaitRequestSchema.parse(request);
|
|
2559
2858
|
this.reconcileWorkerWaits();
|
|
2560
2859
|
const run = this.store.getRun(runId);
|
|
2561
2860
|
const state = this.active.get(runId);
|
|
2861
|
+
this.assertCanRegisterWorkerWait(runId);
|
|
2562
2862
|
if (!run?.delegation || run.delegation.role === 'invalid' || this.historyDeletionPending(runId) ||
|
|
2563
2863
|
this.executionBlockedByRootFinish(run) || !['running', 'waiting'].includes(run.status) ||
|
|
2564
|
-
!state?.session?.open || state.cancelled
|
|
2864
|
+
!state?.session?.open || state.cancelled) {
|
|
2565
2865
|
throw new DelegationPolicyError('incompatible_state', 'run cannot register a request wait');
|
|
2566
2866
|
}
|
|
2567
2867
|
const root = run.delegation.role === 'root' ? run : this.store.getRun(run.delegation.parentRunId);
|
|
@@ -2592,6 +2892,12 @@ export class RunManager {
|
|
|
2592
2892
|
this.executionBlockedByRootFinish(run) || !['queued', 'running', 'waiting'].includes(run.status) ||
|
|
2593
2893
|
this.workerExecutionStopped(runId) || this.hasPendingHumanAsk(runId))
|
|
2594
2894
|
return;
|
|
2895
|
+
// Retain conversation input until the registered CI wait receives scheduler admission.
|
|
2896
|
+
// Converting this monitor into a worker wake would strand both admission paths.
|
|
2897
|
+
if (run.ciWait) {
|
|
2898
|
+
this.queueCiWake(runId);
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2595
2901
|
const input = run.agentInputs?.find(entry => entry.conversation && !entry.deliveredAt);
|
|
2596
2902
|
if (!input)
|
|
2597
2903
|
return;
|
|
@@ -2788,14 +3094,19 @@ export class RunManager {
|
|
|
2788
3094
|
const root = parent.delegation.role === 'root' ? parent : this.store.getRun(parent.delegation.parentRunId);
|
|
2789
3095
|
const requests = root?.delegation?.role === 'root' ? root.delegation.conversation?.outcomes ?? [] : [];
|
|
2790
3096
|
let next = reconcileWorkerWait(selected, outcomes, now, requests);
|
|
2791
|
-
|
|
3097
|
+
// The opening prompt (also during recovery) and a reserved submission are
|
|
3098
|
+
// already being processed. Their delayed receipts cannot interrupt a wait
|
|
3099
|
+
// registered by that very turn. Only inputs still awaiting a turn can.
|
|
3100
|
+
const flight = this.active.get(parent.id)?.agentInputFlight;
|
|
3101
|
+
const pendingInputs = parent.agentInputs?.filter(input => !input.deliveredAt &&
|
|
3102
|
+
input.id !== parent.continuationMessage?.agentInputId && !flight?.inputIds.includes(input.id)) ?? [];
|
|
3103
|
+
const incoming = pendingInputs.find(input => input.conversation &&
|
|
2792
3104
|
!(input.conversation.kind === 'reply' && input.conversation.requestId && wait.requestIds?.includes(input.conversation.requestId)));
|
|
2793
3105
|
if (incoming && next.phase !== 'wake-pending')
|
|
2794
3106
|
next = { ...next, phase: 'wake-pending', reason: 'message', wakeId: incoming.id };
|
|
2795
3107
|
// A full queue of selected replies still needs admission. Use the last
|
|
2796
3108
|
// already accepted input as the checkpoint rather than adding a 33rd input.
|
|
2797
3109
|
// Its content is preserved; delivery includes the wait receipt separately.
|
|
2798
|
-
const pendingInputs = parent.agentInputs?.filter(input => !input.deliveredAt) ?? [];
|
|
2799
3110
|
if (next.phase === 'wake-pending' && pendingInputs.length >= 32 &&
|
|
2800
3111
|
!parent.agentInputs?.some(input => input.id === next.wakeId)) {
|
|
2801
3112
|
next = { ...next, wakeId: pendingInputs.at(-1).id };
|
|
@@ -2989,11 +3300,11 @@ export class RunManager {
|
|
|
2989
3300
|
this.flushAgentInputs(runId);
|
|
2990
3301
|
}
|
|
2991
3302
|
/** Reservation is synchronous; only an acknowledged transport may checkpoint delivery. */
|
|
2992
|
-
submitAgentInput(runId, state, content,
|
|
3303
|
+
submitAgentInput(runId, state, content, inputIds = []) {
|
|
2993
3304
|
const session = state.session;
|
|
2994
3305
|
if (!session?.open || state.agentInputFlight || state.cancelled || state.finishRequested || state.agentInputError || state.pendingHumanAsk)
|
|
2995
3306
|
return false;
|
|
2996
|
-
const flight = { session,
|
|
3307
|
+
const flight = { session, inputIds };
|
|
2997
3308
|
state.agentInputFlight = flight;
|
|
2998
3309
|
const current = () => !this.disposed && this.active.get(runId) === state && state.session === session &&
|
|
2999
3310
|
state.agentInputFlight === flight && !state.cancelled && !state.finishRequested && !this.workerExecutionStopped(runId) &&
|
|
@@ -3036,13 +3347,24 @@ export class RunManager {
|
|
|
3036
3347
|
if (!current())
|
|
3037
3348
|
return;
|
|
3038
3349
|
try {
|
|
3039
|
-
if (
|
|
3350
|
+
if (inputIds.length) {
|
|
3040
3351
|
const queue = this.store.getRun(runId)?.agentInputs ?? [];
|
|
3041
|
-
if (!queue.some(input => input.id
|
|
3352
|
+
if (!queue.some(input => inputIds.includes(input.id) && !input.deliveredAt))
|
|
3042
3353
|
return;
|
|
3043
|
-
this.store.
|
|
3044
|
-
|
|
3045
|
-
|
|
3354
|
+
const ciWakeId = this.store.getRun(runId)?.ciWait?.wakeId;
|
|
3355
|
+
if (ciWakeId && inputIds.includes(ciWakeId)) {
|
|
3356
|
+
// CI lifecycle input owns its turn; never retire a conversation batch with it.
|
|
3357
|
+
if (inputIds.length !== 1)
|
|
3358
|
+
throw new Error('CI observation must be delivered separately');
|
|
3359
|
+
this.store.commitCiWaitDelivery(runId, ciWakeId);
|
|
3360
|
+
this.ciWakeAdmitted.delete(runId);
|
|
3361
|
+
}
|
|
3362
|
+
else {
|
|
3363
|
+
const deliveredAt = new Date().toISOString();
|
|
3364
|
+
this.store.commitAgentInputs(runId, queue.map(input => inputIds.includes(input.id) && !input.deliveredAt
|
|
3365
|
+
? { ...input, deliveredAt } : input));
|
|
3366
|
+
}
|
|
3367
|
+
if (inputIds.includes(this.workerWait(runId)?.wakeId ?? ''))
|
|
3046
3368
|
this.withdrawWorkerWait(runId);
|
|
3047
3369
|
this.reconcileWorkerWaits();
|
|
3048
3370
|
}
|
|
@@ -3059,7 +3381,7 @@ export class RunManager {
|
|
|
3059
3381
|
return;
|
|
3060
3382
|
if (this.flushAgentInputs(runId))
|
|
3061
3383
|
return;
|
|
3062
|
-
if (state.atTurnBoundary !== session || state.pendingHumanAsk || this.workerWait(runId) || this.hasQueuedAgentInputs(runId))
|
|
3384
|
+
if (state.atTurnBoundary !== session || state.pendingHumanAsk || this.workerWait(runId) || this.store.getRun(runId)?.ciWait || this.hasQueuedAgentInputs(runId))
|
|
3063
3385
|
return;
|
|
3064
3386
|
if (state.doneAtBoundary === session) {
|
|
3065
3387
|
if (this.deferParentCompletion(runId))
|
|
@@ -3105,8 +3427,10 @@ export class RunManager {
|
|
|
3105
3427
|
deliveries?.add(flight.settled);
|
|
3106
3428
|
void flight.settled.then(() => deliveries?.delete(flight.settled));
|
|
3107
3429
|
this.resumeParkedRun(runId, state);
|
|
3108
|
-
if (
|
|
3430
|
+
if (inputIds.includes(this.workerWait(runId)?.wakeId ?? ''))
|
|
3109
3431
|
state.workerWakeTurn = session;
|
|
3432
|
+
if (inputIds.includes(this.store.getRun(runId)?.ciWait?.wakeId ?? ''))
|
|
3433
|
+
state.ciWakeTurn = session;
|
|
3110
3434
|
return true;
|
|
3111
3435
|
}
|
|
3112
3436
|
/** False leaves the durable entry untouched. No human-answer/slash-expansion fallback. */
|
|
@@ -3119,9 +3443,23 @@ export class RunManager {
|
|
|
3119
3443
|
(this.workerWait(runId)?.phase === 'registered') ||
|
|
3120
3444
|
(this.workerWait(runId)?.phase === 'wake-pending' && !this.workerWakeAdmitted.has(runId)))
|
|
3121
3445
|
return false;
|
|
3122
|
-
|
|
3123
|
-
|
|
3446
|
+
if (state.pendingHumanAsk || this.hasUnansweredHumanAsk(runId))
|
|
3447
|
+
return false;
|
|
3448
|
+
// The opening prompt already carries this input. It must not be resubmitted
|
|
3449
|
+
// by a readiness callback before the first successful turn checkpoints it.
|
|
3450
|
+
if (state.openingAgentInputId)
|
|
3124
3451
|
return false;
|
|
3452
|
+
if (run.ciWait) {
|
|
3453
|
+
if (!this.ciWakeAdmitted.has(runId) || state.atTurnBoundary !== state.session)
|
|
3454
|
+
return false;
|
|
3455
|
+
// Preserve CI admission and atomic retirement; conversations batch on the next turn.
|
|
3456
|
+
const input = run.agentInputs?.find(entry => entry.id === run.ciWait?.wakeId && !entry.deliveredAt);
|
|
3457
|
+
return !!input && this.submitAgentInput(runId, state, [{ type: 'text', text: this.formatAgentInput(runId, input) }], [input.id]);
|
|
3458
|
+
}
|
|
3459
|
+
const batch = agentInputBatch(run.agentInputs ?? [], input => this.formatAgentInput(runId, input));
|
|
3460
|
+
return !!batch && this.submitAgentInput(runId, state, [{ type: 'text', text: batch.text }], batch.inputs.map(input => input.id));
|
|
3461
|
+
}
|
|
3462
|
+
formatAgentInput(runId, input) {
|
|
3125
3463
|
const attribution = input.conversation;
|
|
3126
3464
|
const text = attribution
|
|
3127
3465
|
? `Agent conversation ${JSON.stringify({ id: input.id, ...attribution })}${this.workerWait(runId)?.reason === 'message' ? `; interrupted wait ${this.workerWait(runId).id} to receive this message; outstanding requests remain pending` : ''}\n${input.text}`
|
|
@@ -3129,7 +3467,12 @@ export class RunManager {
|
|
|
3129
3467
|
const wait = this.workerWait(runId);
|
|
3130
3468
|
const receipt = wait?.wakeId === input.id && wait.wakeId !== wait.id && wait.reason !== 'message'
|
|
3131
3469
|
? `\nWait ${wait.id} (${wait.reason}); request outcomes: ${JSON.stringify(wait.requestOutcomes ?? [])}; worker outcomes: ${JSON.stringify(wait.outcomes.map(outcome => ({ ...outcome, ...(outcome.summary ? { summary: outcome.summary.slice(0, 256) } : {}) })))}. This is not review approval.` : '';
|
|
3132
|
-
return
|
|
3470
|
+
return text + receipt + this.agentInputOutcomeText(input);
|
|
3471
|
+
}
|
|
3472
|
+
agentInputOutcomeText(input) {
|
|
3473
|
+
const root = this.store.getRun(input.parentRunId);
|
|
3474
|
+
const outcome = input.conversation && root?.delegation?.role === 'root' ? root.delegation.conversation?.outcomes.find(entry => entry.requestId === (input.conversation?.requestId ?? input.id)) : undefined;
|
|
3475
|
+
return outcome ? `\nRequest outcome: ${outcome.status}. This delivery does not reopen the settled request.` : '';
|
|
3133
3476
|
}
|
|
3134
3477
|
/** Deliver anything `deferMessage` buffered, once the session is live. */
|
|
3135
3478
|
flushDeferred(runId) {
|
|
@@ -3208,6 +3551,8 @@ export class RunManager {
|
|
|
3208
3551
|
const run = this.store.getRun(runId);
|
|
3209
3552
|
if (!run || run.stopping || this.workerExecutionStopped(runId) || this.executionBlockedByRootFinish(run))
|
|
3210
3553
|
return false;
|
|
3554
|
+
if (!userAuthored && run.ciWait)
|
|
3555
|
+
return false;
|
|
3211
3556
|
const text = content
|
|
3212
3557
|
.filter((b) => b.type === 'text')
|
|
3213
3558
|
.map((b) => b.text)
|
|
@@ -3217,7 +3562,10 @@ export class RunManager {
|
|
|
3217
3562
|
// these as user attachments (vs. agent tool screenshots) on disk (#357).
|
|
3218
3563
|
const persisted = userAuthored ? this.persistPastedAttachments(runId, content) : [];
|
|
3219
3564
|
const images = persisted.map((saved) => saved.url);
|
|
3565
|
+
const ciHumanMessage = userAuthored && run.ciWait
|
|
3566
|
+
? { id: randomUUID(), text, images, createdAt: new Date().toISOString() } : undefined;
|
|
3220
3567
|
if (userAuthored) {
|
|
3568
|
+
this.withdrawCiWait(runId, ciHumanMessage);
|
|
3221
3569
|
this.withdrawWorkerWait(runId);
|
|
3222
3570
|
this.store.appendEvent(runId, {
|
|
3223
3571
|
type: 'user-message',
|
|
@@ -3240,6 +3588,8 @@ export class RunManager {
|
|
|
3240
3588
|
: this.submitAgentInput(runId, state, deliverable);
|
|
3241
3589
|
if (delivered) {
|
|
3242
3590
|
if (userAuthored) {
|
|
3591
|
+
if (ciHumanMessage)
|
|
3592
|
+
this.store.commitQueuedMessageDelivery(runId, ciHumanMessage.id);
|
|
3243
3593
|
if (answeringAskSeq !== undefined) {
|
|
3244
3594
|
this.store.appendEvent(runId, { type: 'human-input-delivered', askSeq: answeringAskSeq });
|
|
3245
3595
|
}
|
|
@@ -3264,6 +3614,7 @@ export class RunManager {
|
|
|
3264
3614
|
const state = this.active.get(runId);
|
|
3265
3615
|
if (decision.action === 'session' && state?.session) {
|
|
3266
3616
|
try {
|
|
3617
|
+
this.withdrawCiWait(runId);
|
|
3267
3618
|
this.withdrawWorkerWait(runId);
|
|
3268
3619
|
}
|
|
3269
3620
|
catch {
|
|
@@ -3348,7 +3699,7 @@ export class RunManager {
|
|
|
3348
3699
|
continueRun(runId, opts = {},
|
|
3349
3700
|
/** Restart recovery may discover several interrupted tasks at once. Those
|
|
3350
3701
|
* continuations are queued; an explicit user Continue remains immediate. */
|
|
3351
|
-
deferForCapacity = false) {
|
|
3702
|
+
deferForCapacity = false, conversation) {
|
|
3352
3703
|
if (agentModelsLocked(this.repoRoot) && (opts.model?.trim() || opts.effort?.trim())) {
|
|
3353
3704
|
return { ok: false, error: AGENT_MODELS_LOCKED_ERROR };
|
|
3354
3705
|
}
|
|
@@ -3357,6 +3708,9 @@ export class RunManager {
|
|
|
3357
3708
|
const run = this.store.getRun(runId);
|
|
3358
3709
|
if (!run)
|
|
3359
3710
|
return { ok: false, error: 'not found' };
|
|
3711
|
+
if (conversation && (!deferForCapacity || run.delegation?.role !== 'worker' || run.delegation.parentRunId !== conversation.rootId ||
|
|
3712
|
+
!['done', 'review', 'failed'].includes(run.status)))
|
|
3713
|
+
return { ok: false, error: 'Only a settled owned worker can be resumed by its parent message' };
|
|
3360
3714
|
if (run.stopping)
|
|
3361
3715
|
return { ok: false, error: 'run is still stopping' };
|
|
3362
3716
|
if (this.historyDeletionPending(runId))
|
|
@@ -3397,15 +3751,17 @@ export class RunManager {
|
|
|
3397
3751
|
if (this.executionBlockedByRootFinish(run))
|
|
3398
3752
|
return { ok: false, error: 'parent finish is pending' };
|
|
3399
3753
|
const answers = deferForCapacity ? [run.continuationMessage, ...(run.queuedMessages ?? [])] : [opts];
|
|
3400
|
-
if (pendingHumanAsk && ((deferForCapacity && run.continuationMessage?.origin !== 'human') ||
|
|
3754
|
+
if (pendingHumanAsk && (conversation || (deferForCapacity && run.continuationMessage?.origin !== 'human') ||
|
|
3401
3755
|
!answers.some(answer => answer?.text?.trim() || answer?.images?.length))) {
|
|
3402
3756
|
return { ok: false, error: 'pending human question requires an explicit answer' };
|
|
3403
3757
|
}
|
|
3758
|
+
// A disconnected CI monitor has durable intent but no live session to receive human input.
|
|
3759
|
+
const disconnectedCiWait = !!run.ciWait && !this.isActive(runId) && !hasRegisteredRunProcess(runId);
|
|
3404
3760
|
// `review` is continuable too — that's the "Send back" path (spec 009).
|
|
3405
|
-
if (!['done', 'failed', 'cancelled', 'review'].includes(run.status) && !(pendingHumanAsk && run.status === 'waiting') &&
|
|
3761
|
+
if (!disconnectedCiWait && !['done', 'failed', 'cancelled', 'review'].includes(run.status) && !(pendingHumanAsk && run.status === 'waiting') &&
|
|
3406
3762
|
!(run.status === 'waiting' && !this.isActive(runId) && ((!hasRegisteredRunProcess(runId) &&
|
|
3407
3763
|
(run.delegation === undefined || run.delegation.role === 'root')) ||
|
|
3408
|
-
(deferForCapacity && run.delegation?.role === 'worker' && !!this.workerWait(runId))))) {
|
|
3764
|
+
(deferForCapacity && run.delegation?.role === 'worker' && (!!this.workerWait(runId) || !!run.ciWait))))) {
|
|
3409
3765
|
return { ok: false, error: `cannot continue a ${run.status} run` };
|
|
3410
3766
|
}
|
|
3411
3767
|
const sessionStep = [...run.steps].reverse().find((s) => s.sessionId);
|
|
@@ -3532,6 +3888,7 @@ export class RunManager {
|
|
|
3532
3888
|
// bound UNATTENDED resumes.
|
|
3533
3889
|
this.clearAutoResume(runId);
|
|
3534
3890
|
if (!deferForCapacity) {
|
|
3891
|
+
this.withdrawCiWait(runId);
|
|
3535
3892
|
this.withdrawWorkerWait(runId);
|
|
3536
3893
|
this.resetParentCompletion(runId);
|
|
3537
3894
|
}
|
|
@@ -3542,7 +3899,7 @@ export class RunManager {
|
|
|
3542
3899
|
const continuationStep = { id: stepId, name: 'Continue', kind: 'agent', synthetic: 'continuation' };
|
|
3543
3900
|
if (run.delegation?.role !== 'worker')
|
|
3544
3901
|
this.store.addStep(runId, continuationStep);
|
|
3545
|
-
const recovered = deferForCapacity ? run.continuationMessage : undefined;
|
|
3902
|
+
const recovered = deferForCapacity && !conversation ? run.continuationMessage : undefined;
|
|
3546
3903
|
const message = recovered ?? this.toQueuedMessage(runId, [
|
|
3547
3904
|
...(opts.text?.trim() ? [{ type: 'text', text: opts.text.trim() }] : []),
|
|
3548
3905
|
...(opts.images ?? []),
|
|
@@ -3552,6 +3909,7 @@ export class RunManager {
|
|
|
3552
3909
|
// and the original prompt/URLs must reach disk before the first startup await.
|
|
3553
3910
|
const acceptedPatch = {
|
|
3554
3911
|
continuationMessage: { ...message, id: stepId, text: prompt,
|
|
3912
|
+
...(conversation ? { agentInputId: conversation.input.id } : {}),
|
|
3555
3913
|
origin: recovered ? recovered.origin : deferForCapacity ? 'lifecycle' : 'human' },
|
|
3556
3914
|
status: deferForCapacity ? 'queued' : 'running',
|
|
3557
3915
|
error: undefined,
|
|
@@ -3563,7 +3921,7 @@ export class RunManager {
|
|
|
3563
3921
|
};
|
|
3564
3922
|
if (run.delegation?.role === 'worker') {
|
|
3565
3923
|
try {
|
|
3566
|
-
this.store.commitWorkerContinuation(runId, acceptedPatch, continuationStep);
|
|
3924
|
+
this.store.commitWorkerContinuation(runId, acceptedPatch, continuationStep, conversation);
|
|
3567
3925
|
}
|
|
3568
3926
|
catch {
|
|
3569
3927
|
// No session or async launch exists yet. Release the private execution
|
|
@@ -3725,6 +4083,12 @@ export class RunManager {
|
|
|
3725
4083
|
// blocks before they reach the model). An image ALSO rides along as a base64 block so the
|
|
3726
4084
|
// model can view it; a file (#950) has nothing to view and travels as its path alone.
|
|
3727
4085
|
const checkpoint = record?.continuationMessage;
|
|
4086
|
+
state.openingAgentInputId = checkpoint?.agentInputId;
|
|
4087
|
+
const openingInput = record?.agentInputs?.find(input => input.id === checkpoint?.agentInputId);
|
|
4088
|
+
// Hydration has already included later human updates and task edits. Append
|
|
4089
|
+
// the current obligation outcome without replacing that composed prompt.
|
|
4090
|
+
if (openingInput)
|
|
4091
|
+
prompt += this.agentInputOutcomeText(openingInput);
|
|
3728
4092
|
const saved = this.readPersistedAttachments(runId, checkpoint?.id === stepId ? checkpoint.images ?? [] : [], 'continuation');
|
|
3729
4093
|
const openingImages = [...(images.length ? contentBlocksOf(images) : saved.blocks), ...persistedImages];
|
|
3730
4094
|
const attachments = [...saved.attachments, ...persistedAttachments];
|
|
@@ -3737,6 +4101,7 @@ export class RunManager {
|
|
|
3737
4101
|
...(attachments.length ? { images: attachments.map((saved) => saved.url) } : {}),
|
|
3738
4102
|
});
|
|
3739
4103
|
let openingAnswerAskSeq = checkpoint?.origin === 'human' ? this.pendingHumanAskSeq(runId) : undefined;
|
|
4104
|
+
state.openingAnswerAskSeq = openingAnswerAskSeq;
|
|
3740
4105
|
state.pendingHumanAsk = openingAnswerAskSeq === undefined && this.hasPendingHumanAsk(runId);
|
|
3741
4106
|
let stepCost = 0;
|
|
3742
4107
|
let turnText = '';
|
|
@@ -3787,13 +4152,40 @@ export class RunManager {
|
|
|
3787
4152
|
sawClaudeScheduleWakeup = true;
|
|
3788
4153
|
if (event.type === 'turn-end') {
|
|
3789
4154
|
state.workerWakeTurn = undefined;
|
|
4155
|
+
state.ciWakeTurn = undefined;
|
|
3790
4156
|
state.atTurnBoundary = state.session;
|
|
4157
|
+
state.ciTurnId = randomUUID();
|
|
3791
4158
|
// A completed turn acknowledges the opening payload. Until then a restart
|
|
3792
4159
|
// must replay it, including a crash after startSession but before delivery.
|
|
3793
4160
|
if (!state.cancelled) {
|
|
4161
|
+
if (state.openingAgentInputId && state.session?.open) {
|
|
4162
|
+
const id = state.openingAgentInputId;
|
|
4163
|
+
const deliveredAt = new Date().toISOString();
|
|
4164
|
+
try {
|
|
4165
|
+
this.store.commitAgentInputs(runId, (this.store.getRun(runId)?.agentInputs ?? []).map(input => input.id === id && !input.deliveredAt ? { ...input, deliveredAt } : input), id);
|
|
4166
|
+
}
|
|
4167
|
+
catch (error) {
|
|
4168
|
+
state.agentInputError = `agent input delivery checkpoint failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
4169
|
+
try {
|
|
4170
|
+
state.session.interrupt();
|
|
4171
|
+
}
|
|
4172
|
+
finally {
|
|
4173
|
+
try {
|
|
4174
|
+
this.store.appendEvent(runId, { type: 'error', message: state.agentInputError });
|
|
4175
|
+
}
|
|
4176
|
+
catch {
|
|
4177
|
+
console.error(`[cez] ${state.agentInputError}`);
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
state.openingAgentInputId = undefined;
|
|
4183
|
+
this.reconcileWorkerWaits();
|
|
4184
|
+
}
|
|
3794
4185
|
if (openingAnswerAskSeq !== undefined && state.session?.open) {
|
|
3795
4186
|
this.store.appendEvent(runId, { type: 'human-input-delivered', askSeq: openingAnswerAskSeq });
|
|
3796
4187
|
openingAnswerAskSeq = undefined;
|
|
4188
|
+
state.openingAnswerAskSeq = undefined;
|
|
3797
4189
|
}
|
|
3798
4190
|
this.store.updateRun(runId, { continuationMessage: undefined });
|
|
3799
4191
|
this.store.flush();
|
|
@@ -3824,8 +4216,9 @@ export class RunManager {
|
|
|
3824
4216
|
this.store.updateRun(runId, { invalidAsk: !ask && askNotes.length > 0 ? true : undefined });
|
|
3825
4217
|
if (ask)
|
|
3826
4218
|
this.prepareHumanAsk(runId, state);
|
|
3827
|
-
const
|
|
3828
|
-
const
|
|
4219
|
+
const ciWaitParked = !!sessionOpen && this.parkCiWait(runId, state);
|
|
4220
|
+
const completionBlocked = !ciWaitParked && !!done && this.deferParentCompletion(runId);
|
|
4221
|
+
const workerWaitParked = ciWaitParked || completionBlocked || (!!sessionOpen && this.parkWorkerWait(runId, state));
|
|
3829
4222
|
state.doneAtBoundary = done ? state.session : undefined;
|
|
3830
4223
|
state.parkAfterAck = sessionOpen && state.session ? { session: state.session, monitoring: !!monitoring } : undefined;
|
|
3831
4224
|
const agentInputDelivered = !ask && !workerWaitParked && this.flushAgentInputs(runId);
|
|
@@ -4030,6 +4423,7 @@ export class RunManager {
|
|
|
4030
4423
|
throw new Error('Worker stopped before launch');
|
|
4031
4424
|
}
|
|
4032
4425
|
const delegation = this.provisionSession(runId, state);
|
|
4426
|
+
const ciTools = await this.provisionCiSession(runId, state);
|
|
4033
4427
|
try {
|
|
4034
4428
|
session = runner.startSession({
|
|
4035
4429
|
// The Continue step is a fresh agent session on the same run — the
|
|
@@ -4044,7 +4438,8 @@ export class RunManager {
|
|
|
4044
4438
|
allowedTools: grants.allowedTools,
|
|
4045
4439
|
bashAllowlist: grants.bashAllowlist,
|
|
4046
4440
|
additionalDirectories: agentDirectories(join(this.dataDir, 'runs'), continueProfile.env),
|
|
4047
|
-
env: { ...continueProfile.env, ...delegation?.env },
|
|
4441
|
+
env: { ...continueProfile.env, ...delegation?.env, ...ciTools?.env },
|
|
4442
|
+
...(ciTools ? { cezarTools: ciTools.descriptor } : {}),
|
|
4048
4443
|
...(delegation ? { restrictNativeDelegation: delegation.restrictNativeDelegation } : {}),
|
|
4049
4444
|
model: continueModel,
|
|
4050
4445
|
effort: continueEffort,
|
|
@@ -4060,6 +4455,8 @@ export class RunManager {
|
|
|
4060
4455
|
});
|
|
4061
4456
|
}
|
|
4062
4457
|
catch (error) {
|
|
4458
|
+
ciTools?.revoke();
|
|
4459
|
+
state.revokeCiTools = undefined;
|
|
4063
4460
|
delegation?.revoke();
|
|
4064
4461
|
state.revokeDelegation = undefined;
|
|
4065
4462
|
throw error;
|
|
@@ -4080,6 +4477,10 @@ export class RunManager {
|
|
|
4080
4477
|
if (this.preserveRunAfterDisposal(runId, state))
|
|
4081
4478
|
return;
|
|
4082
4479
|
this.persistWorkerAssistantResult(runId, stepId, result.text);
|
|
4480
|
+
if (this.store.getRun(runId)?.ciWait && !state.cancelled) {
|
|
4481
|
+
this.store.updateRun(runId, { status: 'running', activity: 'monitoring' });
|
|
4482
|
+
return;
|
|
4483
|
+
}
|
|
4083
4484
|
if (sessionError || state.agentInputError)
|
|
4084
4485
|
throw new Error(sessionError ?? state.agentInputError);
|
|
4085
4486
|
if (!state.cancelled) {
|
|
@@ -4149,6 +4550,10 @@ export class RunManager {
|
|
|
4149
4550
|
if (this.preserveRunAfterDisposal(runId, state))
|
|
4150
4551
|
return;
|
|
4151
4552
|
// Keep the provider failure that triggered teardown, even if teardown rejects.
|
|
4553
|
+
if (this.store.getRun(runId)?.ciWait && !state.cancelled) {
|
|
4554
|
+
this.store.updateRun(runId, { status: 'running', activity: 'monitoring' });
|
|
4555
|
+
return;
|
|
4556
|
+
}
|
|
4152
4557
|
const message = sessionError ?? state.agentInputError ?? (err instanceof Error ? err.message : String(err));
|
|
4153
4558
|
sink.sessionEnded('error', message);
|
|
4154
4559
|
this.store.updateStep(runId, stepId, { status: 'failed', error: message, finishedAt: finishedAt() });
|
|
@@ -4162,6 +4567,8 @@ export class RunManager {
|
|
|
4162
4567
|
this.store.appendEvent(runId, { type: 'lifecycle', message: `continue failed — ${message}` });
|
|
4163
4568
|
}
|
|
4164
4569
|
finally {
|
|
4570
|
+
ciTools?.revoke();
|
|
4571
|
+
state.revokeCiTools = undefined;
|
|
4165
4572
|
delegation?.revoke();
|
|
4166
4573
|
state.revokeDelegation = undefined;
|
|
4167
4574
|
this.recordUsagePeaks(runId);
|
|
@@ -4463,6 +4870,11 @@ export class RunManager {
|
|
|
4463
4870
|
this.requeueWhileHeld(runId, workflow, input, taskBackend, state, i);
|
|
4464
4871
|
return;
|
|
4465
4872
|
}
|
|
4873
|
+
if (this.store.getRun(runId)?.ciWait && !state.cancelled) {
|
|
4874
|
+
this.store.updateRun(runId, { status: 'running', activity: 'monitoring' });
|
|
4875
|
+
this.dropActive(runId);
|
|
4876
|
+
return;
|
|
4877
|
+
}
|
|
4466
4878
|
if (failure) {
|
|
4467
4879
|
this.finishStep(runId, step.id, 'failed', failure, emit);
|
|
4468
4880
|
runError = `step "${step.id}" failed: ${failure}`;
|
|
@@ -4671,7 +5083,9 @@ export class RunManager {
|
|
|
4671
5083
|
sawClaudeScheduleWakeup = true;
|
|
4672
5084
|
if (event.type === 'turn-end') {
|
|
4673
5085
|
state.workerWakeTurn = undefined;
|
|
5086
|
+
state.ciWakeTurn = undefined;
|
|
4674
5087
|
state.atTurnBoundary = state.session;
|
|
5088
|
+
state.ciTurnId = randomUUID();
|
|
4675
5089
|
// v2 `turn.completed` already flushed the coalescers; the v1 turn
|
|
4676
5090
|
// boundary flushes again (idempotent) as a backstop.
|
|
4677
5091
|
sink.flushAll();
|
|
@@ -4680,7 +5094,7 @@ export class RunManager {
|
|
|
4680
5094
|
const done = interactive && sessionOpen && DONE_MARKER_RE.test(turnText.trimEnd());
|
|
4681
5095
|
// `CEZ:ASK` → the user is blocked; wins over `CEZ:MONITORING`, loses to
|
|
4682
5096
|
// `CEZ:DONE` (#473).
|
|
4683
|
-
const askTurn = resolveAskTurn(turnText, completedAssistantText, Boolean((interactive || this.workerWait(runId)) && sessionOpen) && !done);
|
|
5097
|
+
const askTurn = resolveAskTurn(turnText, completedAssistantText, Boolean((interactive || this.workerWait(runId) || this.store.getRun(runId)?.ciWait) && sessionOpen) && !done);
|
|
4684
5098
|
const { ask, notes: askNotes } = askTurn;
|
|
4685
5099
|
discardQueuedMessagesOnAsk(state.session, askTurn);
|
|
4686
5100
|
const monitoring = !this.parentCompletionAttention(runId) &&
|
|
@@ -4698,8 +5112,9 @@ export class RunManager {
|
|
|
4698
5112
|
this.store.updateRun(runId, { invalidAsk: !ask && askNotes.length > 0 ? true : undefined });
|
|
4699
5113
|
if (ask)
|
|
4700
5114
|
this.prepareHumanAsk(runId, state);
|
|
4701
|
-
const
|
|
4702
|
-
const
|
|
5115
|
+
const ciWaitParked = !!sessionOpen && this.parkCiWait(runId, state);
|
|
5116
|
+
const completionBlocked = !ciWaitParked && !!done && this.deferParentCompletion(runId);
|
|
5117
|
+
const workerWaitParked = ciWaitParked || completionBlocked || (!!sessionOpen && this.parkWorkerWait(runId, state));
|
|
4703
5118
|
state.doneAtBoundary = done ? state.session : undefined;
|
|
4704
5119
|
state.parkAfterAck = interactive && sessionOpen && state.session ? { session: state.session, monitoring: !!monitoring } : undefined;
|
|
4705
5120
|
const agentInputDelivered = !ask && !workerWaitParked && this.flushAgentInputs(runId);
|
|
@@ -4820,6 +5235,7 @@ export class RunManager {
|
|
|
4820
5235
|
state.currentStepId = step.id;
|
|
4821
5236
|
this.beginUsageInvocation(runId, state, step.id);
|
|
4822
5237
|
const delegation = this.provisionSession(runId, state);
|
|
5238
|
+
const ciTools = await this.provisionCiSession(runId, state);
|
|
4823
5239
|
try {
|
|
4824
5240
|
if (this.workerExecutionStopped(runId)) {
|
|
4825
5241
|
state.cancelled = true;
|
|
@@ -4838,7 +5254,8 @@ export class RunManager {
|
|
|
4838
5254
|
bashAllowlist: grants.bashAllowlist,
|
|
4839
5255
|
// The handoff file lives outside the worktree — grant access.
|
|
4840
5256
|
additionalDirectories: agentDirectories(join(this.dataDir, 'runs'), stepProfile.env),
|
|
4841
|
-
env: { ...stepProfile.env, ...delegation?.env },
|
|
5257
|
+
env: { ...stepProfile.env, ...delegation?.env, ...ciTools?.env },
|
|
5258
|
+
...(ciTools ? { cezarTools: ciTools.descriptor } : {}),
|
|
4842
5259
|
...(delegation ? { restrictNativeDelegation: delegation.restrictNativeDelegation } : {}),
|
|
4843
5260
|
model: backendModel,
|
|
4844
5261
|
effort: effectiveEffort,
|
|
@@ -4848,7 +5265,7 @@ export class RunManager {
|
|
|
4848
5265
|
}, onEvent, {
|
|
4849
5266
|
autoEndAfterFirstTurn: !interactive,
|
|
4850
5267
|
shouldAutoEnd: () => this.active.get(runId) !== state || state.session !== session ||
|
|
4851
|
-
(!this.workerWait(runId) && !this.parentCompletionPending(runId) && state.workerWakeTurn !== session && !state.agentInputFlight),
|
|
5268
|
+
(!this.workerWait(runId) && !this.store.getRun(runId)?.ciWait && !this.parentCompletionPending(runId) && state.workerWakeTurn !== session && state.ciWakeTurn !== session && !state.agentInputFlight),
|
|
4852
5269
|
onUiEvent: (event) => {
|
|
4853
5270
|
completedAssistantText = appendCompletedAssistantText(completedAssistantText, event);
|
|
4854
5271
|
this.handleRunnerUiEvent(runId, state, sink, event);
|
|
@@ -4857,6 +5274,8 @@ export class RunManager {
|
|
|
4857
5274
|
});
|
|
4858
5275
|
}
|
|
4859
5276
|
catch (err) {
|
|
5277
|
+
ciTools?.revoke();
|
|
5278
|
+
state.revokeCiTools = undefined;
|
|
4860
5279
|
delegation?.revoke();
|
|
4861
5280
|
state.revokeDelegation = undefined;
|
|
4862
5281
|
state.currentStepId = undefined;
|
|
@@ -4906,6 +5325,8 @@ export class RunManager {
|
|
|
4906
5325
|
return message;
|
|
4907
5326
|
}
|
|
4908
5327
|
finally {
|
|
5328
|
+
ciTools?.revoke();
|
|
5329
|
+
state.revokeCiTools = undefined;
|
|
4909
5330
|
delegation?.revoke();
|
|
4910
5331
|
state.revokeDelegation = undefined;
|
|
4911
5332
|
this.recordUsagePeaks(runId);
|
|
@@ -4921,6 +5342,7 @@ export class RunManager {
|
|
|
4921
5342
|
this.queue.splice(at, 1);
|
|
4922
5343
|
}
|
|
4923
5344
|
state.workerWakeTurn = undefined;
|
|
5345
|
+
state.ciWakeTurn = undefined;
|
|
4924
5346
|
state.atTurnBoundary = undefined;
|
|
4925
5347
|
state.doneAtBoundary = undefined;
|
|
4926
5348
|
state.parkAfterAck = undefined;
|
|
@@ -4962,6 +5384,7 @@ export class RunManager {
|
|
|
4962
5384
|
/** Ask precedence also releases an admitted but not yet delivered worker wake.
|
|
4963
5385
|
* Its durable input/wait remain queued until an actual human answer. */
|
|
4964
5386
|
prepareHumanAsk(runId, state) {
|
|
5387
|
+
this.withdrawCiWait(runId);
|
|
4965
5388
|
state.pendingHumanAsk = true;
|
|
4966
5389
|
this.workerWaiting.delete(runId);
|
|
4967
5390
|
this.workerWakeAdmitted.delete(runId);
|
|
@@ -5426,6 +5849,10 @@ export class RunManager {
|
|
|
5426
5849
|
const run = this.store.getRun(runId);
|
|
5427
5850
|
if (!run || run.stopping || this.workerExecutionStopped(runId) || this.executionBlockedByRootFinish(run))
|
|
5428
5851
|
return;
|
|
5852
|
+
if (run.ciWait) {
|
|
5853
|
+
this.clearMonitoringWakeTimer(state, runId);
|
|
5854
|
+
return;
|
|
5855
|
+
}
|
|
5429
5856
|
const minutes = this.semaphore.monitoringWakeIntervalMinutes();
|
|
5430
5857
|
if (minutes === null) {
|
|
5431
5858
|
this.clearMonitoringWakeTimer(state, runId);
|