@sema-agent/core 7.31.1 → 7.32.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/CHANGELOG.md +64 -0
- package/dist/agents/inherited-tool-pool.d.ts +28 -0
- package/dist/agents/inherited-tool-pool.js +15 -0
- package/dist/agents/list-agents-tool.d.ts +7 -0
- package/dist/agents/list-agents-tool.js +9 -2
- package/dist/agents/peer-session-drain.d.ts +2 -0
- package/dist/agents/peer-session-drain.js +1 -0
- package/dist/agents/send-message-tool.d.ts +17 -0
- package/dist/agents/send-message-tool.js +11 -4
- package/dist/agents/subagent.d.ts +10 -1
- package/dist/agents/subagent.js +24 -8
- package/dist/core/ask-origin.d.ts +15 -6
- package/dist/core/ask-origin.js +15 -9
- package/dist/core/background-shell.d.ts +45 -0
- package/dist/core/background-shell.js +13 -0
- package/dist/core/checkpoint-store.d.ts +5 -0
- package/dist/core/checkpoint-store.js +3 -2
- package/dist/core/engine-notice.d.ts +9 -0
- package/dist/core/gate-lanes.js +1 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +2 -0
- package/dist/core/read-root-grant-screen.d.ts +23 -0
- package/dist/core/read-root-grant-screen.js +17 -0
- package/dist/core/runner/contracts.d.ts +1 -1
- package/dist/core/runner/prepare-artifact.d.ts +12 -7
- package/dist/core/runner/prepare-artifact.js +10 -8
- package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
- package/dist/core/runner/prepare-ask-lane.js +5 -1
- package/dist/core/runner/prepare-boundary-parks.js +5 -5
- package/dist/core/runner/prepare-delegation-surface.d.ts +18 -0
- package/dist/core/runner/prepare-delegation-surface.js +6 -25
- package/dist/core/runner/prepare-hands-readface.d.ts +12 -89
- package/dist/core/runner/prepare-hands-readface.js +14 -48
- package/dist/core/runner/prepare-inherited-gate.d.ts +3 -1
- package/dist/core/runner/prepare-inherited-gate.js +2 -1
- package/dist/core/runner/prepare-park-ask.js +6 -4
- package/dist/core/runner/prepare-policy-chain.js +4 -0
- package/dist/core/runner/prepare-task.js +5 -3
- package/dist/core/runner/read-root-grant.d.ts +66 -0
- package/dist/core/runner/read-root-grant.js +77 -0
- package/dist/core/runner/run-notification-lane.d.ts +1 -1
- package/dist/core/runner/run-settle-and-teardown.js +3 -9
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/runner/send-message-continuation.d.ts +45 -0
- package/dist/core/runner/send-message-continuation.js +27 -0
- package/dist/core/runner/session-read-file-states.d.ts +91 -0
- package/dist/core/runner/session-read-file-states.js +43 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +19 -0
- package/dist/core/task-registry-monitor.js +3 -0
- package/dist/core/task-registry-shared.d.ts +16 -3
- package/dist/core/task-registry.d.ts +65 -29
- package/dist/core/task-registry.js +78 -28
- package/dist/core/task-spec.d.ts +5 -3
- package/dist/core/tool-policy.d.ts +23 -0
- package/dist/core/tool-policy.js +15 -4
- package/dist/core/tool-spec.d.ts +2 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +13 -3
- package/dist/engine/execution-env/node-execution-env.js +26 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/scenarios/full-body.d.ts +5 -2
- package/dist/scenarios/full-body.js +14 -7
- package/dist/tools/fs/fs-bash-background-text.d.ts +16 -0
- package/dist/tools/fs/fs-bash-background-text.js +22 -0
- package/dist/tools/fs/fs-bash.d.ts +6 -0
- package/dist/tools/fs/fs-bash.js +9 -27
- package/dist/tools/fs/fs-read.d.ts +2 -2
- package/dist/tools/fs/fs-search-tools.d.ts +3 -2
- package/dist/tools/fs/index.d.ts +8 -1
- package/dist/tools/fs/index.js +2 -3
- package/dist/tools/fs/repo-map.d.ts +2 -1
- package/dist/tools/fs/safety.d.ts +3 -1
- package/dist/tools/fs/safety.js +2 -1
- package/dist/tools/fs/shell-read-check.d.ts +3 -0
- package/dist/tools/fs/shell-read-check.js +1 -1
- package/dist/tools/fs/shell-read-establish.d.ts +3 -1
- package/dist/tools/fs/shell-read-establish.js +2 -2
- package/dist/tools/monitor.d.ts +5 -4
- package/dist/tools/monitor.js +5 -4
- package/dist/tools/todo.d.ts +3 -3
- package/dist/tools/todo.js +5 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +13 -1
- package/wire-changes/core@7.32.0.json +850 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Runner's session-scoped read-file-state CACHE and the licence to inherit from it: session-scoped cache
|
|
3
|
+
* machinery, not a phase step — the Runner owns the instance for its lifetime, the run internals carry it to the
|
|
4
|
+
* hands phase, and the hands phase adopts, inherits and seals seats through it.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReadFileState } from "../../tools/fs/safety.js";
|
|
7
|
+
/**
|
|
8
|
+
* The Runner's per-session read-file-state seats (CC parity: readFileState is minted
|
|
9
|
+
* once per REPL session and mutated across turns; it has NO turn-boundary and NO abort clear site —
|
|
10
|
+
* its only clears are compaction/fork/subagent-cleanup/dispose). sema's map stays per-task (the §11
|
|
11
|
+
* ruling-A closure wiring), so the SESSION scope lives here instead: prepare adopts each task's live
|
|
12
|
+
* map under its sessionId, and the next same-session, same-Runner task INHERITS it (through the same
|
|
13
|
+
* containment judgment the transcript-replay seed applies — see `inheritSessionReadFileState`).
|
|
14
|
+
*
|
|
15
|
+
* This seat is a CACHE, never an authority:
|
|
16
|
+
* - a durable resume's checkpoint seed stays the sole authority on that leg (the seed was cut
|
|
17
|
+
* atomically with the leaf; this seat may be older OR newer than it);
|
|
18
|
+
* - a cross-process continuation misses here and falls back to the transcript replay (rung-2);
|
|
19
|
+
* - eviction (the LRU cap, a Runner restart) degrades to that same replay — never to wrong state.
|
|
20
|
+
*
|
|
21
|
+
* Bounded: an LRU over sessionIds (insertion-order refresh on read), because the SessionStore's own
|
|
22
|
+
* eviction is not observable from here — a dropped seat costs one transcript replay, nothing else.
|
|
23
|
+
* The stored value is the task's LIVE map (object identity), so in-turn mutations — tool write-backs,
|
|
24
|
+
* and above all the compaction clear — are the seat's truth by construction; a snapshot copy
|
|
25
|
+
* here would resurrect summarized-away reads on the next turn.
|
|
26
|
+
*
|
|
27
|
+
* BRANCH BINDING: a sessionId alone does not name a transcript — the same id
|
|
28
|
+
* can be rebranched (`resumeAt`), rewound out-of-band, or DELETED and re-created (`release()` + the
|
|
29
|
+
* default create-on-miss acquire), and the seat's map would then vouch for reads the CURRENT branch
|
|
30
|
+
* never saw. Each seat therefore records `anchorLeafId` — a leaf on the lineage its task produced —
|
|
31
|
+
* and inheritance must first prove the CURRENT branch still descends from that anchor
|
|
32
|
+
* (see {@link sessionSeatAnchorHolds}). The anchor is written twice: at ADOPT (the leaf at prepare;
|
|
33
|
+
* `null` when the branch was still empty) and SEALED at the task's settle to the task's own tail
|
|
34
|
+
* (`reanchor`). A `null` anchor is NEVER inheritable ("the branch is non-empty now" proves
|
|
35
|
+
* nothing about WHOSE messages those are — another Runner repopulating a released id, or a session
|
|
36
|
+
* import, makes a non-empty branch that never contained this seat's reads). An unprovable anchor
|
|
37
|
+
* falls back to the transcript replay, which reads the current branch by construction — cost, never
|
|
38
|
+
* wrongness.
|
|
39
|
+
*/
|
|
40
|
+
export declare class SessionReadFileStates {
|
|
41
|
+
private readonly seats;
|
|
42
|
+
private static readonly CAP;
|
|
43
|
+
/** The session's seat row, or undefined (cross-process / evicted / first turn) ⇒ caller replays.
|
|
44
|
+
* The caller must verify the anchor against the session's CURRENT branch before using `state`. */
|
|
45
|
+
get(sessionId: string): {
|
|
46
|
+
state: ReadFileState;
|
|
47
|
+
anchorLeafId: string | null;
|
|
48
|
+
} | undefined;
|
|
49
|
+
/** Adopt THIS task's live map as the session's seat (every hands leg: fresh, continuation, resume).
|
|
50
|
+
* `anchorLeafId` = the session's leaf as read at THIS prepare (null = the branch was empty; a
|
|
51
|
+
* null-anchored seat is not inheritable until the settle-time {@link reanchor} names a real leaf). */
|
|
52
|
+
adopt(sessionId: string, state: ReadFileState, anchorLeafId: string | null): void;
|
|
53
|
+
/** The settle-time SEAL: re-anchor the seat to the finished task's own tail, so a
|
|
54
|
+
* first-task (null-anchor) seat becomes inheritable only through a leaf its task provably wrote.
|
|
55
|
+
* Identity-guarded: touches the row only while it still holds THIS task's map (a later prepare on
|
|
56
|
+
* the same session has already replaced the seat and owns its own anchor). */
|
|
57
|
+
reanchor(sessionId: string, state: ReadFileState, anchorLeafId: string): void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Does the session's CURRENT branch still descend from the seat's anchor?
|
|
61
|
+
* TRUE is the licence to inherit; anything unprovable answers FALSE (the caller then replays the
|
|
62
|
+
* transcript — the fail-closed direction costs a rebuild, never admits a wrong entry).
|
|
63
|
+
*
|
|
64
|
+
* - anchor `null`: NEVER inheritable (r2). The seat was adopted on an empty branch and its task
|
|
65
|
+
* never sealed (crashed pre-settle, or the leaf read failed) — "the branch is non-empty now"
|
|
66
|
+
* would also be true of a released-and-repopulated id or an imported log, whose messages are not
|
|
67
|
+
* this seat's lineage. The settle-time reanchor is what upgrades such a seat to a real leaf.
|
|
68
|
+
* - anchor non-null: holds iff walking parentIds from the current leaf reaches the anchor within
|
|
69
|
+
* a bounded number of steps (the walk length is one task's appends in the ordinary continuation).
|
|
70
|
+
* A rebranch/rewind moved the leaf OFF the anchor's subtree ⇒ the walk misses ⇒ no inherit; a
|
|
71
|
+
* recreated/foreign log does not contain the anchor ⇒ same answer; a re-IMPORT of the same log
|
|
72
|
+
* keeps the anchor's lineage ⇒ inheritance stays correct there by the same test.
|
|
73
|
+
* - a COMPACTION on the walked segment (A-082 idx2): refuses, even though the branch is perfectly
|
|
74
|
+
* intact. The walked segment is exactly what landed AFTER this seat was anchored, and a
|
|
75
|
+
* compaction there summarized older Read results OUT of the context the model will be shown —
|
|
76
|
+
* the very state the compaction clear removes from the live map at the landing site. That clear reaches the seat
|
|
77
|
+
* only when the SAME Runner did both (the seat IS that map, by object identity); a second Runner
|
|
78
|
+
* on the shared durable session clears its own map and commits the compaction as an ORDINARY
|
|
79
|
+
* LINEAR APPEND, which every other test here passes. Inheriting across it would let requireRead
|
|
80
|
+
* and checkStale both pass on a read whose card the model no longer has — a blind edit from a
|
|
81
|
+
* summarized-away recollection, the exact failure the compaction clear exists to prevent. Refusing
|
|
82
|
+
* costs one transcript replay, and the replay reads the POST-compaction branch by construction,
|
|
83
|
+
* so the fail-closed direction is also the CORRECT state. A compaction at or ABOVE the anchor is
|
|
84
|
+
* not on the walked segment and does not refuse: the seat that observed it already carries its
|
|
85
|
+
* clear (P-483-4), and an ordinary foreign turn with no compaction still inherits (G-483-5) —
|
|
86
|
+
* dropping every seat a foreign Runner touched would restore the cross-turn read-state amnesia this seat-anchor check already ended.
|
|
87
|
+
*/
|
|
88
|
+
export declare function sessionSeatAnchorHolds(getEntry: (id: string) => Promise<{
|
|
89
|
+
parentId: string | null;
|
|
90
|
+
type?: string;
|
|
91
|
+
} | null | undefined>, anchorLeafId: string | null, currentLeafId: string | null): Promise<boolean>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export class SessionReadFileStates {
|
|
2
|
+
seats = new Map();
|
|
3
|
+
static CAP = 1024;
|
|
4
|
+
get(sessionId) {
|
|
5
|
+
const seat = this.seats.get(sessionId);
|
|
6
|
+
if (seat !== undefined) {
|
|
7
|
+
this.seats.delete(sessionId);
|
|
8
|
+
this.seats.set(sessionId, seat);
|
|
9
|
+
}
|
|
10
|
+
return seat;
|
|
11
|
+
}
|
|
12
|
+
adopt(sessionId, state, anchorLeafId) {
|
|
13
|
+
this.seats.delete(sessionId);
|
|
14
|
+
this.seats.set(sessionId, { state, anchorLeafId });
|
|
15
|
+
while (this.seats.size > SessionReadFileStates.CAP) {
|
|
16
|
+
const oldest = this.seats.keys().next().value;
|
|
17
|
+
if (oldest === undefined)
|
|
18
|
+
break;
|
|
19
|
+
this.seats.delete(oldest);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
reanchor(sessionId, state, anchorLeafId) {
|
|
23
|
+
const seat = this.seats.get(sessionId);
|
|
24
|
+
if (seat !== undefined && seat.state === state)
|
|
25
|
+
seat.anchorLeafId = anchorLeafId;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function sessionSeatAnchorHolds(getEntry, anchorLeafId, currentLeafId) {
|
|
29
|
+
if (anchorLeafId === null)
|
|
30
|
+
return false;
|
|
31
|
+
let id = currentLeafId;
|
|
32
|
+
for (let steps = 0; id !== null && steps < 100_000; steps++) {
|
|
33
|
+
if (id === anchorLeafId)
|
|
34
|
+
return true;
|
|
35
|
+
const entry = await getEntry(id);
|
|
36
|
+
if (entry === null || entry === undefined)
|
|
37
|
+
return false;
|
|
38
|
+
if (entry.type === "compaction")
|
|
39
|
+
return false;
|
|
40
|
+
id = entry.parentId;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
@@ -51,6 +51,7 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
|
|
|
51
51
|
toolName: string;
|
|
52
52
|
toolInput: string;
|
|
53
53
|
mandated: boolean;
|
|
54
|
+
mandate: string;
|
|
54
55
|
origin: string;
|
|
55
56
|
spentMicroUsd?: undefined;
|
|
56
57
|
} | {
|
|
@@ -68,5 +69,6 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
|
|
|
68
69
|
toolName?: undefined;
|
|
69
70
|
toolInput?: undefined;
|
|
70
71
|
mandated?: undefined;
|
|
72
|
+
mandate?: undefined;
|
|
71
73
|
origin?: undefined;
|
|
72
74
|
})[];
|
|
@@ -363,6 +363,23 @@ export async function checkpointStoreContract(make, runAssertion) {
|
|
|
363
363
|
assert.equal("mandated" in byId.get("md-false"), false, "a written false is out-of-contract and must read as absent, never project");
|
|
364
364
|
assert.equal("mandated" in byId.get("md-string"), false, "a non-boolean spelling must read as absent, never project verbatim");
|
|
365
365
|
});
|
|
366
|
+
run("mandate word: a row's closed-set word survives + projects; absent stays absent; a word outside the set reads as absent", async () => {
|
|
367
|
+
const store = make();
|
|
368
|
+
const worded = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "mw-word" });
|
|
369
|
+
Object.assign(worded.pendingAction, { mandated: true, mandate: "probe_mandate" });
|
|
370
|
+
const bare = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "mw-absent" });
|
|
371
|
+
Object.assign(bare.pendingAction, { mandated: true });
|
|
372
|
+
const bogus = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "mw-bogus" });
|
|
373
|
+
Object.assign(bogus.pendingAction, { mandated: true, mandate: "bypass_ok" });
|
|
374
|
+
for (const cp of [worded, bare, bogus])
|
|
375
|
+
await store.put(cp.token, cp);
|
|
376
|
+
assert.equal((await store.get(worded.token)).pendingAction.mandate, "probe_mandate", "the row's mandate word must survive the round-trip verbatim");
|
|
377
|
+
assert.equal("mandate" in (await store.get(bare.token)).pendingAction, false, "an absent word must stay OMITTED on the row");
|
|
378
|
+
const byId = new Map((await store.listByScope("tenant-a")).map((s) => [s.sessionId, s]));
|
|
379
|
+
assert.equal(byId.get("mw-word")?.mandate, "probe_mandate");
|
|
380
|
+
assert.equal("mandate" in byId.get("mw-absent"), false, "absent must project the key OMITTED");
|
|
381
|
+
assert.equal("mandate" in byId.get("mw-bogus"), false, "a word outside the closed set must read as absent, never project verbatim");
|
|
382
|
+
});
|
|
366
383
|
await settle();
|
|
367
384
|
}
|
|
368
385
|
const sortByToken = (a, b) => a.token.localeCompare(b.token);
|
|
@@ -388,6 +405,7 @@ export async function checkpointListByScopeSummaries(store) {
|
|
|
388
405
|
kind: "tool_approval",
|
|
389
406
|
origin: "policy",
|
|
390
407
|
mandated: true,
|
|
408
|
+
mandate: "tool_marks",
|
|
391
409
|
toolCallId: "call-pr",
|
|
392
410
|
toolName: "open_pr",
|
|
393
411
|
args: { repo: "x" },
|
|
@@ -428,6 +446,7 @@ export const EXPECTED_LISTBYSCOPE_SUMMARIES = [
|
|
|
428
446
|
toolName: "open_pr",
|
|
429
447
|
toolInput: '{"repo":"x"}',
|
|
430
448
|
mandated: true,
|
|
449
|
+
mandate: "tool_marks",
|
|
431
450
|
origin: "policy",
|
|
432
451
|
},
|
|
433
452
|
{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
2
|
+
import { envDeclaresRetain, retainsBackgroundProcesses } from "./background-shell.js";
|
|
2
3
|
import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_CHARS, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, renderSpoolBody, SPOOL_REPLAY, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
|
|
3
4
|
import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
|
|
4
5
|
import { thrownText } from "./thrown-value.js";
|
|
@@ -41,6 +42,8 @@ export function registerMonitorLane(core, input) {
|
|
|
41
42
|
...(input.onEvent !== undefined ? { onEvent: input.onEvent } : {}),
|
|
42
43
|
...(input.toolResultStore !== undefined ? { toolResultStore: input.toolResultStore } : {}),
|
|
43
44
|
...(input.sessionId !== undefined ? { spillSessionId: input.sessionId } : {}),
|
|
45
|
+
...(retainsBackgroundProcesses({ env: envDeclaresRetain(input.env), task: input.retainBackgroundProcesses === true }) ? { retained: true } : {}),
|
|
46
|
+
...(input.retainBackgroundProcesses !== undefined ? { retainStatedAtLaunch: true } : {}),
|
|
44
47
|
};
|
|
45
48
|
core.handles.set(id, handle);
|
|
46
49
|
try {
|
|
@@ -133,9 +133,11 @@ export interface BackgroundBashTaskHandle extends SemaTaskHandle {
|
|
|
133
133
|
* run-teardown settle/evict, kept alive through the dispose except-list, reaped by
|
|
134
134
|
* {@link TaskRegistry.reapSessionBackground}. Explicit flag only — never inferred from owner shape. */
|
|
135
135
|
sessionScoped?: true;
|
|
136
|
-
/** Deliver-a-living-service ledger mark
|
|
137
|
-
*
|
|
138
|
-
*
|
|
136
|
+
/** Deliver-a-living-service ledger mark — the row's lane of THE retain predicate
|
|
137
|
+
* (`retainsBackgroundProcesses`). Set at REGISTRATION when the launching task
|
|
138
|
+
* (`TaskSpec.retainBackgroundProcesses`) or the env capability declares retain, and by a run teardown
|
|
139
|
+
* whose own spec declares it. In-memory only (shell-lane rows are never persisted); a row without the
|
|
140
|
+
* field reads as not marked. The row is the delivered daemon's ONLY management surface — while the process runs it survives the
|
|
139
141
|
* per-run evict (TaskOutput/TaskStop resolve through this map; the watcher must stay live to mint
|
|
140
142
|
* the natural-exit receipt) and rides every later run's dispose except-list on the same env
|
|
141
143
|
* ({@link TaskRegistry.retainedShellIds}). A terminal row evicts normally. Terminals: explicit
|
|
@@ -144,6 +146,11 @@ export interface BackgroundBashTaskHandle extends SemaTaskHandle {
|
|
|
144
146
|
* row is likewise exempt from the session-release reap ({@link TaskRegistry.reapSessionBackground}):
|
|
145
147
|
* the delivered daemon outlives its session, with the same three terminals. */
|
|
146
148
|
retained?: true;
|
|
149
|
+
/** The launcher STATED the task lane at registration (`retainBackgroundProcesses` was passed, true or
|
|
150
|
+
* false): the mark above already carries it, so a run teardown's own spec lane does not re-judge this row
|
|
151
|
+
* (a later run's declaration must not re-label a process its launch receipt described). Absent ⇒ the
|
|
152
|
+
* launcher did not know the lane (a host tool registering directly): the owning run's teardown supplies it. */
|
|
153
|
+
retainStatedAtLaunch?: true;
|
|
147
154
|
/** design/116 §7 G2b: when a watcher is attached, the registry accumulates the FULL output here (rolling
|
|
148
155
|
* bound) — TaskOutput then serves a RE-READABLE snapshot (CC semantics) instead of a consume-once cursor.
|
|
149
156
|
* `timedOut`/`timeoutSec` record a SELF-INFLICTED background-timeout kill (the env's per-shell budget
|
|
@@ -449,6 +456,8 @@ export interface MonitorTaskHandle extends SemaTaskHandle {
|
|
|
449
456
|
sessionScoped?: true;
|
|
450
457
|
/** Deliver-a-living-service ledger mark — same semantics as the bash handle's `retained` (see there). */
|
|
451
458
|
retained?: true;
|
|
459
|
+
/** Same as the bash handle's `retainStatedAtLaunch` (see there). */
|
|
460
|
+
retainStatedAtLaunch?: true;
|
|
452
461
|
/** Non-persistent only: watcher-enforced hard deadline (epoch ms on the injected clock).
|
|
453
462
|
* (CC parity): this deadline — not the parent run — is the watch's lifetime anchor.
|
|
454
463
|
* A monitor's life = min(timeout, process exit, TaskStop); a persistent one is SESSION-anchored.
|
|
@@ -523,6 +532,10 @@ export interface RegisterMonitorInput extends TaskAccess {
|
|
|
523
532
|
timers?: MonitorTimers;
|
|
524
533
|
/** Batch window (ms, default 200): lines arriving within one tick coalesce into one notification. */
|
|
525
534
|
batchWindowMs?: number;
|
|
535
|
+
/** The LAUNCHING task's retain declaration (`TaskSpec.retainBackgroundProcesses`) — the task lane of
|
|
536
|
+
* THE retain predicate; stamped onto the row's `retained` mark at registration, and passing it states the
|
|
537
|
+
* lane, same as the bash lane (see `RegisterBackgroundBashInput.retainBackgroundProcesses`). */
|
|
538
|
+
retainBackgroundProcesses?: boolean;
|
|
526
539
|
/** The SUSTAINED batch rate the storm bucket refills at (default 50/min). Exceeding it is
|
|
527
540
|
* suppression, not death — see `stormBurst` and MONITOR_STORM_KILL_AFTER_MS. */
|
|
528
541
|
maxBatchesPerMinute?: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentTool, ExecutionEnv } from "../internal/harness.js";
|
|
2
2
|
import type { ToolEffect } from "./types.js";
|
|
3
|
-
import { type BackgroundShellCapability, type BackgroundShellId } from "./background-shell.js";
|
|
3
|
+
import { type BackgroundHardKillOutcome, type BackgroundShellCapability, type BackgroundShellId } from "./background-shell.js";
|
|
4
4
|
import type { WorkflowRunStore } from "./workflow-run-store.js";
|
|
5
5
|
import { type BackgroundAgentStore } from "./background-agent-store.js";
|
|
6
6
|
import type { TaskNotificationPayload } from "./task-notification.js";
|
|
@@ -38,6 +38,22 @@ export interface RegisterBackgroundBashInput extends TaskAccess {
|
|
|
38
38
|
* append at TaskStop/run-teardown (the tail drain), so the file is written even without a watcher —
|
|
39
39
|
* there is just no live mirror. Callers create the file (empty) before registering. */
|
|
40
40
|
outputFile?: string;
|
|
41
|
+
/** The LAUNCHING task's retain declaration (`TaskSpec.retainBackgroundProcesses`) — the task lane of
|
|
42
|
+
* {@link retainsBackgroundProcesses}. Carried onto the row as its `retained` mark at registration
|
|
43
|
+
* (together with the env lane), so faces that never see the spec — the session reap, the exit-boundary
|
|
44
|
+
* hard face — answer the same retain question the launch receipt answered. Passing it (true or false)
|
|
45
|
+
* STATES the lane: a later run teardown does not re-judge the row. Absent = the launcher does not know the
|
|
46
|
+
* lane; the owning run's teardown supplies it. */
|
|
47
|
+
retainBackgroundProcesses?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** One row's receipt from {@link TaskRegistry.killAllBackgroundShellsNow} (closed `outcome` set; `retained`
|
|
50
|
+
* present when the row was retained, whatever the outcome). */
|
|
51
|
+
export interface BackgroundShellHardKillReceipt {
|
|
52
|
+
taskId: string;
|
|
53
|
+
type: "background_bash" | "monitor";
|
|
54
|
+
owner?: string;
|
|
55
|
+
retained?: true;
|
|
56
|
+
outcome: BackgroundHardKillOutcome | "retained_skipped";
|
|
41
57
|
}
|
|
42
58
|
export interface TaskPollOptions {
|
|
43
59
|
workflowStore?: WorkflowRunStore;
|
|
@@ -371,7 +387,7 @@ export declare class TaskRegistry {
|
|
|
371
387
|
* file of a background bash row that (a) the caller can access (same owner/scope/session visibility as
|
|
372
388
|
* a TaskOutput poll — {@link canAccess}) and (b) was registered on the caller's OWN execution env —
|
|
373
389
|
* `env` identity is part of the match, same discipline as {@link retainedShellIds} /
|
|
374
|
-
* {@link
|
|
390
|
+
* {@link runningShellIds}. A canonical path only names a file within ONE env's filesystem
|
|
375
391
|
* namespace: on a shared registry, a row minted on env A must never exempt a same-named outside-root
|
|
376
392
|
* file in env B's namespace (same session or not), so instance equality — never a string proxy — is
|
|
377
393
|
* required. The output file is minted by the ENGINE itself (createTempFile
|
|
@@ -501,10 +517,10 @@ export declare class TaskRegistry {
|
|
|
501
517
|
* Aborts every running session-scoped background child registered under `sessionId` and settles it
|
|
502
518
|
* `killed` (the child's own catch/then would also classify the abort as killed; this makes the registry
|
|
503
519
|
* state immediate rather than waiting on the child's promise). Idempotent.
|
|
504
|
-
* RETAINED shell-lane rows
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
520
|
+
* RETAINED shell-lane rows (THE retain predicate over the row's mark and the env's declaration) are left
|
|
521
|
+
* whole — row, watcher and process (the deliver-a-living-service contract; their terminals: TaskStop, own
|
|
522
|
+
* exit, env destruction, the hard face under `includeRetained: true`). Every other running shell-lane row
|
|
523
|
+
* this session owns is killed and settled. {@link reapAllSessionBackground} delegates here.
|
|
508
524
|
* RB-375 A3 — RETURN-VALUE semantics: the count covers DIRECT hits of this sweep only (rows whose
|
|
509
525
|
* own arm above matched). Children the reaped agents themselves spawned (`sessionScoped:false`
|
|
510
526
|
* grandchildren) die COLLATERALLY, via each reaped agent's own teardown
|
|
@@ -522,11 +538,37 @@ export declare class TaskRegistry {
|
|
|
522
538
|
* pair — `owner` = the sessionId by design/129 discipline for background_bash/monitor/background_agent
|
|
523
539
|
* (RB-215 note: NOT for workflow, whose `owner` is the launching LEG's taskId; its session anchor is
|
|
524
540
|
* `originatingSessionId` — see {@link reapSessionBackground}'s own workflow arm for why) — and runs
|
|
525
|
-
* the per-session reap on each pair;
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
541
|
+
* the per-session reap on each pair; retained rows are left alone (row and process), exactly like the
|
|
542
|
+
* per-session anchor. Timing discipline: call at the hardShutdown TERMINUS, never at drain start — an
|
|
543
|
+
* in-flight turn may still be TaskOutput-following a bg process. Returns total rows reaped.
|
|
544
|
+
*
|
|
545
|
+
* Its kills go through each env's `killBackground` (possibly a TERM-then-timer ladder), and it reaches
|
|
546
|
+
* session-resident rows only: every exit terminus calls {@link killAllBackgroundShellsNow} after it. */
|
|
529
547
|
reapAllSessionBackground(): number;
|
|
548
|
+
/** The EXIT-BOUNDARY hard face over every shell-lane row (`background_bash` + `monitor`), session-resident or
|
|
549
|
+
* not (docs/background-process-lifetime.md is the contract). SYNCHRONOUS (no await, no timer) and
|
|
550
|
+
* IDEMPOTENT. Visits every row except `completed` (a `killed` row may sit inside a softer stop's grace
|
|
551
|
+
* window; a `failed` one may have lost its watcher, not its process). A retained row (THE retain predicate)
|
|
552
|
+
* is left alone under `includeRetained: false` — no default — unless an explicit stop already settled it
|
|
553
|
+
* `killed`. Every other visited row goes through the seat {@link BackgroundShellCapability.killBackgroundNow}.
|
|
554
|
+
* One receipt per visited row: `killed` (the seat delivered its kill now) / `already_gone` / `retained_skipped`
|
|
555
|
+
* / `no_pgid` (no synchronous kill landed — no seat, the seat said so or threw; the env's ordinary
|
|
556
|
+
* `killBackground` is then dispatched, nothing claimed). Only a running row the seat killed is settled
|
|
557
|
+
* `killed` (stop source "system", no notification). `reason` is forensics only.
|
|
558
|
+
* `sessionId` + `scope` narrow the visit to the rows {@link reapSessionBackground} (sessionId, scope) keys on
|
|
559
|
+
* — the same access (`canAccess` with owner = session = sessionId, in that scope): every session-resident
|
|
560
|
+
* row of the session, plus its run rows where the run's task id is the session id (the default); a run
|
|
561
|
+
* named with its own task id is not reached. Absent ⇒ the whole registry. Same receipts either way. */
|
|
562
|
+
killAllBackgroundShellsNow(opts: {
|
|
563
|
+
includeRetained: boolean;
|
|
564
|
+
reason?: string;
|
|
565
|
+
} & ({
|
|
566
|
+
sessionId?: undefined;
|
|
567
|
+
scope?: undefined;
|
|
568
|
+
} | {
|
|
569
|
+
sessionId: string;
|
|
570
|
+
scope: string;
|
|
571
|
+
})): BackgroundShellHardKillReceipt[];
|
|
530
572
|
/** subscribe to `reapSessionBackground` (the design/129 session terminal anchor).
|
|
531
573
|
* Returns an unsubscribe. Hooks are best-effort observers; a throwing hook is swallowed. */
|
|
532
574
|
onSessionReap(hook: (sessionId: string, scope?: string) => void): () => void;
|
|
@@ -568,29 +610,19 @@ export declare class TaskRegistry {
|
|
|
568
610
|
* completion notification at the terminal state. `unref()`d — a watcher never keeps the process alive.
|
|
569
611
|
* Stops itself on terminal, on a vanished shell (env disposed), or when the handle is evicted. */
|
|
570
612
|
private startBashWatcher;
|
|
613
|
+
/** RETAINED shells (THE retain predicate) still running on THIS env, any owner — the park-only sweep's
|
|
614
|
+
* spare list. Env identity is part of the match (a shared registry never exempts another env's ids). */
|
|
615
|
+
retainedShellIds(env: unknown): BackgroundShellId[];
|
|
616
|
+
/** Every shell-lane row still RUNNING on this env, any owner — the run teardown's dispose except-list.
|
|
617
|
+
* A run's teardown disposes only shells it can attribute to itself: its settle sweep has already killed
|
|
618
|
+
* and settled the rows of this run that end with it, so whatever registered row is still running here is
|
|
619
|
+
* either something that settle kept (session-resident, an in-window Monitor, retained) or belongs to
|
|
620
|
+
* another run or session sharing the env. Unregistered env shells are still disposed. */
|
|
621
|
+
runningShellIds(env: unknown): BackgroundShellId[];
|
|
571
622
|
/** design/135 G2: register a spawned background process as a MONITOR — the watcher polls at the batch
|
|
572
623
|
* window (200ms), turns each completed stdout line into an event, batches lines within one tick into
|
|
573
624
|
* ONE notification, enforces the non-persistent timeout, auto-stops an event storm, and fires exactly
|
|
574
625
|
* one terminal notification (exit code / timeout / auto-stop / env-side kill). */
|
|
575
|
-
/** The shells a run teardown must LEAVE ALIVE: running SESSION-scoped monitors of this
|
|
576
|
-
* session on THIS env (their whole point is outliving the turn; the env-wide dispose would otherwise
|
|
577
|
-
* orphan the watch — handle alive, process dead, watcher reporting a bogus env-death terminal). Env
|
|
578
|
-
* identity is part of the match so a shared registry never exempts another env's ids. */
|
|
579
|
-
sessionResidentShellIds(sessionId: string, env: unknown): BackgroundShellId[];
|
|
580
|
-
/** The OTHER shells a run teardown must leave alive: NON-persistent monitors still inside
|
|
581
|
-
* their timeout window, on THIS env. CC semantics: Monitor(persistent:false) lives to timeout_ms — the
|
|
582
|
-
* parent run ending is not an exit (timeout / process exit / TaskStop are). Deliberately NO owner
|
|
583
|
-
* filter: on a shared env an earlier run's (or another session's) in-window watch must survive this
|
|
584
|
-
* run's env-wide dispose too. Env identity stays part of the match, same as
|
|
585
|
-
* {@link sessionResidentShellIds} (a shared registry never exempts another env's ids). */
|
|
586
|
-
timeoutResidentShellIds(env: unknown): BackgroundShellId[];
|
|
587
|
-
/** The THIRD dispose except-list lane: RETAINED shells (the settle sweep's deliver-a-living-service
|
|
588
|
-
* mark) still running on THIS env. Deliberately NO owner filter, same as
|
|
589
|
-
* {@link timeoutResidentShellIds}: on a shared static env a LATER task's env-wide teardown dispose
|
|
590
|
-
* must not reap an EARLIER task's delivered daemon (that task's own teardown skipped the dispose;
|
|
591
|
-
* the next run's sweep was the remaining kill path). Env identity stays part of the match
|
|
592
|
-
* (a shared registry never exempts another env's ids). */
|
|
593
|
-
retainedShellIds(env: unknown): BackgroundShellId[];
|
|
594
626
|
registerMonitor(input: RegisterMonitorInput): string;
|
|
595
627
|
registerWorkflow(input: RegisterWorkflowInput): string;
|
|
596
628
|
pollTask(id: string, access: TaskAccess, opts?: TaskPollOptions): Promise<UnifiedTaskResult>;
|
|
@@ -616,6 +648,10 @@ export declare class TaskRegistry {
|
|
|
616
648
|
* explicit flag tells them apart. Bulk attribution should use {@link markStopSourceForOwner}, not a
|
|
617
649
|
* list()-driven loop (the 500-row cap). */
|
|
618
650
|
sessionScoped: boolean;
|
|
651
|
+
/** Present on a shell-lane row (`background_bash` / `monitor`) that THE retain predicate keeps alive
|
|
652
|
+
* past the automatic reaps — the "keeps running" half of an exit listing: the controlled-exit reap and
|
|
653
|
+
* the hard face under `includeRetained: false` leave it running; every other running shell row stops. */
|
|
654
|
+
retained?: true;
|
|
619
655
|
description?: string;
|
|
620
656
|
toolUseId?: string;
|
|
621
657
|
createdAt: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { defineTool, errorResult } from "./tools.js";
|
|
3
|
-
import { hasBackgroundShell } from "./background-shell.js";
|
|
3
|
+
import { envDeclaresRetain, hasBackgroundShell, retainsBackgroundProcesses } from "./background-shell.js";
|
|
4
4
|
import { shutdownDebug } from "./shutdown-debug.js";
|
|
5
5
|
import { canAccessAgentRecord, } from "./background-agent-store.js";
|
|
6
6
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
@@ -15,6 +15,9 @@ import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane,
|
|
|
15
15
|
import { toolFace } from "./tool-catalog-entries.js";
|
|
16
16
|
import { thrownText } from "./thrown-value.js";
|
|
17
17
|
export { canAccessWorkflowRun, clipTaskOutput, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE };
|
|
18
|
+
function shellRowRetained(h) {
|
|
19
|
+
return retainsBackgroundProcesses({ marked: h.retained === true, env: envDeclaresRetain(h.env) });
|
|
20
|
+
}
|
|
18
21
|
const BLOCK_DEFAULT_TIMEOUT_MS = 30_000;
|
|
19
22
|
const BLOCK_MAX_TIMEOUT_MS = 600_000;
|
|
20
23
|
function blockDeadline(timeoutMs, now) {
|
|
@@ -276,6 +279,8 @@ export class TaskRegistry {
|
|
|
276
279
|
shellId: input.shellId,
|
|
277
280
|
...(input.onTerminal !== undefined ? { onTerminal: input.onTerminal } : {}),
|
|
278
281
|
...(input.outputFile !== undefined ? { outputFile: input.outputFile } : {}),
|
|
282
|
+
...(retainsBackgroundProcesses({ env: envDeclaresRetain(input.env), task: input.retainBackgroundProcesses === true }) ? { retained: true } : {}),
|
|
283
|
+
...(input.retainBackgroundProcesses !== undefined ? { retainStatedAtLaunch: true } : {}),
|
|
279
284
|
});
|
|
280
285
|
if (input.onTerminal)
|
|
281
286
|
this.startBashWatcher(id, input.onTerminal);
|
|
@@ -396,7 +401,7 @@ export class TaskRegistry {
|
|
|
396
401
|
continue;
|
|
397
402
|
if (handle.type === "monitor" && monitorInTimeoutWindow(handle))
|
|
398
403
|
continue;
|
|
399
|
-
if ((handle.type === "background_bash" || handle.type === "monitor") && handle
|
|
404
|
+
if ((handle.type === "background_bash" || handle.type === "monitor") && shellRowRetained(handle))
|
|
400
405
|
continue;
|
|
401
406
|
const before = handle.stopSource;
|
|
402
407
|
this.markStopSource(handle.id, source);
|
|
@@ -425,7 +430,7 @@ export class TaskRegistry {
|
|
|
425
430
|
continue;
|
|
426
431
|
if (handle.status !== "running" || !canAccess(handle, access))
|
|
427
432
|
continue;
|
|
428
|
-
if ((handle.retained === true
|
|
433
|
+
if (retainsBackgroundProcesses({ marked: handle.retained === true, env: envDeclaresRetain(handle.env), task: opts?.retainProcesses === true && handle.retainStatedAtLaunch !== true, envDying: opts?.envDying === true })) {
|
|
429
434
|
handle.retained = true;
|
|
430
435
|
continue;
|
|
431
436
|
}
|
|
@@ -506,18 +511,16 @@ export class TaskRegistry {
|
|
|
506
511
|
const access = { owner: sessionId, sessionId, ...(scope !== undefined ? { scope } : {}) };
|
|
507
512
|
let reaped = 0;
|
|
508
513
|
for (const handle of this.handles.values()) {
|
|
509
|
-
if ((handle.type === "background_bash" || handle.type === "monitor") && handle.
|
|
514
|
+
if ((handle.type === "background_bash" || handle.type === "monitor") && handle.status === "running" && shellRowRetained(handle))
|
|
510
515
|
continue;
|
|
511
516
|
if (handle.type === "monitor" && handle.status === "running" && handle.sessionScoped && canAccess(handle, access)) {
|
|
512
|
-
shutdownDebug("session reap: monitor row", { id: handle.id, shellId: handle.shellId
|
|
517
|
+
shutdownDebug("session reap: monitor row", { id: handle.id, shellId: handle.shellId });
|
|
513
518
|
this.markStopSource(handle.id, "system");
|
|
514
519
|
if (handle.watcher !== undefined) {
|
|
515
520
|
handle.timers.clearInterval(handle.watcher);
|
|
516
521
|
handle.watcher = undefined;
|
|
517
522
|
}
|
|
518
|
-
|
|
519
|
-
void this.reportSessionReapKillFailure("monitor", handle);
|
|
520
|
-
}
|
|
523
|
+
void this.reportSessionReapKillFailure("monitor", handle);
|
|
521
524
|
handle.status = "killed";
|
|
522
525
|
handle.stoppedBy = handle.stopSource ?? "system";
|
|
523
526
|
handle.updatedAt = Date.now();
|
|
@@ -526,15 +529,13 @@ export class TaskRegistry {
|
|
|
526
529
|
continue;
|
|
527
530
|
}
|
|
528
531
|
if (handle.type === "background_bash" && handle.status === "running" && handle.sessionScoped && canAccess(handle, access)) {
|
|
529
|
-
shutdownDebug("session reap: background_bash row", { id: handle.id, shellId: handle.shellId
|
|
532
|
+
shutdownDebug("session reap: background_bash row", { id: handle.id, shellId: handle.shellId });
|
|
530
533
|
this.markStopSource(handle.id, "system");
|
|
531
534
|
if (handle.watcher !== undefined) {
|
|
532
535
|
clearInterval(handle.watcher);
|
|
533
536
|
handle.watcher = undefined;
|
|
534
537
|
}
|
|
535
|
-
|
|
536
|
-
void this.reportSessionReapKillFailure("background_bash", handle);
|
|
537
|
-
}
|
|
538
|
+
void this.reportSessionReapKillFailure("background_bash", handle);
|
|
538
539
|
handle.status = "killed";
|
|
539
540
|
handle.stoppedBy = handle.stopSource ?? "system";
|
|
540
541
|
handle.updatedAt = Date.now();
|
|
@@ -586,6 +587,63 @@ export class TaskRegistry {
|
|
|
586
587
|
reaped += this.reapSessionBackground(p.owner, p.scope);
|
|
587
588
|
return reaped;
|
|
588
589
|
}
|
|
590
|
+
killAllBackgroundShellsNow(opts) {
|
|
591
|
+
const sessionAccess = opts.sessionId !== undefined ? { owner: opts.sessionId, sessionId: opts.sessionId, scope: opts.scope } : undefined;
|
|
592
|
+
const receipts = [];
|
|
593
|
+
const settledOwners = new Set();
|
|
594
|
+
for (const handle of this.handles.values()) {
|
|
595
|
+
if (handle.type !== "background_bash" && handle.type !== "monitor")
|
|
596
|
+
continue;
|
|
597
|
+
if (sessionAccess !== undefined && !canAccess(handle, sessionAccess))
|
|
598
|
+
continue;
|
|
599
|
+
if (handle.status === "completed")
|
|
600
|
+
continue;
|
|
601
|
+
const retained = shellRowRetained(handle);
|
|
602
|
+
const base = { taskId: handle.id, type: handle.type, ...(handle.owner !== undefined ? { owner: handle.owner } : {}), ...(retained ? { retained: true } : {}) };
|
|
603
|
+
if (retained && !opts.includeRetained && handle.status !== "killed") {
|
|
604
|
+
receipts.push({ ...base, outcome: "retained_skipped" });
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
let outcome;
|
|
608
|
+
try {
|
|
609
|
+
outcome = handle.env.killBackgroundNow !== undefined ? handle.env.killBackgroundNow(handle.shellId) : "no_pgid";
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
shutdownDebug("hard kill: seat threw", { id: handle.id, shellId: handle.shellId, error: thrownText(err) });
|
|
613
|
+
outcome = "no_pgid";
|
|
614
|
+
}
|
|
615
|
+
if (outcome !== "killed" && outcome !== "already_gone" && outcome !== "no_pgid")
|
|
616
|
+
outcome = "no_pgid";
|
|
617
|
+
if (outcome === "no_pgid") {
|
|
618
|
+
try {
|
|
619
|
+
void handle.env.killBackground(handle.shellId).catch(() => undefined);
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
shutdownDebug("hard kill: row", { id: handle.id, shellId: handle.shellId, outcome, reason: opts.reason ?? "" });
|
|
625
|
+
if (handle.status === "running" && outcome === "killed") {
|
|
626
|
+
this.markStopSource(handle.id, "system");
|
|
627
|
+
if (handle.watcher !== undefined) {
|
|
628
|
+
if (handle.type === "monitor")
|
|
629
|
+
handle.timers.clearInterval(handle.watcher);
|
|
630
|
+
else
|
|
631
|
+
clearInterval(handle.watcher);
|
|
632
|
+
handle.watcher = undefined;
|
|
633
|
+
}
|
|
634
|
+
handle.status = "killed";
|
|
635
|
+
handle.stoppedBy = handle.stopSource ?? "system";
|
|
636
|
+
handle.updatedAt = Date.now();
|
|
637
|
+
mintCompletionId(handle);
|
|
638
|
+
if (handle.owner !== undefined)
|
|
639
|
+
settledOwners.add(handle.owner);
|
|
640
|
+
}
|
|
641
|
+
receipts.push({ ...base, outcome });
|
|
642
|
+
}
|
|
643
|
+
for (const owner of settledOwners)
|
|
644
|
+
this.pokeBgQuiescence(owner);
|
|
645
|
+
return receipts;
|
|
646
|
+
}
|
|
589
647
|
onSessionReap(hook) {
|
|
590
648
|
this.sessionReapHooks.add(hook);
|
|
591
649
|
return () => void this.sessionReapHooks.delete(hook);
|
|
@@ -595,7 +653,7 @@ export class TaskRegistry {
|
|
|
595
653
|
for (const handle of this.handles.values()) {
|
|
596
654
|
if (handle.type !== "background_bash" && handle.type !== "monitor" && handle.type !== "background_agent")
|
|
597
655
|
continue;
|
|
598
|
-
if (handle.type !== "background_agent" && handle
|
|
656
|
+
if (handle.type !== "background_agent" && shellRowRetained(handle))
|
|
599
657
|
continue;
|
|
600
658
|
if (handle.status === "running" && handle.owner === owner)
|
|
601
659
|
n++;
|
|
@@ -777,28 +835,19 @@ export class TaskRegistry {
|
|
|
777
835
|
handle.watcher = setInterval(() => void tick(), 1_000);
|
|
778
836
|
handle.watcher.unref?.();
|
|
779
837
|
}
|
|
780
|
-
|
|
781
|
-
const out = [];
|
|
782
|
-
for (const handle of this.handles.values()) {
|
|
783
|
-
if ((handle.type === "monitor" || handle.type === "background_bash") && handle.status === "running" && handle.sessionScoped === true && handle.owner === sessionId && handle.env === env) {
|
|
784
|
-
out.push(handle.shellId);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
return out;
|
|
788
|
-
}
|
|
789
|
-
timeoutResidentShellIds(env) {
|
|
838
|
+
retainedShellIds(env) {
|
|
790
839
|
const out = [];
|
|
791
840
|
for (const handle of this.handles.values()) {
|
|
792
|
-
if (handle.type === "monitor" && handle.
|
|
841
|
+
if ((handle.type === "background_bash" || handle.type === "monitor") && handle.status === "running" && shellRowRetained(handle) && handle.env === env) {
|
|
793
842
|
out.push(handle.shellId);
|
|
794
843
|
}
|
|
795
844
|
}
|
|
796
845
|
return out;
|
|
797
846
|
}
|
|
798
|
-
|
|
847
|
+
runningShellIds(env) {
|
|
799
848
|
const out = [];
|
|
800
849
|
for (const handle of this.handles.values()) {
|
|
801
|
-
if ((handle.type === "background_bash" || handle.type === "monitor") && handle.status === "running" && handle.
|
|
850
|
+
if ((handle.type === "background_bash" || handle.type === "monitor") && handle.status === "running" && handle.env === env) {
|
|
802
851
|
out.push(handle.shellId);
|
|
803
852
|
}
|
|
804
853
|
}
|
|
@@ -961,6 +1010,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
|
|
|
961
1010
|
type: h.type,
|
|
962
1011
|
status: h.status,
|
|
963
1012
|
sessionScoped: h.sessionScoped === true,
|
|
1013
|
+
...((h.type === "background_bash" || h.type === "monitor") && shellRowRetained(h) ? { retained: true } : {}),
|
|
964
1014
|
...(h.description !== undefined ? { description: h.description } : {}),
|
|
965
1015
|
...(h.toolUseId !== undefined ? { toolUseId: h.toolUseId } : {}),
|
|
966
1016
|
createdAt: h.createdAt,
|
|
@@ -1003,7 +1053,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
|
|
|
1003
1053
|
if (handle.type === "monitor" && !handle.sessionScoped && canAccess(handle, access)) {
|
|
1004
1054
|
if (monitorInTimeoutWindow(handle))
|
|
1005
1055
|
continue;
|
|
1006
|
-
if (handle
|
|
1056
|
+
if (shellRowRetained(handle) && handle.status === "running")
|
|
1007
1057
|
continue;
|
|
1008
1058
|
this.handles.delete(id);
|
|
1009
1059
|
if (handle.status === "running" && handle.owner !== undefined)
|
|
@@ -1019,7 +1069,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
|
|
|
1019
1069
|
continue;
|
|
1020
1070
|
if (handle.sessionScoped)
|
|
1021
1071
|
continue;
|
|
1022
|
-
if (handle
|
|
1072
|
+
if (shellRowRetained(handle) && handle.status === "running")
|
|
1023
1073
|
continue;
|
|
1024
1074
|
this.handles.delete(id);
|
|
1025
1075
|
if (handle.status === "running" && handle.owner !== undefined)
|
package/dist/core/task-spec.d.ts
CHANGED
|
@@ -1138,9 +1138,11 @@ export interface TaskSpec {
|
|
|
1138
1138
|
* retention there would be illusory, so the teardown keeps its honest killed receipts instead.
|
|
1139
1139
|
* - win32: best-effort "no active kill" only; without detached process groups, process-tree survival
|
|
1140
1140
|
* past the host is a platform boundary (some children may still exit with it).
|
|
1141
|
-
* -
|
|
1142
|
-
*
|
|
1143
|
-
*
|
|
1141
|
+
* - Automatic reaps leave a retained process alone: the run-teardown sweep (settle + dispose), the
|
|
1142
|
+
* session-release reap, and the exit-boundary hard face under `includeRetained: false`. What still ends
|
|
1143
|
+
* it: the TaskStop tool, its per-shell background timeout (while the host runs), env destruction, and the
|
|
1144
|
+
* hard face under `includeRetained: true`. The row carries the lane it was launched under (a later run's
|
|
1145
|
+
* declaration does not re-label a session-resident row) — see docs/background-process-lifetime.md.
|
|
1144
1146
|
* - NOT inherited by delegated child tasks: each child's own spec decides for the child's own
|
|
1145
1147
|
* background processes.
|
|
1146
1148
|
*/
|