@qodeca/xezar 0.11.2 → 0.13.0
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 +69 -18
- package/dist/agent-config/catalog.d.ts +10 -0
- package/dist/agent-config/catalog.js.map +1 -1
- package/dist/automations/coordinator.js +8 -0
- package/dist/automations/coordinator.js.map +1 -1
- package/dist/automations/types.d.ts +2 -0
- package/dist/contract/automations.d.ts +8 -0
- package/dist/contract/events.test.d.ts +1 -0
- package/dist/contract/index.js +87 -3
- package/dist/contract/runs.d.ts +25 -0
- package/dist/contract/runs.test.d.ts +1 -0
- package/dist/contract/skills.d.ts +1 -0
- package/dist/contract/workflows.d.ts +6 -0
- package/dist/contract/workspace.d.ts +37 -4
- package/dist/core/agent-env.js +8 -1
- package/dist/core/agent-env.js.map +1 -1
- package/dist/core/agent-runner.d.ts +13 -0
- package/dist/core/agent-runner.js +22 -0
- package/dist/core/agent-runner.js.map +1 -1
- package/dist/core/backend-detect.js +11 -1
- package/dist/core/backend-detect.js.map +1 -1
- package/dist/core/claude-cli-runner.js +8 -2
- package/dist/core/claude-cli-runner.js.map +1 -1
- package/dist/core/codex-app-server-runner.js +6 -2
- package/dist/core/codex-app-server-runner.js.map +1 -1
- package/dist/core/opencode-server-runner.d.ts +12 -0
- package/dist/core/opencode-server-runner.js +326 -17
- package/dist/core/opencode-server-runner.js.map +1 -1
- package/dist/core/pi-model-catalog.d.ts +52 -0
- package/dist/core/pi-model-catalog.js +135 -0
- package/dist/core/pi-model-catalog.js.map +1 -0
- package/dist/core/pi-runner.d.ts +3 -0
- package/dist/core/pi-runner.js +139 -12
- package/dist/core/pi-runner.js.map +1 -1
- package/dist/core/pi-ui-mapper.js +39 -3
- package/dist/core/pi-ui-mapper.js.map +1 -1
- package/dist/core/runner-model-catalog.js +11 -2
- package/dist/core/runner-model-catalog.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +8 -0
- package/dist/paths.js.map +1 -1
- package/dist/runs/event-corrections.d.ts +12 -0
- package/dist/runs/event-corrections.js +141 -0
- package/dist/runs/event-corrections.js.map +1 -0
- package/dist/runs/event-history.d.ts +5 -1
- package/dist/runs/event-history.js +66 -5
- package/dist/runs/event-history.js.map +1 -1
- package/dist/runs/project-writer.d.ts +11 -0
- package/dist/runs/project-writer.js +115 -0
- package/dist/runs/project-writer.js.map +1 -0
- package/dist/runs/store.d.ts +2 -0
- package/dist/runs/store.js +13 -2
- package/dist/runs/store.js.map +1 -1
- package/dist/runs/task-markers.d.ts +4 -0
- package/dist/runs/task-markers.js +63 -5
- package/dist/runs/task-markers.js.map +1 -1
- package/dist/server/capabilities.d.ts +7 -5
- package/dist/server/capabilities.js +11 -7
- package/dist/server/capabilities.js.map +1 -1
- package/dist/server/project-context.js +2 -0
- package/dist/server/project-context.js.map +1 -1
- package/dist/server/server.d.ts +199 -47
- package/dist/server/server.js +195 -11
- package/dist/server/server.js.map +1 -1
- package/dist/workflows/run.d.ts +67 -2
- package/dist/workflows/run.js +190 -44
- package/dist/workflows/run.js.map +1 -1
- package/dist/workflows/types.d.ts +41 -0
- package/dist/workflows/types.js +72 -0
- package/dist/workflows/types.js.map +1 -1
- package/dist/workspace/config.d.ts +42 -0
- package/dist/workspace/config.js +132 -3
- package/dist/workspace/config.js.map +1 -1
- package/dist/workspace/semaphore.d.ts +63 -3
- package/dist/workspace/semaphore.js +76 -3
- package/dist/workspace/semaphore.js.map +1 -1
- package/package.json +3 -3
- package/web/dist/assets/{alert-dialog-CGskMvgc.js → alert-dialog-BVsVcbFy.js} +1 -1
- package/web/dist/assets/arrow-down-CYgAlZtW.js +1 -0
- package/web/dist/assets/arrow-left-BOqE0a3k.js +1 -0
- package/web/dist/assets/{centered-state-B_V_Ai2h.js → centered-state-Djgly6Ni.js} +1 -1
- package/web/dist/assets/chevron-right-Cc9BWnbN.js +1 -0
- package/web/dist/assets/{collapsible-B_8uYwHb.js → collapsible-DZ4D-DHG.js} +1 -1
- package/web/dist/assets/{commit-list-C4Dv6vUL.js → commit-list-B-z1JL9G.js} +1 -1
- package/web/dist/assets/{compare-variants-et0mplGi.js → compare-variants-CdS8r8E2.js} +1 -1
- package/web/dist/assets/{diff-CbozNUJq.js → diff-BKew9rQX.js} +2 -2
- package/web/dist/assets/{diff-stat-btAmKFJr.js → diff-stat-D0mc5V6Z.js} +1 -1
- package/web/dist/assets/{diff-view-BpFTcG3R.js → diff-view-xpIcuLIC.js} +1 -1
- package/web/dist/assets/{dropdown-menu-Z80KMInL.js → dropdown-menu-CEQziH7X.js} +1 -1
- package/web/dist/assets/editable-title-BCX3T2BY.js +1 -0
- package/web/dist/assets/{ellipsis-vertical-Bmi2u2TG.js → ellipsis-vertical-ByVdDc4z.js} +1 -1
- package/web/dist/assets/{file-DKoYWRsv.js → file-xBSB5hXO.js} +1 -1
- package/web/dist/assets/{folder-u01HScRT.js → folder-CldgbZ5A.js} +1 -1
- package/web/dist/assets/{git-toolbar-Q2kk2F7v.js → git-toolbar-BYlnf4qf.js} +1 -1
- package/web/dist/assets/github-CqGK62Lw.js +1 -0
- package/web/dist/assets/{image-preview-BR3iIIhf.js → image-preview-Cv5mZr0n.js} +1 -1
- package/web/dist/assets/index-3nxVmdn5.css +2 -0
- package/web/dist/assets/index-qX7n11FX.js +9 -0
- package/web/dist/assets/{markdown-5FuCZDA4.js → markdown-CgEEE2ND.js} +1 -1
- package/web/dist/assets/{new-task-form-BydHBbef.js → new-task-form-1r2bgsdD.js} +1 -1
- package/web/dist/assets/{pill-vNzaaYxj.js → pill-q-x22_ej.js} +1 -1
- package/web/dist/assets/{project-router-BbCzrHKs.js → project-router-VDN4jQM8.js} +1 -1
- package/web/dist/assets/{prompt-templates-B7Fs2b1S.js → prompt-templates-BmZtCe-L.js} +1 -1
- package/web/dist/assets/{refresh-cw-DGliez3E.js → refresh-cw-DcNCj2nu.js} +1 -1
- package/web/dist/assets/{repo-git-h1RFfly3.js → repo-git-C2On8SlI.js} +1 -1
- package/web/dist/assets/{run-diff-Bq9vkSNK.js → run-diff-vguEMJzh.js} +1 -1
- package/web/dist/assets/{run-header-Cd8mSKBG.js → run-header-DM7MiKj9.js} +1 -1
- package/web/dist/assets/{search-x-69n_672Y.js → search-x-QmhaAbg5.js} +1 -1
- package/web/dist/assets/{skill-empty-hint-CL0g7WBL.js → skill-empty-hint-BrzUwByZ.js} +1 -1
- package/web/dist/assets/{skills-BqAzyGS3.js → skills-Bj-uG_Hz.js} +1 -1
- package/web/dist/assets/{sparkles-B8agSxBZ.js → sparkles-CP8QDOgX.js} +1 -1
- package/web/dist/assets/{square-terminal-Cf4Og6ct.js → square-terminal-CGypshSD.js} +1 -1
- package/web/dist/assets/{tab-link-Cp2-CJS8.js → tab-link-D4lFzzI1.js} +1 -1
- package/web/dist/assets/{task-changes-DwXPFXqG.js → task-changes-6aopqAmV.js} +1 -1
- package/web/dist/assets/{task-commits-Bessua-c.js → task-commits-u-dwaCyw.js} +1 -1
- package/web/dist/assets/{task-files-DxFIhRPP.js → task-files-IMOZnIUK.js} +1 -1
- package/web/dist/assets/task-thread-CbMPmrWq.js +9 -0
- package/web/dist/assets/{textarea--cpYshee.js → textarea-CyBCyKIk.js} +1 -1
- package/web/dist/assets/{thread-loading-ZhJref0J.js → thread-loading-Cuq2jchC.js} +1 -1
- package/web/dist/assets/{trash-2--ZtrzUx3.js → trash-2-B6Dp3ZNs.js} +1 -1
- package/web/dist/assets/{triangle-alert-D5tJwRD2.js → triangle-alert-xBya8FUp.js} +1 -1
- package/web/dist/assets/{upload-SnwVxKOA.js → upload-D6MXEB9p.js} +1 -1
- package/web/dist/assets/{use-desktop-Sr1QlxhT.js → use-desktop-fLDKEJoB.js} +1 -1
- package/web/dist/assets/{use-submit-shortcut-JY8M_MPL.js → use-submit-shortcut-JTpNzU-u.js} +1 -1
- package/web/dist/assets/{utils-Bb8ohcr3.js → utils--z9hRtl3.js} +1 -1
- package/web/dist/assets/{workflows-9HC240Wx.js → workflows-BY3KTkFw.js} +3 -3
- package/web/dist/assets/{zoomable-image-0G3IBXWC.js → zoomable-image-GTYwLtkS.js} +1 -1
- package/web/dist/index.html +23 -23
- package/web/dist/assets/arrow-down-G8adPhbu.js +0 -1
- package/web/dist/assets/arrow-left-BJsIW6A0.js +0 -1
- package/web/dist/assets/chevron-right-CFzn-von.js +0 -1
- package/web/dist/assets/editable-title-wS5NWwob.js +0 -1
- package/web/dist/assets/github-DjEYCxwt.js +0 -1
- package/web/dist/assets/index-BcI5hug3.css +0 -2
- package/web/dist/assets/index-Cz4GSAqV.js +0 -7
- package/web/dist/assets/task-thread-Dhkupf1-.js +0 -9
package/dist/workflows/run.d.ts
CHANGED
|
@@ -4,7 +4,15 @@ import { type Skill } from '../skills.ts';
|
|
|
4
4
|
import type { QueuedMessage, RunRecord, RunStore } from '../runs/store.ts';
|
|
5
5
|
import { WorkspaceSemaphore, type AccountHolds } from '../workspace/semaphore.ts';
|
|
6
6
|
import { type WorkflowDef } from './types.ts';
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* An interactive session that hears nothing from the user closes itself.
|
|
9
|
+
*
|
|
10
|
+
* Kept as the module default so `stepTimeoutMs`-style callers and old imports still
|
|
11
|
+
* resolve, but enforcement no longer reads it directly: `armIdleTimer` asks the shared
|
|
12
|
+
* `WorkspaceSemaphore`, so `resources.idleTimeoutMinutes` takes effect on the next park
|
|
13
|
+
* without a restart. This constant IS that setting's default, via
|
|
14
|
+
* `DEFAULT_IDLE_TIMEOUT_MINUTES`.
|
|
15
|
+
*/
|
|
8
16
|
export declare const IDLE_TIMEOUT_MS: number;
|
|
9
17
|
/**
|
|
10
18
|
* Preserve boundaries between complete assistant text blocks while a turn is
|
|
@@ -28,6 +36,9 @@ export declare function periodicAutosaveEnabled(env?: NodeJS.ProcessEnv): boolea
|
|
|
28
36
|
* files or Git state. Isolated worktree runs are unaffected.
|
|
29
37
|
*/
|
|
30
38
|
export declare function repositoryRootLockDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
39
|
+
/** Safety cap on autonomous auto-continues per run — stops a stuck agent from nudging forever.
|
|
40
|
+
* Exported so the regression suite pins the bound against the real number instead of a copy. */
|
|
41
|
+
export declare const MAX_AUTO_CONTINUES = 40;
|
|
31
42
|
/**
|
|
32
43
|
* Auto-resume after a provider usage limit (spec 2026-08-03-auto-resume-after-usage-limit).
|
|
33
44
|
*
|
|
@@ -254,6 +265,15 @@ export declare class RunManager {
|
|
|
254
265
|
private readonly offUsage;
|
|
255
266
|
/** The stalled-queue watchdog (see `rescueStalledQueue`). */
|
|
256
267
|
private readonly queueWatchdog;
|
|
268
|
+
/** The rescue sweeps that have STARTED and not settled yet (#125). `clearInterval` cancels the
|
|
269
|
+
* next tick; it cannot cancel the tick already running, and a sweep is async — it awaits
|
|
270
|
+
* `reviveWorkflow` before appending NDJSON. dispose() settles these so a torn-down manager is
|
|
271
|
+
* provably finished writing before its caller removes the data root under it. */
|
|
272
|
+
private readonly rescuesInFlight;
|
|
273
|
+
/** Set by dispose(): this manager makes no further moves. Re-checked at every await boundary a
|
|
274
|
+
* rescue crosses, because a sweep that was already past its first check when dispose() landed
|
|
275
|
+
* would otherwise write an event — and re-populate the queue dispose() had just emptied. */
|
|
276
|
+
private disposed;
|
|
257
277
|
/** Set by the watchdog for exactly one sweep: ignore the usage-limit hold and make progress. */
|
|
258
278
|
private forceNextPump;
|
|
259
279
|
/** Runs the watchdog started despite the hold. The spawn-time gate (`requeueWhileHeld`) would
|
|
@@ -278,8 +298,19 @@ export declare class RunManager {
|
|
|
278
298
|
* repo-root locks, and empties the queued state so nothing fires later.
|
|
279
299
|
* Live sessions are NOT ended here: run lifecycle stays the caller's policy;
|
|
280
300
|
* dispose only guarantees the manager makes no further moves on its own.
|
|
301
|
+
*
|
|
302
|
+
* Every side effect below is SYNCHRONOUS, so a caller that ignores the return value behaves
|
|
303
|
+
* exactly as it did before #125. The returned promise settles the one thing dispose cannot do
|
|
304
|
+
* synchronously: a queue-watchdog rescue that had already started. `clearInterval` stops the
|
|
305
|
+
* next tick, never the running one — the same discipline `AGENTS.md` records for the e2e
|
|
306
|
+
* fixture servers, where `kill()` only delivers the signal and the helper awaits the exit.
|
|
307
|
+
* Await it whenever the data root is about to be removed; a test that deletes its temp
|
|
308
|
+
* directory without awaiting is the ENOENT in #125.
|
|
309
|
+
*
|
|
310
|
+
* It never rejects: a sweep that fails still belongs to whoever started it (the watchdog floats
|
|
311
|
+
* it exactly as before), and teardown must not become a second place that error surfaces.
|
|
281
312
|
*/
|
|
282
|
-
dispose(): void
|
|
313
|
+
dispose(): Promise<void>;
|
|
283
314
|
/**
|
|
284
315
|
* Pause any active run whose whole process tree exceeds the WORKSPACE
|
|
285
316
|
* `resources.memoryLimitMb`, freeing its slot so the queue advances
|
|
@@ -384,6 +415,11 @@ export declare class RunManager {
|
|
|
384
415
|
* `continue-N` step and the session before it are durable, which is enough. Otherwise the
|
|
385
416
|
* workflow is revived from the record. A run that can be neither is failed loudly rather than
|
|
386
417
|
* left in the queue as a ghost.
|
|
418
|
+
*
|
|
419
|
+
* Both effects here — an NDJSON append and a queue push — must not outlive dispose(), so the
|
|
420
|
+
* flag is re-read after `reviveWorkflow`, the one await this method crosses (#125). Bailing
|
|
421
|
+
* leaves the record `queued`, which is exactly what it was: re-adoption is what boot recovery
|
|
422
|
+
* does for the next owner of this data directory.
|
|
387
423
|
*/
|
|
388
424
|
private reviveQueuedRun;
|
|
389
425
|
/**
|
|
@@ -466,8 +502,14 @@ export declare class RunManager {
|
|
|
466
502
|
* honest hold, with a real deadline behind it this time.
|
|
467
503
|
*
|
|
468
504
|
* Public so a test can drive the wedge directly instead of waiting out the interval.
|
|
505
|
+
*
|
|
506
|
+
* The sweep itself is `sweepStalledQueue`; this wrapper only publishes it to `rescuesInFlight`
|
|
507
|
+
* so dispose() can settle it (#125). Every entry point goes through here — the interval and the
|
|
508
|
+
* tests alike — so nothing can start a sweep that teardown cannot see. The schedule, the
|
|
509
|
+
* conditions and the effects are untouched.
|
|
469
510
|
*/
|
|
470
511
|
rescueStalledQueue(now?: number): Promise<void>;
|
|
512
|
+
private sweepStalledQueue;
|
|
471
513
|
/**
|
|
472
514
|
* The accounts this project is currently holding: one key per run parked on a usage-limit
|
|
473
515
|
* resume that has not come due yet (spec 2026-08-03-auto-resume-after-usage-limit).
|
|
@@ -639,6 +681,21 @@ export declare class RunManager {
|
|
|
639
681
|
error?: string;
|
|
640
682
|
};
|
|
641
683
|
private runContinuation;
|
|
684
|
+
/**
|
|
685
|
+
* Autonomous turn end (#489): never hand the ball back to the user — nudge the agent to keep
|
|
686
|
+
* going and report whether the ball stayed with it. `false` means "park normally", which is
|
|
687
|
+
* every non-autonomous run, a run whose pause cleared `autonomous` (the memory-limit
|
|
688
|
+
* suppression above), a cancelled one, a closed session that cannot take the message, and a
|
|
689
|
+
* run that has spent its `MAX_AUTO_CONTINUES` budget.
|
|
690
|
+
*
|
|
691
|
+
* ONE helper, called from BOTH turn-end handlers, because the split is exactly what broke:
|
|
692
|
+
* `execute` set `autonomous` on its `ActiveRun` and its handler never read it, while
|
|
693
|
+
* `runContinuation`'s handler read it and its `ActiveRun` never set it — so the nudge fired
|
|
694
|
+
* nowhere and every autonomous run parked after one turn (#141). AGENTS.md names both traps:
|
|
695
|
+
* grep the TYPE at every construction site, and treat the two near-identical turn-end handlers
|
|
696
|
+
* as one place. Keep this the only sender of `AUTONOMOUS_NUDGE`.
|
|
697
|
+
*/
|
|
698
|
+
private autoContinueTurn;
|
|
642
699
|
private execute;
|
|
643
700
|
/** Returns an error message, or null on success. */
|
|
644
701
|
private runAgentStep;
|
|
@@ -747,6 +804,14 @@ export declare class RunManager {
|
|
|
747
804
|
* shows the tool result's `[screenshot]` placeholder (or the image count).
|
|
748
805
|
*/
|
|
749
806
|
private persistAttachment;
|
|
807
|
+
/**
|
|
808
|
+
* Arm the idle wall clock for a session that just parked at `waiting`.
|
|
809
|
+
*
|
|
810
|
+
* The duration comes from the shared semaphore's cached `resources.idleTimeoutMinutes`,
|
|
811
|
+
* so a change made in Settings applies to the next park with no restart. `null` means
|
|
812
|
+
* the operator chose "never close on idle": no timer is armed, and the only exits left
|
|
813
|
+
* are a user message and Cancel — both of which a `waiting` run already advertises.
|
|
814
|
+
*/
|
|
750
815
|
private armIdleTimer;
|
|
751
816
|
private clearIdleTimer;
|
|
752
817
|
private reconcileMonitoringWakeTimers;
|
package/dist/workflows/run.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createRunner } from '../core/runner-factory.js';
|
|
|
9
9
|
import { modelConflictsWithRunner } from '../core/model-presets.js';
|
|
10
10
|
import { AGENT_MODELS_LOCKED_ERROR, agentModelsLocked } from '../core/agent-model-policy.js';
|
|
11
11
|
import { ModelIdentityError, formatModelIdentity, normalizeModelForBackend, } from '../core/model-identity.js';
|
|
12
|
-
import { HANDOFF_ONLY_INSTRUCTIONS, HANDOFF_INSTRUCTIONS, appendHandoffHeartbeat,
|
|
12
|
+
import { HANDOFF_ONLY_INSTRUCTIONS, HANDOFF_INSTRUCTIONS, appendHandoffHeartbeat, handoffPath, seedHandoffFile, } from '../handoff.js';
|
|
13
13
|
import { todosPath } from '../todos.js';
|
|
14
14
|
// Contract VALUES, like `workspaceUiStateSchema` in workspace/migrations.ts: the attachment
|
|
15
15
|
// vocabulary the routes validate with is the same one the engine stores and re-reads by, so the
|
|
@@ -31,15 +31,24 @@ import { autoNamingActive, generateRunName, liveTitleUpdatesEnabled, postValidat
|
|
|
31
31
|
import { reviewGateEnabled } from '../runs/review-gate.js';
|
|
32
32
|
import { resolveProfileEnvForRoot } from '../workspace/agent-profiles.js';
|
|
33
33
|
import { DEFAULT_AGENT_ACCOUNT_ID } from '../workspace/agent-accounts.js';
|
|
34
|
+
import { DEFAULT_IDLE_TIMEOUT_MINUTES } from '../workspace/config.js';
|
|
34
35
|
import { WorkspaceSemaphore } from '../workspace/semaphore.js';
|
|
35
36
|
import { UiEventSink } from '../runs/ui-event-sink.js';
|
|
36
|
-
import { chainStepNote, DEFAULT_ALLOWED_TOOLS, stepKind } from './types.js';
|
|
37
|
+
import { chainStepNote, DEFAULT_ALLOWED_TOOLS, stepKind, stepTimeoutMs, } from './types.js';
|
|
37
38
|
const CHECK_OUTPUT_CAP = 20_000;
|
|
38
39
|
async function configuredModelProvider(backend, repoRoot) {
|
|
39
40
|
return readAgentModelProvider(backend, repoRoot).catch(() => undefined);
|
|
40
41
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
42
|
+
/**
|
|
43
|
+
* An interactive session that hears nothing from the user closes itself.
|
|
44
|
+
*
|
|
45
|
+
* Kept as the module default so `stepTimeoutMs`-style callers and old imports still
|
|
46
|
+
* resolve, but enforcement no longer reads it directly: `armIdleTimer` asks the shared
|
|
47
|
+
* `WorkspaceSemaphore`, so `resources.idleTimeoutMinutes` takes effect on the next park
|
|
48
|
+
* without a restart. This constant IS that setting's default, via
|
|
49
|
+
* `DEFAULT_IDLE_TIMEOUT_MINUTES`.
|
|
50
|
+
*/
|
|
51
|
+
export const IDLE_TIMEOUT_MS = DEFAULT_IDLE_TIMEOUT_MINUTES * 60_000;
|
|
43
52
|
/**
|
|
44
53
|
* Task-completion marker from the agent contract (HANDOFF_INSTRUCTIONS): a
|
|
45
54
|
* turn whose text ends with `XEZ:DONE` means "goal achieved, nothing to ask" —
|
|
@@ -157,8 +166,9 @@ export function repositoryRootLockDisabled(env = process.env) {
|
|
|
157
166
|
return env.XEZ_DISABLE_REPO_LOCK === '1';
|
|
158
167
|
}
|
|
159
168
|
const REPOSITORY_ROOT_LOCK_DISABLED_NOTE = 'repository-root lock disabled by XEZ_DISABLE_REPO_LOCK=1 (shared checkout is unsafe)';
|
|
160
|
-
/** Safety cap on autonomous auto-continues per run — stops a stuck agent from nudging forever.
|
|
161
|
-
|
|
169
|
+
/** Safety cap on autonomous auto-continues per run — stops a stuck agent from nudging forever.
|
|
170
|
+
* Exported so the regression suite pins the bound against the real number instead of a copy. */
|
|
171
|
+
export const MAX_AUTO_CONTINUES = 40;
|
|
162
172
|
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.';
|
|
163
173
|
const MONITORING_WAKE_NUDGE = 'Re-check the downstream work you were monitoring. Continue toward the task goal; emit XEZ:MONITORING again only if it is still pending.';
|
|
164
174
|
/**
|
|
@@ -428,6 +438,15 @@ export class RunManager {
|
|
|
428
438
|
offUsage;
|
|
429
439
|
/** The stalled-queue watchdog (see `rescueStalledQueue`). */
|
|
430
440
|
queueWatchdog;
|
|
441
|
+
/** The rescue sweeps that have STARTED and not settled yet (#125). `clearInterval` cancels the
|
|
442
|
+
* next tick; it cannot cancel the tick already running, and a sweep is async — it awaits
|
|
443
|
+
* `reviveWorkflow` before appending NDJSON. dispose() settles these so a torn-down manager is
|
|
444
|
+
* provably finished writing before its caller removes the data root under it. */
|
|
445
|
+
rescuesInFlight = new Set();
|
|
446
|
+
/** Set by dispose(): this manager makes no further moves. Re-checked at every await boundary a
|
|
447
|
+
* rescue crosses, because a sweep that was already past its first check when dispose() landed
|
|
448
|
+
* would otherwise write an event — and re-populate the queue dispose() had just emptied. */
|
|
449
|
+
disposed = false;
|
|
431
450
|
/** Set by the watchdog for exactly one sweep: ignore the usage-limit hold and make progress. */
|
|
432
451
|
forceNextPump = false;
|
|
433
452
|
/** Runs the watchdog started despite the hold. The spawn-time gate (`requeueWhileHeld`) would
|
|
@@ -466,8 +485,20 @@ export class RunManager {
|
|
|
466
485
|
* repo-root locks, and empties the queued state so nothing fires later.
|
|
467
486
|
* Live sessions are NOT ended here: run lifecycle stays the caller's policy;
|
|
468
487
|
* dispose only guarantees the manager makes no further moves on its own.
|
|
488
|
+
*
|
|
489
|
+
* Every side effect below is SYNCHRONOUS, so a caller that ignores the return value behaves
|
|
490
|
+
* exactly as it did before #125. The returned promise settles the one thing dispose cannot do
|
|
491
|
+
* synchronously: a queue-watchdog rescue that had already started. `clearInterval` stops the
|
|
492
|
+
* next tick, never the running one — the same discipline `AGENTS.md` records for the e2e
|
|
493
|
+
* fixture servers, where `kill()` only delivers the signal and the helper awaits the exit.
|
|
494
|
+
* Await it whenever the data root is about to be removed; a test that deletes its temp
|
|
495
|
+
* directory without awaiting is the ENOENT in #125.
|
|
496
|
+
*
|
|
497
|
+
* It never rejects: a sweep that fails still belongs to whoever started it (the watchdog floats
|
|
498
|
+
* it exactly as before), and teardown must not become a second place that error surfaces.
|
|
469
499
|
*/
|
|
470
500
|
dispose() {
|
|
501
|
+
this.disposed = true;
|
|
471
502
|
this.offUsage();
|
|
472
503
|
this.offSemaphore();
|
|
473
504
|
clearInterval(this.queueWatchdog);
|
|
@@ -489,6 +520,7 @@ export class RunManager {
|
|
|
489
520
|
this.pendingContinuations.clear();
|
|
490
521
|
this.memoryPausing.clear();
|
|
491
522
|
this.lastNamerKey.clear();
|
|
523
|
+
return Promise.allSettled([...this.rescuesInFlight]).then(() => undefined);
|
|
492
524
|
}
|
|
493
525
|
/**
|
|
494
526
|
* Pause any active run whose whole process tree exceeds the WORKSPACE
|
|
@@ -504,11 +536,12 @@ export class RunManager {
|
|
|
504
536
|
const runIds = Object.keys(snapshot).filter((runId) => this.active.has(runId));
|
|
505
537
|
if (runIds.length === 0)
|
|
506
538
|
return;
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
|
|
539
|
+
// Limit from the shared semaphore's in-memory cache (step 2.5: refreshed at boot and
|
|
540
|
+
// on a config PUT — never N per-tick file reads across N projects). This repo's OWN
|
|
541
|
+
// `.xezar/config.json` `memoryLimitMb` wins when it sets one and the workspace ceiling
|
|
542
|
+
// applies otherwise (B2) — the same more-specific-wins rule `projectMaxParallel`
|
|
543
|
+
// follows. Before B2 the per-repo key saved successfully and was then ignored here.
|
|
544
|
+
const limitMb = this.semaphore.projectMemoryLimitMb(this.repoRoot);
|
|
512
545
|
if (!limitMb || limitMb <= 0)
|
|
513
546
|
return;
|
|
514
547
|
const limitBytes = limitMb * 1024 * 1024;
|
|
@@ -558,6 +591,14 @@ export class RunManager {
|
|
|
558
591
|
XEZ_HANDOFF_FILE: handoffPath(this.dataDir, runId),
|
|
559
592
|
XEZ_TASK_ID: runId,
|
|
560
593
|
XEZ_TODOS_FILE: generateFollowups ? todosPath(this.dataDir) : '',
|
|
594
|
+
// The stored env-passthrough list (F), carried on the per-run env so it reaches EVERY
|
|
595
|
+
// backend through the one path they all share (`buildChildEnv(spec.env)`) — no runner
|
|
596
|
+
// seam change, and it is live because the semaphore refreshes on a config PUT. Set to
|
|
597
|
+
// `''` rather than omitted when the list is empty, for the same reason
|
|
598
|
+
// `XEZ_TODOS_FILE` is: the child inherits `process.env`, so an omitted key would let
|
|
599
|
+
// this process's own `XEZ_ENV_PASSTHROUGH` shine through and quietly override the
|
|
600
|
+
// stored "forward nothing" choice.
|
|
601
|
+
XEZ_ENV_PASSTHROUGH: this.semaphore.agentEnvPassthrough().join(','),
|
|
561
602
|
...agentTmpEnv(this.dataDir, runId),
|
|
562
603
|
};
|
|
563
604
|
}
|
|
@@ -607,7 +648,7 @@ export class RunManager {
|
|
|
607
648
|
// The global inbox is the ceiling on the per-run flag (#471). Enforced here rather than
|
|
608
649
|
// at the HTTP route because `xezar run`, the inbox's own "▶ Run" and variants all reach
|
|
609
650
|
// startRun directly — a route-level gate would leave those writing todos.json.
|
|
610
|
-
generateFollowups: followupsEnabled() ? input.generateFollowups : false,
|
|
651
|
+
generateFollowups: this.semaphore.followupsEnabled() ? input.generateFollowups : false,
|
|
611
652
|
// Persist autonomy on the record (#489) so the terminal review gate
|
|
612
653
|
// (`settleSuccess`) and the group-pick winner-park can honor it — mid-run
|
|
613
654
|
// auto-nudge reads `input.autonomous` (`execute`), but the record is the
|
|
@@ -846,8 +887,15 @@ export class RunManager {
|
|
|
846
887
|
* `continue-N` step and the session before it are durable, which is enough. Otherwise the
|
|
847
888
|
* workflow is revived from the record. A run that can be neither is failed loudly rather than
|
|
848
889
|
* left in the queue as a ghost.
|
|
890
|
+
*
|
|
891
|
+
* Both effects here — an NDJSON append and a queue push — must not outlive dispose(), so the
|
|
892
|
+
* flag is re-read after `reviveWorkflow`, the one await this method crosses (#125). Bailing
|
|
893
|
+
* leaves the record `queued`, which is exactly what it was: re-adoption is what boot recovery
|
|
894
|
+
* does for the next owner of this data directory.
|
|
849
895
|
*/
|
|
850
896
|
async reviveQueuedRun(run, reason) {
|
|
897
|
+
if (this.disposed)
|
|
898
|
+
return;
|
|
851
899
|
const queuedContinuation = [...run.steps]
|
|
852
900
|
.reverse()
|
|
853
901
|
.find((step) => step.status === 'pending' && step.id.startsWith('continue-'));
|
|
@@ -872,6 +920,8 @@ export class RunManager {
|
|
|
872
920
|
return;
|
|
873
921
|
}
|
|
874
922
|
const workflow = await this.reviveWorkflow(run);
|
|
923
|
+
if (this.disposed)
|
|
924
|
+
return;
|
|
875
925
|
if (!workflow) {
|
|
876
926
|
this.store.updateRun(run.id, {
|
|
877
927
|
status: 'failed',
|
|
@@ -888,7 +938,7 @@ export class RunManager {
|
|
|
888
938
|
// safe either way — but a run queued while the inbox was on and recovered after it was
|
|
889
939
|
// switched off would otherwise keep echoing `generateFollowups: true` on a run that
|
|
890
940
|
// demonstrably produced none. Normalize the record, the way startRun does.
|
|
891
|
-
const generateFollowups = followupsEnabled() ? run.generateFollowups : false;
|
|
941
|
+
const generateFollowups = this.semaphore.followupsEnabled() ? run.generateFollowups : false;
|
|
892
942
|
if (generateFollowups !== run.generateFollowups) {
|
|
893
943
|
this.store.updateRun(run.id, { generateFollowups });
|
|
894
944
|
}
|
|
@@ -1248,13 +1298,32 @@ export class RunManager {
|
|
|
1248
1298
|
* honest hold, with a real deadline behind it this time.
|
|
1249
1299
|
*
|
|
1250
1300
|
* Public so a test can drive the wedge directly instead of waiting out the interval.
|
|
1301
|
+
*
|
|
1302
|
+
* The sweep itself is `sweepStalledQueue`; this wrapper only publishes it to `rescuesInFlight`
|
|
1303
|
+
* so dispose() can settle it (#125). Every entry point goes through here — the interval and the
|
|
1304
|
+
* tests alike — so nothing can start a sweep that teardown cannot see. The schedule, the
|
|
1305
|
+
* conditions and the effects are untouched.
|
|
1251
1306
|
*/
|
|
1252
1307
|
async rescueStalledQueue(now = Date.now()) {
|
|
1308
|
+
if (this.disposed)
|
|
1309
|
+
return;
|
|
1310
|
+
const sweep = this.sweepStalledQueue(now);
|
|
1311
|
+
this.rescuesInFlight.add(sweep);
|
|
1312
|
+
try {
|
|
1313
|
+
await sweep;
|
|
1314
|
+
}
|
|
1315
|
+
finally {
|
|
1316
|
+
this.rescuesInFlight.delete(sweep);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
async sweepStalledQueue(now) {
|
|
1253
1320
|
// First, the worst shape: a record that says `queued` while the engine holds no job, no
|
|
1254
1321
|
// continuation and no queue entry for it. `pump()` cannot see such a run — it iterates the
|
|
1255
1322
|
// queue, and this one is not in it — so nothing will ever start it. Re-adopt it through the
|
|
1256
1323
|
// same path boot recovery uses.
|
|
1257
1324
|
for (const run of this.store.listRuns()) {
|
|
1325
|
+
if (this.disposed)
|
|
1326
|
+
return;
|
|
1258
1327
|
if (run.status !== 'queued')
|
|
1259
1328
|
continue;
|
|
1260
1329
|
if (this.active.has(run.id) || this.starting.has(run.id))
|
|
@@ -1266,6 +1335,8 @@ export class RunManager {
|
|
|
1266
1335
|
console.warn(`[xez] queue watchdog: re-adopting queued run ${run.id} the engine had lost`);
|
|
1267
1336
|
await this.reviveQueuedRun(run, 'queue watchdog');
|
|
1268
1337
|
}
|
|
1338
|
+
if (this.disposed)
|
|
1339
|
+
return;
|
|
1269
1340
|
if (this.queue.length === 0)
|
|
1270
1341
|
return;
|
|
1271
1342
|
if (this.busySlots() > 0 || this.starting.size > 0)
|
|
@@ -1963,11 +2034,23 @@ export class RunManager {
|
|
|
1963
2034
|
const record = this.store.getRun(runId);
|
|
1964
2035
|
// The env is a live ceiling: a run created while the inbox was on must not keep writing
|
|
1965
2036
|
// follow-ups after it is switched off.
|
|
1966
|
-
const generateFollowups = followupsEnabled() && record?.generateFollowups !== false;
|
|
2037
|
+
const generateFollowups = this.semaphore.followupsEnabled() && record?.generateFollowups !== false;
|
|
1967
2038
|
const cwd = record?.worktreePath && existsSync(record.worktreePath)
|
|
1968
2039
|
? record.worktreePath
|
|
1969
2040
|
: this.repoRoot;
|
|
1970
|
-
|
|
2041
|
+
// Autonomy is re-read from the RECORD, the way restart recovery re-threads it into
|
|
2042
|
+
// `execute`'s input: a continuation builds its OWN `ActiveRun`, and omitting the field here
|
|
2043
|
+
// is what made every Continue and every recovered resume silently non-autonomous while the
|
|
2044
|
+
// record still said otherwise (#141) — the same construction-site asymmetry `state.skills`
|
|
2045
|
+
// hit in #811. `autoContinues` starts at 0 rather than `undefined` so `MAX_AUTO_CONTINUES`
|
|
2046
|
+
// bounds this path too; the budget is per `ActiveRun`, so a Continue starts a fresh one.
|
|
2047
|
+
const state = {
|
|
2048
|
+
cancelled: false,
|
|
2049
|
+
interrupt: () => undefined,
|
|
2050
|
+
cwd,
|
|
2051
|
+
autonomous: record?.autonomous === true,
|
|
2052
|
+
autoContinues: 0,
|
|
2053
|
+
};
|
|
1971
2054
|
this.active.set(runId, state);
|
|
1972
2055
|
this.starting.delete(runId);
|
|
1973
2056
|
if (state.cwd === this.repoRoot) {
|
|
@@ -2091,23 +2174,12 @@ export class RunManager {
|
|
|
2091
2174
|
state.session?.end();
|
|
2092
2175
|
return;
|
|
2093
2176
|
}
|
|
2177
|
+
let autoContinued = false;
|
|
2094
2178
|
if (sessionOpen) {
|
|
2095
2179
|
// Autonomous (#autonomous): never hand the ball back to the user. Nudge the agent to
|
|
2096
|
-
// keep going (bounded by MAX_AUTO_CONTINUES) instead of parking at `waiting`.
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
!state.cancelled &&
|
|
2100
|
-
(() => {
|
|
2101
|
-
const sent = state.session?.sendMessage([{ type: 'text', text: AUTONOMOUS_NUDGE }]);
|
|
2102
|
-
if (!sent)
|
|
2103
|
-
return false;
|
|
2104
|
-
state.autoContinues = (state.autoContinues ?? 0) + 1;
|
|
2105
|
-
this.store.appendEvent(runId, {
|
|
2106
|
-
type: 'note',
|
|
2107
|
-
message: `autonomous — continuing without pausing (${state.autoContinues}/${MAX_AUTO_CONTINUES})`,
|
|
2108
|
-
});
|
|
2109
|
-
return true;
|
|
2110
|
-
})();
|
|
2180
|
+
// keep going (bounded by MAX_AUTO_CONTINUES) instead of parking at `waiting`. Shared
|
|
2181
|
+
// with `execute`'s turn-end handler — one sender, both sites (#141).
|
|
2182
|
+
autoContinued = this.autoContinueTurn(runId, state);
|
|
2111
2183
|
if (!autoContinued) {
|
|
2112
2184
|
// `XEZ:ASK` → park `waiting` (attention) AND surface the structured
|
|
2113
2185
|
// question as an ask card (#473). `XEZ:MONITORING` → non-attention
|
|
@@ -2143,7 +2215,10 @@ export class RunManager {
|
|
|
2143
2215
|
if (this.store.getRun(runId)?.autoResumeAttempts !== undefined) {
|
|
2144
2216
|
this.store.updateRun(runId, { autoResumeAttempts: undefined });
|
|
2145
2217
|
}
|
|
2146
|
-
appendHandoffHeartbeat(this.dataDir, runId,
|
|
2218
|
+
appendHandoffHeartbeat(this.dataDir, runId,
|
|
2219
|
+
// A nudged turn did NOT park — the agent is working again, so the heartbeat must not
|
|
2220
|
+
// read `waiting` (#141; before the fix this branch was unreachable).
|
|
2221
|
+
`turn complete — status=${autoContinued ? 'running' : monitoring ? 'monitoring' : sessionOpen ? 'waiting' : 'running'}`);
|
|
2147
2222
|
}
|
|
2148
2223
|
};
|
|
2149
2224
|
// Backend + model come off the record: the run's current backend by default, or the
|
|
@@ -2264,11 +2339,37 @@ export class RunManager {
|
|
|
2264
2339
|
if (session.pid !== undefined)
|
|
2265
2340
|
registerRunProcess(runId, session.pid);
|
|
2266
2341
|
const finishedAt = () => new Date().toISOString();
|
|
2342
|
+
/**
|
|
2343
|
+
* The turn's teardown — usage peaks, timers, and the turn-end autosave commit — runs
|
|
2344
|
+
* BEFORE this turn's terminal status is published, which is the order `execute` already
|
|
2345
|
+
* uses on the workflow path (`run finalize`, then `settleSuccess`, then `dropActive`,
|
|
2346
|
+
* with no await between the last two). The continuation path had it the other way round,
|
|
2347
|
+
* and that asymmetry is the second half of #155: `settleSuccess` published `review` and
|
|
2348
|
+
* only then did the `finally` spawn git for `autosaveCommit`, so for the whole width of
|
|
2349
|
+
* that commit the run advertised the review gate while still sitting in `active`.
|
|
2350
|
+
* `finish()` refuses to accept a `review` run while `isActive(runId)` holds — the guard
|
|
2351
|
+
* that stops a Finish from racing a send-back continuation into `done` — so a ✓ Accept
|
|
2352
|
+
* arriving in that window answered `409 no open session` and the accept was lost, with
|
|
2353
|
+
* the run left parked at `review` forever. Idempotent, because the terminal paths call it
|
|
2354
|
+
* explicitly and `finally` calls it again for a throw that never reached one.
|
|
2355
|
+
*/
|
|
2356
|
+
let turnTornDown = false;
|
|
2357
|
+
const endTurn = async () => {
|
|
2358
|
+
if (turnTornDown)
|
|
2359
|
+
return;
|
|
2360
|
+
turnTornDown = true;
|
|
2361
|
+
this.recordUsagePeaks(runId);
|
|
2362
|
+
this.clearIdleTimer(state);
|
|
2363
|
+
this.clearAutosaveTimer(state);
|
|
2364
|
+
if (state.cwd !== this.repoRoot)
|
|
2365
|
+
await autosaveCommit(state.cwd, 'turn end');
|
|
2366
|
+
};
|
|
2267
2367
|
try {
|
|
2268
2368
|
await session.result;
|
|
2269
2369
|
if (sessionError)
|
|
2270
2370
|
throw new Error(sessionError);
|
|
2271
2371
|
sink.sessionEnded(state.cancelled ? 'cancelled' : 'end_turn');
|
|
2372
|
+
await endTurn();
|
|
2272
2373
|
if (state.cancelled) {
|
|
2273
2374
|
this.store.updateStep(runId, stepId, { status: 'cancelled', finishedAt: finishedAt() });
|
|
2274
2375
|
this.store.updateRun(runId, { status: 'cancelled', finishedAt: finishedAt(), currentStepId: undefined });
|
|
@@ -2285,6 +2386,7 @@ export class RunManager {
|
|
|
2285
2386
|
catch (err) {
|
|
2286
2387
|
const message = err instanceof Error ? err.message : String(err);
|
|
2287
2388
|
sink.sessionEnded('error', message);
|
|
2389
|
+
await endTurn();
|
|
2288
2390
|
this.store.updateStep(runId, stepId, { status: 'failed', error: message, finishedAt: finishedAt() });
|
|
2289
2391
|
appendHandoffHeartbeat(this.dataDir, runId, `step "${stepId}" complete — status=failed`);
|
|
2290
2392
|
this.store.updateRun(runId, {
|
|
@@ -2296,14 +2398,38 @@ export class RunManager {
|
|
|
2296
2398
|
this.store.appendEvent(runId, { type: 'lifecycle', message: `continue failed — ${message}` });
|
|
2297
2399
|
}
|
|
2298
2400
|
finally {
|
|
2299
|
-
|
|
2300
|
-
this.clearIdleTimer(state);
|
|
2301
|
-
this.clearAutosaveTimer(state);
|
|
2302
|
-
if (state.cwd !== this.repoRoot)
|
|
2303
|
-
await autosaveCommit(state.cwd, 'turn end');
|
|
2401
|
+
await endTurn();
|
|
2304
2402
|
this.dropActive(runId);
|
|
2305
2403
|
}
|
|
2306
2404
|
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Autonomous turn end (#489): never hand the ball back to the user — nudge the agent to keep
|
|
2407
|
+
* going and report whether the ball stayed with it. `false` means "park normally", which is
|
|
2408
|
+
* every non-autonomous run, a run whose pause cleared `autonomous` (the memory-limit
|
|
2409
|
+
* suppression above), a cancelled one, a closed session that cannot take the message, and a
|
|
2410
|
+
* run that has spent its `MAX_AUTO_CONTINUES` budget.
|
|
2411
|
+
*
|
|
2412
|
+
* ONE helper, called from BOTH turn-end handlers, because the split is exactly what broke:
|
|
2413
|
+
* `execute` set `autonomous` on its `ActiveRun` and its handler never read it, while
|
|
2414
|
+
* `runContinuation`'s handler read it and its `ActiveRun` never set it — so the nudge fired
|
|
2415
|
+
* nowhere and every autonomous run parked after one turn (#141). AGENTS.md names both traps:
|
|
2416
|
+
* grep the TYPE at every construction site, and treat the two near-identical turn-end handlers
|
|
2417
|
+
* as one place. Keep this the only sender of `AUTONOMOUS_NUDGE`.
|
|
2418
|
+
*/
|
|
2419
|
+
autoContinueTurn(runId, state) {
|
|
2420
|
+
if (!state.autonomous || state.cancelled)
|
|
2421
|
+
return false;
|
|
2422
|
+
if ((state.autoContinues ?? 0) >= MAX_AUTO_CONTINUES)
|
|
2423
|
+
return false;
|
|
2424
|
+
if (!state.session?.sendMessage([{ type: 'text', text: AUTONOMOUS_NUDGE }]))
|
|
2425
|
+
return false;
|
|
2426
|
+
state.autoContinues = (state.autoContinues ?? 0) + 1;
|
|
2427
|
+
this.store.appendEvent(runId, {
|
|
2428
|
+
type: 'note',
|
|
2429
|
+
message: `autonomous — continuing without pausing (${state.autoContinues}/${MAX_AUTO_CONTINUES})`,
|
|
2430
|
+
});
|
|
2431
|
+
return true;
|
|
2432
|
+
}
|
|
2307
2433
|
// ---- execution -----------------------------------------------------------
|
|
2308
2434
|
async execute(runId, workflow, input) {
|
|
2309
2435
|
const state = {
|
|
@@ -2730,7 +2856,13 @@ export class RunManager {
|
|
|
2730
2856
|
return;
|
|
2731
2857
|
}
|
|
2732
2858
|
const waiting = interactive && sessionOpen;
|
|
2733
|
-
|
|
2859
|
+
// Autonomous (#autonomous): the turn is over and the session is open, which is exactly
|
|
2860
|
+
// the moment this run must NOT park. Same helper, same bound, same note as
|
|
2861
|
+
// `runContinuation`'s handler (#141) — the field `execute` has always set is finally
|
|
2862
|
+
// read here. `interactive` still gates it: a mid-workflow step ends its own session and
|
|
2863
|
+
// the next step follows, so there is no ball to hand back and nothing to nudge.
|
|
2864
|
+
const autoContinued = waiting ? this.autoContinueTurn(runId, state) : false;
|
|
2865
|
+
if (waiting && !autoContinued) {
|
|
2734
2866
|
// Turn over, session open. Either the ball is in the user's court
|
|
2735
2867
|
// (`waiting`) — optionally with a structured `XEZ:ASK` question the
|
|
2736
2868
|
// cockpit renders as an ask card (#473) — or the agent declared it is
|
|
@@ -2764,7 +2896,7 @@ export class RunManager {
|
|
|
2764
2896
|
}
|
|
2765
2897
|
// Xez's own heartbeat — the handoff stays current even when the
|
|
2766
2898
|
// agent forgets to write (spec 007).
|
|
2767
|
-
appendHandoffHeartbeat(this.dataDir, runId, `turn complete — status=${monitoring ? 'monitoring' : waiting ? 'waiting' : 'running'}`);
|
|
2899
|
+
appendHandoffHeartbeat(this.dataDir, runId, `turn complete — status=${autoContinued ? 'running' : monitoring ? 'monitoring' : waiting ? 'waiting' : 'running'}`);
|
|
2768
2900
|
}
|
|
2769
2901
|
};
|
|
2770
2902
|
const stepBackend = step.runner ?? taskBackend;
|
|
@@ -2797,7 +2929,7 @@ export class RunManager {
|
|
|
2797
2929
|
let stepProfile;
|
|
2798
2930
|
try {
|
|
2799
2931
|
stepProfile = await this.agentEnvForStep(runId, stepBackend, {
|
|
2800
|
-
generateFollowups: followupsEnabled() && input.generateFollowups !== false,
|
|
2932
|
+
generateFollowups: this.semaphore.followupsEnabled() && input.generateFollowups !== false,
|
|
2801
2933
|
});
|
|
2802
2934
|
}
|
|
2803
2935
|
catch (err) {
|
|
@@ -2814,7 +2946,7 @@ export class RunManager {
|
|
|
2814
2946
|
session = runner.startSession({
|
|
2815
2947
|
// Skill body, then the run's extra prompt (POST override or config
|
|
2816
2948
|
// default), then the handoff/todos contract — every agent step.
|
|
2817
|
-
systemPrompt: composeSystemPrompt(systemPrompt, extraSystemPrompt, followupsEnabled() && input.generateFollowups !== false
|
|
2949
|
+
systemPrompt: composeSystemPrompt(systemPrompt, extraSystemPrompt, this.semaphore.followupsEnabled() && input.generateFollowups !== false
|
|
2818
2950
|
? HANDOFF_INSTRUCTIONS
|
|
2819
2951
|
: HANDOFF_ONLY_INSTRUCTIONS),
|
|
2820
2952
|
userPrompt,
|
|
@@ -2827,8 +2959,10 @@ export class RunManager {
|
|
|
2827
2959
|
env: stepProfile.env,
|
|
2828
2960
|
model: backendModel,
|
|
2829
2961
|
sessionId,
|
|
2830
|
-
// Interactive sessions have no wall clock — the idle timer rules.
|
|
2831
|
-
|
|
2962
|
+
// Interactive sessions have no wall clock — the idle timer rules. A
|
|
2963
|
+
// step's own `timeout` (#22) outranks that; with the field absent
|
|
2964
|
+
// this is byte-for-byte the pre-#22 `interactive ? 0 : undefined`.
|
|
2965
|
+
timeoutMs: stepTimeoutMs(step, interactive),
|
|
2832
2966
|
}, onEvent, {
|
|
2833
2967
|
autoEndAfterFirstTurn: !interactive,
|
|
2834
2968
|
onUiEvent: (event) => this.handleRunnerUiEvent(runId, state, sink, event),
|
|
@@ -3235,17 +3369,29 @@ export class RunManager {
|
|
|
3235
3369
|
return null;
|
|
3236
3370
|
}
|
|
3237
3371
|
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Arm the idle wall clock for a session that just parked at `waiting`.
|
|
3374
|
+
*
|
|
3375
|
+
* The duration comes from the shared semaphore's cached `resources.idleTimeoutMinutes`,
|
|
3376
|
+
* so a change made in Settings applies to the next park with no restart. `null` means
|
|
3377
|
+
* the operator chose "never close on idle": no timer is armed, and the only exits left
|
|
3378
|
+
* are a user message and Cancel — both of which a `waiting` run already advertises.
|
|
3379
|
+
*/
|
|
3238
3380
|
armIdleTimer(runId, state) {
|
|
3239
3381
|
this.clearIdleTimer(state);
|
|
3382
|
+
const minutes = this.semaphore.idleTimeoutMinutes();
|
|
3383
|
+
if (minutes === null)
|
|
3384
|
+
return;
|
|
3385
|
+
const timeoutMs = minutes * 60_000;
|
|
3240
3386
|
state.idleTimer = setTimeout(() => {
|
|
3241
3387
|
if (state.session?.open && !state.cancelled) {
|
|
3242
3388
|
this.store.appendEvent(runId, {
|
|
3243
3389
|
type: 'lifecycle',
|
|
3244
|
-
message: `session closed after ${Math.round(
|
|
3390
|
+
message: `session closed after ${Math.round(timeoutMs / 60_000)}m of inactivity`,
|
|
3245
3391
|
});
|
|
3246
3392
|
state.session.end();
|
|
3247
3393
|
}
|
|
3248
|
-
},
|
|
3394
|
+
}, timeoutMs);
|
|
3249
3395
|
state.idleTimer.unref?.();
|
|
3250
3396
|
}
|
|
3251
3397
|
clearIdleTimer(state) {
|