@sema-agent/core 7.24.3 → 7.25.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 +101 -1
- package/dist/agents/peer-session-drain.d.ts +16 -1
- package/dist/agents/peer-session-drain.js +16 -1
- package/dist/agents/send-message-tool.d.ts +10 -2
- package/dist/agents/send-message-tool.js +18 -7
- package/dist/agents/subagent.js +13 -10
- package/dist/core/background-agent-store.d.ts +3 -3
- package/dist/core/checkpoint-store.d.ts +1 -0
- package/dist/core/delegation-frames.d.ts +5 -0
- package/dist/core/gate-fold.js +85 -0
- package/dist/core/gate-lanes.js +23 -1
- package/dist/core/gate-outcome.d.ts +8 -2
- package/dist/core/gate-outcome.js +3 -1
- package/dist/core/hooks.d.ts +29 -0
- package/dist/core/hooks.js +2 -0
- package/dist/core/mailbox-store.d.ts +5 -0
- package/dist/core/mailbox-store.js +1 -1
- package/dist/core/parent-spec-seat.d.ts +7 -1
- package/dist/core/parent-spec-seat.js +2 -0
- package/dist/core/permission-rule-consent.js +29 -47
- package/dist/core/permission-rule-provider.js +4 -3
- package/dist/core/permission-rule-store.d.ts +143 -7
- package/dist/core/permission-rule-store.js +165 -15
- package/dist/core/permission-rules.js +14 -10
- package/dist/core/rule-denial-sentence.d.ts +75 -0
- package/dist/core/rule-denial-sentence.js +16 -0
- package/dist/core/runner/contracts.d.ts +36 -2
- package/dist/core/runner/gate-exit.d.ts +14 -0
- package/dist/core/runner/gate-exit.js +4 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +4 -4
- package/dist/core/runner/prepare-delegation-surface.js +2 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
- package/dist/core/runner/prepare-gate-stations.js +7 -4
- package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
- package/dist/core/runner/prepare-hands-readface.js +25 -10
- package/dist/core/runner/prepare-inherited-gate.d.ts +23 -0
- package/dist/core/runner/prepare-inherited-gate.js +11 -6
- package/dist/core/runner/prepare-policy-chain.d.ts +15 -1
- package/dist/core/runner/prepare-policy-chain.js +42 -22
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -2
- package/dist/core/runner/prepare-suspend-saga.js +4 -3
- package/dist/core/runner/prepare-task.js +10 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +11 -4
- package/dist/core/runner/prepare-wiring-manifest.js +8 -4
- package/dist/core/runner/resume-apply.js +2 -0
- package/dist/core/runner/run-harness-handlers.js +1 -0
- package/dist/core/runner/run-identity-wiring.d.ts +2 -2
- package/dist/core/runner/run-leg.js +28 -1
- package/dist/core/runner/run-notification-lane.d.ts +2 -2
- package/dist/core/runner/run-notification-lane.js +2 -1
- package/dist/core/runner/run-settle-and-teardown.js +1 -0
- package/dist/core/runner/session-rule-policy.d.ts +73 -2
- package/dist/core/runner/session-rule-policy.js +121 -21
- package/dist/core/session-policy-store.d.ts +27 -3
- package/dist/core/session-policy-store.js +12 -4
- package/dist/core/store-contracts/background-agent-store-contract.js +10 -0
- package/dist/core/store-contracts/peer-directory-contract.d.ts +23 -0
- package/dist/core/store-contracts/peer-directory-contract.js +82 -0
- package/dist/core/task-event.d.ts +6 -0
- package/dist/core/task-notification.d.ts +79 -4
- package/dist/core/task-notification.js +90 -1
- package/dist/core/task-registry-agent.js +1 -2
- package/dist/core/task-registry-monitor.js +2 -1
- package/dist/core/task-registry-shared.d.ts +53 -5
- package/dist/core/task-registry-shared.js +13 -2
- package/dist/core/task-registry.js +8 -5
- package/dist/core/task-spec.d.ts +12 -5
- package/dist/core/tool-catalog-entries.js +1 -0
- package/dist/core/tool-spec.d.ts +1 -1
- package/dist/core/wiring-manifest.d.ts +2 -2
- package/dist/engine/loop/agent-loop.js +1 -1
- package/dist/engine/loop/types.d.ts +4 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -3
- package/dist/orchestration/workflow.js +12 -3
- package/dist/prompts/coordinator.d.ts +2 -1
- package/dist/stores/file/adoption/adopt.js +23 -1
- package/dist/stores/file/permission-rule-store.js +3 -1
- package/dist/stores/file/session-policy-store.d.ts +31 -0
- package/dist/stores/file/session-policy-store.js +87 -7
- package/dist/tools/fs/bash-program-position.d.ts +4 -26
- package/dist/tools/fs/bash-program-position.js +0 -11
- package/dist/tools/fs/bash-readonly-classifier.d.ts +20 -309
- package/dist/tools/fs/bash-readonly-classifier.js +28 -766
- package/dist/tools/fs/fs-bash.d.ts +43 -207
- package/dist/tools/fs/fs-bash.js +20 -341
- package/dist/tools/fs/index.d.ts +12 -13
- package/dist/tools/fs/index.js +3 -4
- package/dist/tools/fs/read-deny.d.ts +25 -0
- package/dist/tools/fs/read-deny.js +19 -0
- package/dist/tools/fs/safety.js +2 -6
- package/dist/tools/fs/shell-read-check.d.ts +99 -0
- package/dist/tools/fs/shell-read-check.js +229 -0
- package/dist/tools/fs/shell-read-establish.d.ts +70 -115
- package/dist/tools/fs/shell-read-establish.js +216 -102
- package/dist/tools/fs/shell-read-judge.d.ts +231 -0
- package/dist/tools/fs/shell-read-judge.js +170 -0
- package/dist/tools/fs/shell-read-plan.d.ts +3 -0
- package/dist/tools/fs/shell-read-plan.js +82 -19
- package/dist/tools/monitor.d.ts +5 -5
- package/dist/tools/monitor.js +3 -5
- package/dist/tools/task-list.d.ts +4 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +19 -3
- package/wire-changes/core@7.24.4.json +54 -0
- package/wire-changes/core@7.25.0.json +671 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { resolvePeerSessions } from "../../agents/peer-directory.js";
|
|
3
|
+
import { beginContract } from "./contract-harness.js";
|
|
4
|
+
export const PEER_DIRECTORY_CONTRACT_SCOPE = "peer-kit-A";
|
|
5
|
+
function row(name, sessionId, over = {}) {
|
|
6
|
+
return { schemaVersion: 1, sessionId, scope: PEER_DIRECTORY_CONTRACT_SCOPE, name, startedAt: 1_000, updatedAt: 2_000, liveness: "live", ...over };
|
|
7
|
+
}
|
|
8
|
+
async function withDirs(mk, fn) {
|
|
9
|
+
const live = [];
|
|
10
|
+
const make = () => {
|
|
11
|
+
const d = mk();
|
|
12
|
+
live.push(d);
|
|
13
|
+
return d;
|
|
14
|
+
};
|
|
15
|
+
try {
|
|
16
|
+
await fn(make);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
for (const d of live.splice(0))
|
|
20
|
+
d.close?.();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function peerDirectoryContract(mk, hooks) {
|
|
24
|
+
const { run: runRaw, settle } = beginContract(hooks.runAssertion);
|
|
25
|
+
const run = (name, fn) => runRaw(name, () => withDirs(mk, fn));
|
|
26
|
+
const S = PEER_DIRECTORY_CONTRACT_SCOPE;
|
|
27
|
+
run("scope wall: a listing is the accessor's scope only", async (make) => {
|
|
28
|
+
const d = make();
|
|
29
|
+
await hooks.write(d, row("alpha", "sess-a"));
|
|
30
|
+
await hooks.write(d, row("beta", "sess-b", { scope: "peer-kit-B" }));
|
|
31
|
+
const inA = await d.listPeerSessions({ scope: S });
|
|
32
|
+
assert.deepEqual(inA.map((r) => r.sessionId), ["sess-a"], "scope A must list exactly its own row");
|
|
33
|
+
const inB = await d.listPeerSessions({ scope: "peer-kit-B" });
|
|
34
|
+
assert.deepEqual(inB.map((r) => r.sessionId), ["sess-b"]);
|
|
35
|
+
const inC = await d.listPeerSessions({ scope: "peer-kit-C" });
|
|
36
|
+
assert.equal(inC.length, 0, "an unrelated scope sees nothing");
|
|
37
|
+
});
|
|
38
|
+
run("the full set is listed (live + dead + deleted) and every listed row is a copy", async (make) => {
|
|
39
|
+
const d = make();
|
|
40
|
+
await hooks.write(d, row("alpha", "sess-a"));
|
|
41
|
+
await hooks.write(d, row("beta", "sess-b", { liveness: "dead", diedAt: 3_000 }));
|
|
42
|
+
await hooks.write(d, row("gamma", "sess-c", { liveness: "deleted", diedAt: 3_000 }));
|
|
43
|
+
const listed = await d.listPeerSessions({ scope: S });
|
|
44
|
+
assert.deepEqual([...listed.map((r) => r.sessionId)].sort(), ["sess-a", "sess-b", "sess-c"], "dead and deleted rows are MARKED, never dropped from a listing");
|
|
45
|
+
assert.deepEqual(Object.fromEntries(listed.map((r) => [r.sessionId, r.liveness])), { "sess-a": "live", "sess-b": "dead", "sess-c": "deleted" });
|
|
46
|
+
const first = listed.find((r) => r.sessionId === "sess-a");
|
|
47
|
+
first.name = "scribbled";
|
|
48
|
+
const again = await d.listPeerSessions({ scope: S });
|
|
49
|
+
assert.equal(again.find((r) => r.sessionId === "sess-a").name, "alpha", "a listed row is a copy");
|
|
50
|
+
});
|
|
51
|
+
run("the engine's resolver over the listing: live by name, dead still found, deleted refuses", async (make) => {
|
|
52
|
+
const d = make();
|
|
53
|
+
await hooks.write(d, row("alpha", "sess-a"));
|
|
54
|
+
await hooks.write(d, row("beta", "sess-b", { liveness: "dead", diedAt: 3_000 }));
|
|
55
|
+
await hooks.write(d, row("gamma", "sess-c", { liveness: "deleted", diedAt: 3_000 }));
|
|
56
|
+
const rows = await d.listPeerSessions({ scope: S });
|
|
57
|
+
const alpha = resolvePeerSessions(rows, "alpha");
|
|
58
|
+
assert.equal(alpha.status, "found");
|
|
59
|
+
const beta = resolvePeerSessions(rows, "beta");
|
|
60
|
+
assert.equal(beta.status, "found", "a dead row is the name truth offline delivery resolves through");
|
|
61
|
+
const gamma = resolvePeerSessions(rows, "gamma");
|
|
62
|
+
assert.equal(gamma.status, "deleted", "a tombstone resolves only to refuse");
|
|
63
|
+
assert.equal(resolvePeerSessions(rows, "session.sess-b").status, "found", "an address resolves to a KNOWN row");
|
|
64
|
+
assert.equal(resolvePeerSessions(rows, "session.sess-zzz").status, "not_found", "an address nobody registered is not an append target");
|
|
65
|
+
});
|
|
66
|
+
run("restorePeerSession (where implemented) is CAS-on-absent: restores an absent row, never overwrites a present one", async (make) => {
|
|
67
|
+
const d = make();
|
|
68
|
+
if (d.restorePeerSession === undefined)
|
|
69
|
+
return;
|
|
70
|
+
const restored = await d.restorePeerSession(row("alpha", "sess-a", { liveness: "dead", diedAt: 3_000 }));
|
|
71
|
+
assert.equal(restored, true, "an absent row is restored");
|
|
72
|
+
assert.deepEqual((await d.listPeerSessions({ scope: S })).map((r) => r.sessionId), ["sess-a"]);
|
|
73
|
+
await hooks.write(d, row("alpha", "sess-a", { updatedAt: 9_000 }));
|
|
74
|
+
const again = await d.restorePeerSession(row("alpha-stale", "sess-a", { liveness: "dead", diedAt: 3_000 }));
|
|
75
|
+
assert.equal(again, false, "a present row wins over a restore");
|
|
76
|
+
const current = (await d.listPeerSessions({ scope: S })).find((r) => r.sessionId === "sess-a");
|
|
77
|
+
assert.equal(current.name, "alpha");
|
|
78
|
+
assert.equal(current.liveness, "live");
|
|
79
|
+
assert.equal(current.updatedAt, 9_000, "the existing row is byte-unchanged by a losing restore");
|
|
80
|
+
});
|
|
81
|
+
await settle();
|
|
82
|
+
}
|
|
@@ -883,6 +883,12 @@ export type TaskEvent = ({
|
|
|
883
883
|
* nor an agent-type — the child then keeps its taskId (NOT the raw objective, which could leak a delegated
|
|
884
884
|
* secret to a progress-scoped consumer — dual-review Q2). Sanitized (control-char-stripped, length-capped). */
|
|
885
885
|
name?: string;
|
|
886
|
+
/** The EXPLICIT `Agent({name})` spawn identity of the sub-run, when the spawn carried one — the
|
|
887
|
+
* teammate KEY, as opposed to {@link name} (the display label): the same string the fleet row's
|
|
888
|
+
* spawn frame (`BackgroundChildEvent.spawnName`), the roster row and the child's uplink `from`
|
|
889
|
+
* carry, so a consumer binds "@name → row" by string equality. Validated at spawn
|
|
890
|
+
* (`^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$`). ABSENT = an unnamed delegation, never a missing value. */
|
|
891
|
+
spawnName?: string;
|
|
886
892
|
/**
|
|
887
893
|
* The model this sub-run was PREPARED with — the resolved id, read off the leg's prepared model
|
|
888
894
|
* at the mint, so it is the same value `TaskResult.model` and the `task.start` trace frame carry.
|
|
@@ -1,7 +1,25 @@
|
|
|
1
|
+
import type { MailboxAppendMessage, MailboxStore } from "./mailbox-store.js";
|
|
1
2
|
/** design/135 G2: `"event"` is the ONE non-terminal member — a Monitor batch notification (new stdout
|
|
2
3
|
* lines from a still-running watch). Everything else is a terminal completion notification. */
|
|
3
4
|
export type TaskNotificationStatus = "completed" | "failed" | "killed" | "cancelled" | "event";
|
|
4
5
|
export type SystemInjectionPriority = "now" | "next" | "later";
|
|
6
|
+
/**
|
|
7
|
+
* The outcome an injection entry settles with — what the session did with the frame:
|
|
8
|
+
* · `queued` — a live lane took it (it rides that run's next turn boundary; accepted, not yet consumed);
|
|
9
|
+
* · `parked` — no live lane; pended IN PROCESS on {@link PendingSessionNotifications} for the
|
|
10
|
+
* session's next run in this process (a process restart loses it);
|
|
11
|
+
* · `parked_durably` — no live lane; appended to the session's DURABLE box
|
|
12
|
+
* ({@link PendingSessionNotifications.bindSessionBox}) — the next run drains it, in any process;
|
|
13
|
+
* · `dropped_duplicate` — the session already delivered this exact frame.
|
|
14
|
+
* The park backend is the seat's choice ({@link routeOrParkNotification}), never the producer's.
|
|
15
|
+
*/
|
|
16
|
+
export type NotificationInjectionOutcome = "queued" | "parked" | "parked_durably" | "dropped_duplicate";
|
|
17
|
+
export declare const NOTIFICATION_INJECTION_OUTCOMES: readonly ["queued", "parked", "parked_durably", "dropped_duplicate"];
|
|
18
|
+
/** Is `value` one of the injector's outcome words? A host-wired uplink may answer anything (a bare
|
|
19
|
+
* `void`, an array's `push` count) — such an answer reads as "outcome not reported", never as a word. */
|
|
20
|
+
export declare function isNotificationInjectionOutcome(value: unknown): value is NotificationInjectionOutcome;
|
|
21
|
+
/** The disposition a park/route decision reports back — the injection outcome minus the dedup fold. */
|
|
22
|
+
export type NotificationDisposition = Exclude<NotificationInjectionOutcome, "dropped_duplicate">;
|
|
5
23
|
/** The closed value set of {@link SystemInjectionPriority}, as data — the wire-facing entries validate
|
|
6
24
|
* against THIS rather than against three inline string literals, so widening the type widens the guard.
|
|
7
25
|
* (Names taken verbatim from CC 2.1.223's `UYo` @ `CC:644918`; see the semantics note on
|
|
@@ -135,6 +153,12 @@ export interface TaskNotificationPayload {
|
|
|
135
153
|
* outside caller can neither mint a chain nor wear the peer class. */
|
|
136
154
|
peer?: {
|
|
137
155
|
hopChain: string[];
|
|
156
|
+
/** The SENDING run's session id, when the producer knows it — the reply/receipt address a durable
|
|
157
|
+
* park of this frame records (`MailboxPeerMeta.fromSession`). Engine-read; never rendered. */
|
|
158
|
+
fromSession?: string;
|
|
159
|
+
/** The sender's admission key (the same token the send-point gate charged), so a durable park
|
|
160
|
+
* re-judges the frame under the same sender at the drain. Engine-read; never rendered. */
|
|
161
|
+
senderKey?: string;
|
|
138
162
|
};
|
|
139
163
|
/**
|
|
140
164
|
* design/385 §1.4 d1 — the delegated-child → parent UPLINK carrier (`SendMessage("main")`). Its
|
|
@@ -264,7 +288,7 @@ export interface SystemInjection<TPayload = unknown> {
|
|
|
264
288
|
* listener: "queued" once the boundary steer accepted, "parked" when the idle tail-race (or the
|
|
265
289
|
* engine-note backlog cap) rejected and the payload was pended per session. Absent =
|
|
266
290
|
* fire-and-forget producer (no receipt to honor). */
|
|
267
|
-
onDisposition?: (d:
|
|
291
|
+
onDisposition?: (d: NotificationDisposition) => void;
|
|
268
292
|
}
|
|
269
293
|
/** design/373 (adversarial r3, recorded rule): the key deliberately EXCLUDES the injection tier.
|
|
270
294
|
* The key names an EVENT's identity; `priority` is delivery metadata about one submission of it.
|
|
@@ -356,6 +380,27 @@ export declare function renderTaskNotificationXml(n: TaskNotificationPayload): s
|
|
|
356
380
|
export declare const MAX_PENDING_EVENTS_PER_TASK = 50;
|
|
357
381
|
export declare const MAX_PENDING_EVENTS_PER_SESSION = 200;
|
|
358
382
|
export declare const MAX_PENDING_SESSIONS = 100;
|
|
383
|
+
/** Session-box bindings are three references and a handle; bounded far above the parked-session cap so
|
|
384
|
+
* a long-lived leader's binding is not evicted by parked traffic (eviction = the park backend degrades
|
|
385
|
+
* to in-process, which the receipt then says). */
|
|
386
|
+
export declare const MAX_SESSION_BOX_BINDINGS = 4096;
|
|
387
|
+
/**
|
|
388
|
+
* A session's DURABLE box, bound to the park seat by the leg that mounts its session-box drain — one
|
|
389
|
+
* mint (the drain mount judgment), two readers (the drain and {@link routeOrParkNotification}), so
|
|
390
|
+
* "a frame parked here is drained there" holds by construction. Absent for a session whose legs mount
|
|
391
|
+
* no drain (a delegated child, an ungrammatical id, no cross-process-safe mailbox).
|
|
392
|
+
*/
|
|
393
|
+
export interface SessionBoxBinding {
|
|
394
|
+
mailbox: MailboxStore;
|
|
395
|
+
/** The registry/mailbox scope the session's legs mount in (the drain reads the same scope). */
|
|
396
|
+
scope: string;
|
|
397
|
+
/** The box handle (`session.<id>`), minted after the address grammar passed. */
|
|
398
|
+
handle: string;
|
|
399
|
+
/** The session id the box belongs to — the `fromRoot` a durably parked child frame records. */
|
|
400
|
+
sessionId: string;
|
|
401
|
+
/** Where an append fault is disclosed (the frame then takes the in-process park — never lost). */
|
|
402
|
+
onError?: (e: unknown) => void;
|
|
403
|
+
}
|
|
359
404
|
/**
|
|
360
405
|
* RB-375 A2 (ruled 2026-08-03) — is this frame the TERMINAL of a delegated background agent?
|
|
361
406
|
*
|
|
@@ -423,7 +468,7 @@ export interface DrainedPendingNotifications {
|
|
|
423
468
|
* · `"unavailable"` — the lane can no longer take anything; the caller tries the next lane, and
|
|
424
469
|
* finally the parked store.
|
|
425
470
|
*/
|
|
426
|
-
export type LiveNotificationLane = (payload: TaskNotificationPayload, priority: SystemInjectionPriority, onDisposition?: (d:
|
|
471
|
+
export type LiveNotificationLane = (payload: TaskNotificationPayload, priority: SystemInjectionPriority, onDisposition?: (d: NotificationDisposition) => void) => "taken" | "pending" | "duplicate" | "unavailable";
|
|
427
472
|
/** One run's seat: its two entry points, plus the two bookkeeping verbs its owner drives. */
|
|
428
473
|
export interface LiveNotificationLaneSeat {
|
|
429
474
|
/**
|
|
@@ -500,7 +545,7 @@ export declare class SessionLiveNotificationLanes {
|
|
|
500
545
|
* special-cased: a lane that just refused this frame reports `"unavailable"` for it from then on.
|
|
501
546
|
*/
|
|
502
547
|
deliver(sessionId: string, payload: TaskNotificationPayload, priority: SystemInjectionPriority, opts?: {
|
|
503
|
-
onDisposition?: (d:
|
|
548
|
+
onDisposition?: (d: NotificationDisposition) => void;
|
|
504
549
|
}): "taken" | "pending" | "duplicate" | "unavailable";
|
|
505
550
|
/** Sessions holding at least one live lane (observability + the tests' rollback detector). */
|
|
506
551
|
get size(): number;
|
|
@@ -528,6 +573,16 @@ export declare class PendingSessionNotifications {
|
|
|
528
573
|
/** Whole-session losses whose tombstone was itself evicted — the count survives, the attribution
|
|
529
574
|
* does not. Surfaced on the next session-level disclosure so it is never simply forgotten. */
|
|
530
575
|
private unattributedDrops;
|
|
576
|
+
/** The durable park backend per session ({@link SessionBoxBinding}); insertion-ordered, bounded. */
|
|
577
|
+
private readonly sessionBoxes;
|
|
578
|
+
/**
|
|
579
|
+
* Bind (or, with `undefined`, unbind) the session's durable box. Written at EVERY drain mount of a
|
|
580
|
+
* session's leg, so the binding tracks the deployment's current seats (a mailbox swapped out
|
|
581
|
+
* unbinds at the next mount). A re-bind refreshes the entry's LRU position.
|
|
582
|
+
*/
|
|
583
|
+
bindSessionBox(sessionId: string, box: SessionBoxBinding | undefined): void;
|
|
584
|
+
/** The session's durable box, when a leg of it mounted the drain in this process. */
|
|
585
|
+
sessionBoxFor(sessionId: string): SessionBoxBinding | undefined;
|
|
531
586
|
pend(sessionId: string, n: TaskNotificationPayload, priority?: SystemInjectionPriority): void;
|
|
532
587
|
/** Remove and return the session's pendings (one-shot — the next run consumes them exactly once).
|
|
533
588
|
* design/373 §3.5: the batch comes out PRIORITY-MAJOR (stable sort — pend order within a class;
|
|
@@ -548,6 +603,13 @@ export declare class PendingSessionNotifications {
|
|
|
548
603
|
* next drain. Callers ask BEFORE routing and BEFORE parking.
|
|
549
604
|
*/
|
|
550
605
|
holdsKey(sessionId: string, dedupKey: string): boolean;
|
|
606
|
+
/** Keys the session's DURABLE box took ({@link routeOrParkNotification}'s durable arm) — the third holder
|
|
607
|
+
* {@link holdsKey} consults, so a retry of a frame the box already holds folds exactly as one the
|
|
608
|
+
* in-process store holds. Bounded like {@link routed}; a key is released when the append faulted and
|
|
609
|
+
* the frame took the in-process park instead (that store then owns it). */
|
|
610
|
+
private readonly durable;
|
|
611
|
+
noteDurablePark(sessionId: string, dedupKey: string): void;
|
|
612
|
+
forgetDurablePark(sessionId: string, dedupKey: string): void;
|
|
551
613
|
/** Record that this session has DELIVERED this frame — see {@link routed}. Written at ONE place, the
|
|
552
614
|
* harness's consumption sink (the step where a queued frame enters the model's input), so the record
|
|
553
615
|
* can never run ahead of the delivery it claims. */
|
|
@@ -565,7 +627,20 @@ export declare class PendingSessionNotifications {
|
|
|
565
627
|
* ({@link LiveNotificationLaneSeat.markDelivered});
|
|
566
628
|
* · nothing can take it ⇒ park, carrying the tier, for the session's next run.
|
|
567
629
|
*/
|
|
568
|
-
export declare function routeOrParkNotification(store: PendingSessionNotifications, sessionId: string, payload: TaskNotificationPayload, priority: SystemInjectionPriority, onDisposition?: (d:
|
|
630
|
+
export declare function routeOrParkNotification(store: PendingSessionNotifications, sessionId: string, payload: TaskNotificationPayload, priority: SystemInjectionPriority, onDisposition?: (d: NotificationDisposition) => void): void;
|
|
631
|
+
/**
|
|
632
|
+
* The durable form of a parked peer-words frame: the SAME `peer_message` record the cross-session lane
|
|
633
|
+
* appends (the recipient's drain renders it through the one envelope and judges it through the one
|
|
634
|
+
* admission gate), with `fromRoot` = the box owner's own session — a frame reaches this seat only
|
|
635
|
+
* through the injector its owner handed to a child of its tree, so the same-tree fact is the seat's to
|
|
636
|
+
* state, not the producer's to claim.
|
|
637
|
+
*/
|
|
638
|
+
export declare function sessionBoxRecordOf(payload: TaskNotificationPayload & {
|
|
639
|
+
agentMessage: {
|
|
640
|
+
from: string;
|
|
641
|
+
body: string;
|
|
642
|
+
};
|
|
643
|
+
}, box: Pick<SessionBoxBinding, "sessionId">): MailboxAppendMessage;
|
|
569
644
|
/**
|
|
570
645
|
* Delivery-side overflow disclosure: fold the per-task drop counts into the drained payloads. Two
|
|
571
646
|
* carrier shapes, chosen per survivor:
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { escapeEnvelopeTag, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
2
2
|
import { PEER_MESSAGE_NOTICE } from "../agents/peer-admission.js";
|
|
3
3
|
import { buildCrossSessionEnvelope, CROSS_SESSION_MESSAGE_NOTICE, neutralizePeerBody } from "../agents/cross-session-envelope.js";
|
|
4
|
+
export const NOTIFICATION_INJECTION_OUTCOMES = ["queued", "parked", "parked_durably", "dropped_duplicate"];
|
|
5
|
+
export function isNotificationInjectionOutcome(value) {
|
|
6
|
+
return typeof value === "string" && NOTIFICATION_INJECTION_OUTCOMES.includes(value);
|
|
7
|
+
}
|
|
4
8
|
export const SYSTEM_INJECTION_PRIORITIES = ["now", "next", "later"];
|
|
5
9
|
export function isSystemInjectionPriority(value) {
|
|
6
10
|
return typeof value === "string" && SYSTEM_INJECTION_PRIORITIES.includes(value);
|
|
@@ -107,6 +111,7 @@ const MAX_DEDUP_KEYS = 2_000;
|
|
|
107
111
|
export const MAX_PENDING_EVENTS_PER_TASK = 50;
|
|
108
112
|
export const MAX_PENDING_EVENTS_PER_SESSION = 200;
|
|
109
113
|
export const MAX_PENDING_SESSIONS = 100;
|
|
114
|
+
export const MAX_SESSION_BOX_BINDINGS = 4096;
|
|
110
115
|
const MAX_TOMBSTONES = 5_000;
|
|
111
116
|
const TERMINAL_STATUSES = new Set(["completed", "failed", "killed", "cancelled"]);
|
|
112
117
|
export function isDelegatedAgentTerminal(n) {
|
|
@@ -186,6 +191,22 @@ export class PendingSessionNotifications {
|
|
|
186
191
|
droppedSessions = 0;
|
|
187
192
|
evictedSessions = new Map();
|
|
188
193
|
unattributedDrops = 0;
|
|
194
|
+
sessionBoxes = new Map();
|
|
195
|
+
bindSessionBox(sessionId, box) {
|
|
196
|
+
this.sessionBoxes.delete(sessionId);
|
|
197
|
+
if (box === undefined)
|
|
198
|
+
return;
|
|
199
|
+
this.sessionBoxes.set(sessionId, box);
|
|
200
|
+
while (this.sessionBoxes.size > MAX_SESSION_BOX_BINDINGS) {
|
|
201
|
+
const oldest = this.sessionBoxes.keys().next().value;
|
|
202
|
+
if (oldest === undefined)
|
|
203
|
+
break;
|
|
204
|
+
this.sessionBoxes.delete(oldest);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
sessionBoxFor(sessionId) {
|
|
208
|
+
return this.sessionBoxes.get(sessionId);
|
|
209
|
+
}
|
|
189
210
|
pend(sessionId, n, priority) {
|
|
190
211
|
let s = this.sessions.get(sessionId);
|
|
191
212
|
if (s === undefined) {
|
|
@@ -251,7 +272,36 @@ export class PendingSessionNotifications {
|
|
|
251
272
|
}
|
|
252
273
|
routed = new Map();
|
|
253
274
|
holdsKey(sessionId, dedupKey) {
|
|
254
|
-
return this.sessions.get(sessionId)?.keys.has(dedupKey) === true || this.routed.get(sessionId)?.has(dedupKey) === true;
|
|
275
|
+
return this.sessions.get(sessionId)?.keys.has(dedupKey) === true || this.routed.get(sessionId)?.has(dedupKey) === true || this.durable.get(sessionId)?.has(dedupKey) === true;
|
|
276
|
+
}
|
|
277
|
+
durable = new Map();
|
|
278
|
+
noteDurablePark(sessionId, dedupKey) {
|
|
279
|
+
let mine = this.durable.get(sessionId);
|
|
280
|
+
if (mine === undefined) {
|
|
281
|
+
if (this.durable.size >= MAX_PENDING_SESSIONS) {
|
|
282
|
+
const oldest = this.durable.keys().next().value;
|
|
283
|
+
if (oldest !== undefined)
|
|
284
|
+
this.durable.delete(oldest);
|
|
285
|
+
}
|
|
286
|
+
mine = new Set();
|
|
287
|
+
this.durable.set(sessionId, mine);
|
|
288
|
+
}
|
|
289
|
+
mine.add(dedupKey);
|
|
290
|
+
if (mine.size > MAX_PENDING_EVENTS_PER_SESSION) {
|
|
291
|
+
for (const k of mine) {
|
|
292
|
+
mine.delete(k);
|
|
293
|
+
if (mine.size <= MAX_PENDING_EVENTS_PER_SESSION)
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
forgetDurablePark(sessionId, dedupKey) {
|
|
299
|
+
const mine = this.durable.get(sessionId);
|
|
300
|
+
if (mine === undefined)
|
|
301
|
+
return;
|
|
302
|
+
mine.delete(dedupKey);
|
|
303
|
+
if (mine.size === 0)
|
|
304
|
+
this.durable.delete(sessionId);
|
|
255
305
|
}
|
|
256
306
|
markDelivered(sessionId, dedupKey) {
|
|
257
307
|
let mine = this.routed.get(sessionId);
|
|
@@ -285,6 +335,30 @@ export function routeOrParkNotification(store, sessionId, payload, priority, onD
|
|
|
285
335
|
}
|
|
286
336
|
const routed = store.liveLanes.deliver(sessionId, payload, priority, { ...(onDisposition !== undefined ? { onDisposition } : {}) });
|
|
287
337
|
if (routed === "unavailable") {
|
|
338
|
+
const box = store.sessionBoxFor(sessionId);
|
|
339
|
+
const carrier = payload.agentMessage;
|
|
340
|
+
if (box !== undefined && carrier !== undefined) {
|
|
341
|
+
store.noteDurablePark(sessionId, dedupKey);
|
|
342
|
+
const record = sessionBoxRecordOf({ ...payload, agentMessage: carrier }, box);
|
|
343
|
+
let appended;
|
|
344
|
+
try {
|
|
345
|
+
appended = Promise.resolve(box.mailbox.append(box.scope, box.handle, record));
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
appended = Promise.reject(e);
|
|
349
|
+
}
|
|
350
|
+
appended.then(() => onDisposition?.("parked_durably"), (e) => {
|
|
351
|
+
store.forgetDurablePark(sessionId, dedupKey);
|
|
352
|
+
try {
|
|
353
|
+
box.onError?.(e);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
}
|
|
357
|
+
store.pend(sessionId, payload, priority);
|
|
358
|
+
onDisposition?.("parked");
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
288
362
|
store.pend(sessionId, payload, priority);
|
|
289
363
|
onDisposition?.("parked");
|
|
290
364
|
}
|
|
@@ -292,6 +366,21 @@ export function routeOrParkNotification(store, sessionId, payload, priority, onD
|
|
|
292
366
|
onDisposition?.("queued");
|
|
293
367
|
}
|
|
294
368
|
}
|
|
369
|
+
export function sessionBoxRecordOf(payload, box) {
|
|
370
|
+
const peer = payload.peer;
|
|
371
|
+
return {
|
|
372
|
+
from: payload.agentMessage.from,
|
|
373
|
+
content: payload.agentMessage.body,
|
|
374
|
+
sentAt: Date.now(),
|
|
375
|
+
hopChain: peer !== undefined ? [...peer.hopChain] : [],
|
|
376
|
+
peerMeta: {
|
|
377
|
+
...(peer?.fromSession !== undefined ? { fromSession: peer.fromSession } : {}),
|
|
378
|
+
...(peer?.senderKey !== undefined ? { senderKey: peer.senderKey } : {}),
|
|
379
|
+
fromRoot: box.sessionId,
|
|
380
|
+
kind: "peer_message",
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
295
384
|
function evictOldest(s, match) {
|
|
296
385
|
let idx = s.items.findIndex((i) => match(i) && !TERMINAL_STATUSES.has(i.status));
|
|
297
386
|
if (idx === -1)
|
|
@@ -419,7 +419,6 @@ export function registerBackgroundAgentLane(core, input) {
|
|
|
419
419
|
...(input.agentType !== undefined ? { agentType: input.agentType } : {}),
|
|
420
420
|
...(input.description !== undefined ? { description: input.description } : {}),
|
|
421
421
|
...(input.model !== undefined ? { model: input.model } : {}),
|
|
422
|
-
...(input.teamName !== undefined ? { teamName: input.teamName } : {}),
|
|
423
422
|
writerId: core.writerId,
|
|
424
423
|
writerEpoch: 1,
|
|
425
424
|
spawnedAt: now,
|
|
@@ -1236,7 +1235,7 @@ export async function deliverToRunningAgentLane(core, id, access, notification,
|
|
|
1236
1235
|
let result;
|
|
1237
1236
|
try {
|
|
1238
1237
|
const d = await handle.notify?.(notification, opts);
|
|
1239
|
-
if (d === "parked") {
|
|
1238
|
+
if (d === "parked" || d === "parked_durably") {
|
|
1240
1239
|
result = handle.retainedContinuation === true ? { ok: true, disposition: "parked" } : { ok: false, reason: "not_running" };
|
|
1241
1240
|
}
|
|
1242
1241
|
else {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
2
|
-
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, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
|
|
2
|
+
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
3
|
import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
|
|
4
4
|
import { thrownText } from "./thrown-value.js";
|
|
5
5
|
export function registerMonitorLane(core, input) {
|
|
@@ -339,6 +339,7 @@ export async function pollMonitorLane(handle, filter, deadline, signal) {
|
|
|
339
339
|
details: {
|
|
340
340
|
status: running ? "running" : handle.status,
|
|
341
341
|
exitCode: spool.exitCode,
|
|
342
|
+
replay: SPOOL_REPLAY,
|
|
342
343
|
...(spool.droppedBytes !== undefined ? { bytesDroppedBeforeCursor: spool.droppedBytes } : {}),
|
|
343
344
|
...(spool.timedOut === true ? { timedOut: true, ...(spool.timeoutSec !== undefined ? { timeoutSec: spool.timeoutSec } : {}) } : {}),
|
|
344
345
|
},
|
|
@@ -5,6 +5,7 @@ import type { BackgroundAgentRecord, BackgroundAgentStore } from "./background-a
|
|
|
5
5
|
import type { WorkflowHandle, WorkflowRun } from "../orchestration/workflow.js";
|
|
6
6
|
import type { TaskNotificationPayload } from "./task-notification.js";
|
|
7
7
|
import type { ToolResultStore } from "./tool-result-store.js";
|
|
8
|
+
import type { AssertAllKeysHandled } from "./ask-origin.js";
|
|
8
9
|
export type SemaTaskType = "background_bash" | "workflow" | "background_agent" | "monitor";
|
|
9
10
|
/** design/134 §3.3 (R3): WHO initiated a kill — recorded at the initiation site (first-marker-wins),
|
|
10
11
|
* because the settle-side then/catch context has already lost it. OPEN ENUM on the wire, now in the
|
|
@@ -254,7 +255,7 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
|
|
|
254
255
|
* ends park per session inside the injector itself (the lane-teardown branch). */
|
|
255
256
|
notify?: (notification: import("./task-notification.js").TaskNotificationPayload, opts?: {
|
|
256
257
|
priority?: import("./task-notification.js").SystemInjectionPriority;
|
|
257
|
-
}) => "
|
|
258
|
+
}) => import("./task-notification.js").NotificationInjectionOutcome | Promise<import("./task-notification.js").NotificationInjectionOutcome>;
|
|
258
259
|
/** design/147 S2a — messages that arrived in the STARTUP window (row already
|
|
259
260
|
* running, lane not yet bound). Bounded; flushed in order by attachAgentNotify. Discarded at
|
|
260
261
|
* terminal settle: a run that failed during prepare never had a lane to park through, and the
|
|
@@ -658,6 +659,54 @@ export declare function spoolDropNote(acct: {
|
|
|
658
659
|
droppedBytes?: number;
|
|
659
660
|
dropUnknown?: true;
|
|
660
661
|
}): string;
|
|
662
|
+
/**
|
|
663
|
+
* HOW a poll's output body may be read AGAIN — the machine word beside `details`, previously legible
|
|
664
|
+
* only from the stdout heading's prose.
|
|
665
|
+
*
|
|
666
|
+
* `full` = the whole output of the task, re-rendered from the registry's own spool on every poll: a
|
|
667
|
+
* later poll answers the same bytes plus whatever arrived, so a consumer REPLACES what it holds.
|
|
668
|
+
* `cursor` = only what is NEW since the previous read: the env cursor was consumed and those bytes
|
|
669
|
+
* cannot be served twice, so a consumer APPENDS.
|
|
670
|
+
*
|
|
671
|
+
* The distinction was machine-invisible and the one key nearby reads the two faces OPPOSITELY:
|
|
672
|
+
* `bytesDroppedBeforeCursor` rides the spool faces only when something was actually dropped, and the
|
|
673
|
+
* increment face unconditionally (value possibly 0). A consumer discriminating on that key's PRESENCE
|
|
674
|
+
* therefore reads a spool that happened to drop bytes as an increment — and appends a body that was a
|
|
675
|
+
* full re-render, printing the same text twice.
|
|
676
|
+
*/
|
|
677
|
+
export declare const TASK_OUTPUT_REPLAY_KINDS: readonly ["full", "cursor"];
|
|
678
|
+
export type TaskOutputReplay = (typeof TASK_OUTPUT_REPLAY_KINDS)[number];
|
|
679
|
+
/**
|
|
680
|
+
* The ONE table the word and the prose both come out of. Each face's stdout/stderr headings live here
|
|
681
|
+
* rather than at the three legs that render them, so the banner a person reads and the key a program
|
|
682
|
+
* reads cannot drift — the failure mode this whole field exists to end (the two spool renderers were
|
|
683
|
+
* hand-copied twins before {@link renderSpoolBody}, and their drift was three disclosure bugs).
|
|
684
|
+
*
|
|
685
|
+
* `stdoutWhenGapped` is the heading a face falls back to once a gap has been disclosed. `undefined` is
|
|
686
|
+
* a ROW, not an omission: an increment window claims nothing about completeness to begin with ("new
|
|
687
|
+
* stdout" is true whether or not bytes were lost before it), so it has no claim to downgrade — while a
|
|
688
|
+
* spool advertising itself as "full" with a disclosed gap would be saying something false.
|
|
689
|
+
*/
|
|
690
|
+
export declare const REPLAY_FACE: {
|
|
691
|
+
readonly full: {
|
|
692
|
+
readonly stdout: "stdout (full, re-readable)";
|
|
693
|
+
readonly stdoutWhenGapped: "stdout (re-readable spool — INCOMPLETE, see note above)";
|
|
694
|
+
readonly stderr: "stderr";
|
|
695
|
+
};
|
|
696
|
+
readonly cursor: {
|
|
697
|
+
readonly stdout: "new stdout";
|
|
698
|
+
readonly stdoutWhenGapped: undefined;
|
|
699
|
+
readonly stderr: "new stderr";
|
|
700
|
+
};
|
|
701
|
+
};
|
|
702
|
+
export type ReplayHeadingTableCoversEveryReplay = AssertAllKeysHandled<Exclude<TaskOutputReplay, keyof typeof REPLAY_FACE>>;
|
|
703
|
+
/** The re-readable spool face: the monitor lane and the bash spool lane, both rendered by
|
|
704
|
+
* {@link renderSpoolBody}. Named once so the three legs that report it cannot each spell it. */
|
|
705
|
+
export declare const SPOOL_REPLAY = "full";
|
|
706
|
+
/** The consume-once increment face: the bash blocked wait, which accumulates env-cursor increments. */
|
|
707
|
+
export declare const INCREMENT_REPLAY = "cursor";
|
|
708
|
+
/** The stdout heading of a face, downgraded when a gap has been disclosed (see {@link REPLAY_FACE}). */
|
|
709
|
+
export declare function stdoutHeadingOf(replay: TaskOutputReplay, gapDisclosed: boolean): string;
|
|
661
710
|
/** The re-readable spool fields {@link renderSpoolBody} reads — the intersection of the bash handle's
|
|
662
711
|
* optional `spool` and the monitor handle's mandatory one (both carry supersets: `mirrorFailed` is
|
|
663
712
|
* bash-only, `drainFailed`/`dropNotified` monitor-only). Declared as a named shape, not inlined, so
|
|
@@ -878,15 +927,14 @@ export interface RegisterBackgroundAgentInput extends TaskAccess {
|
|
|
878
927
|
* internals chain; equals parentSessionId at depth 1). Persisted on the handle, the durable row
|
|
879
928
|
* and the roster so recovery faces enumerate the whole tree under the root without alias walks. */
|
|
880
929
|
rootSessionId?: string;
|
|
881
|
-
/** design/151 S3b — revival lookup
|
|
882
|
-
*
|
|
883
|
-
* named-teammate spawn would.
|
|
930
|
+
/** design/151 S3b — the revival lookup key (CC meta-sidecar shape): the model RECORD KEY,
|
|
931
|
+
* persisted on the durable row so a tier-3 revival rebuilds the spec the way a fresh
|
|
932
|
+
* named-teammate spawn would. A lookup key only, never a serialized spec. `model` is NOT the
|
|
884
933
|
* resolved model id: a spawn that named a model in WORDS records the caller's spelling verbatim
|
|
885
934
|
* (catalog key / tier word / CC alias) so the revival can re-resolve it against the catalog in
|
|
886
935
|
* force at WAKE time; only a spawn that carried a Model OBJECT records that object's id. Same
|
|
887
936
|
* value and same rule as the roster row's `model` column. */
|
|
888
937
|
model?: string;
|
|
889
|
-
teamName?: string;
|
|
890
938
|
toolUseId?: string;
|
|
891
939
|
abort: AbortController;
|
|
892
940
|
now?: number;
|
|
@@ -101,14 +101,25 @@ export function spoolDropNote(acct) {
|
|
|
101
101
|
return "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n";
|
|
102
102
|
return "";
|
|
103
103
|
}
|
|
104
|
+
export const TASK_OUTPUT_REPLAY_KINDS = ["full", "cursor"];
|
|
105
|
+
export const REPLAY_FACE = {
|
|
106
|
+
full: { stdout: "stdout (full, re-readable)", stdoutWhenGapped: "stdout (re-readable spool — INCOMPLETE, see note above)", stderr: "stderr" },
|
|
107
|
+
cursor: { stdout: "new stdout", stdoutWhenGapped: undefined, stderr: "new stderr" },
|
|
108
|
+
};
|
|
109
|
+
export const SPOOL_REPLAY = "full";
|
|
110
|
+
export const INCREMENT_REPLAY = "cursor";
|
|
111
|
+
export function stdoutHeadingOf(replay, gapDisclosed) {
|
|
112
|
+
const face = REPLAY_FACE[replay];
|
|
113
|
+
return gapDisclosed ? (face.stdoutWhenGapped ?? face.stdout) : face.stdout;
|
|
114
|
+
}
|
|
104
115
|
export function renderSpoolBody(spool, opts) {
|
|
105
116
|
const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
|
|
106
117
|
const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : opts.running ? "running" : opts.handleStatus) + timedOutClause;
|
|
107
118
|
const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)${opts.rolledExtraClause}\n` : "";
|
|
108
119
|
const droppedNote = spoolDropNote(spool);
|
|
109
|
-
const stdoutHeading = droppedNote !== "" || opts.drainNote !== ""
|
|
120
|
+
const stdoutHeading = stdoutHeadingOf(SPOOL_REPLAY, droppedNote !== "" || opts.drainNote !== "");
|
|
110
121
|
return (`status: ${displayStatus}\n${rolledNote}${droppedNote}${opts.drainNote}--- ${stdoutHeading} ---\n${opts.shape(spool.stdout) || "(none)"}\n` +
|
|
111
|
-
`--- stderr ---\n${opts.shape(spool.stderr) || "(none)"}`);
|
|
122
|
+
`--- ${REPLAY_FACE[SPOOL_REPLAY].stderr} ---\n${opts.shape(spool.stderr) || "(none)"}`);
|
|
112
123
|
}
|
|
113
124
|
export function droppedGapNote(spool) {
|
|
114
125
|
if ((spool.droppedBytes ?? 0) > 0)
|
|
@@ -8,7 +8,7 @@ import { createSafeNotifier } from "./safe-notify.js";
|
|
|
8
8
|
import { stripCwdSentinel } from "../tools/fs/fs-shared.js";
|
|
9
9
|
import { registerMonitorLane, pollMonitorLane, stopMonitorLane } from "./task-registry-monitor.js";
|
|
10
10
|
import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task-registry-workflow.js";
|
|
11
|
-
import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, renderSpoolBody, spoolDropNote, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
|
|
11
|
+
import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, renderSpoolBody, stdoutHeadingOf, REPLAY_FACE, SPOOL_REPLAY, INCREMENT_REPLAY, spoolDropNote, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
|
|
12
12
|
export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
|
|
13
13
|
import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
|
|
14
14
|
import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, recordBackgroundAgentOrgAdmissionLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, backgroundAgentCycleSeqLane, activeDelegationHandlesLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
|
|
@@ -1075,6 +1075,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
|
|
|
1075
1075
|
details: {
|
|
1076
1076
|
status: running ? "running" : handle.status,
|
|
1077
1077
|
exitCode: spool.exitCode,
|
|
1078
|
+
replay: SPOOL_REPLAY,
|
|
1078
1079
|
...(spool.droppedBytes !== undefined ? { bytesDroppedBeforeCursor: spool.droppedBytes } : {}),
|
|
1079
1080
|
...(spool.timedOut === true ? { timedOut: true, ...(spool.timeoutSec !== undefined ? { timeoutSec: spool.timeoutSec } : {}) } : {}),
|
|
1080
1081
|
},
|
|
@@ -1099,12 +1100,13 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
|
|
|
1099
1100
|
if (r.error.code === "not_found")
|
|
1100
1101
|
return noTask(requestedId, runningAgentFooterLane(this.core, access));
|
|
1101
1102
|
const retrieval_status = r.error.code === "timeout" ? "timeout" : "not_ready";
|
|
1102
|
-
const
|
|
1103
|
+
const carriedOutput = stdout.length > 0 || stderr.length > 0;
|
|
1104
|
+
const salvaged = carriedOutput
|
|
1103
1105
|
? `\n--- output accumulated before the error ---\n${clipTaskOutput(stdout, handle.outputFile)}${stderr ? `\n--- stderr ---\n${clipTaskOutput(stderr, handle.outputFile)}` : ""}`
|
|
1104
1106
|
: "";
|
|
1105
1107
|
return {
|
|
1106
1108
|
content: `Error (TaskOutput): ${r.error.message}${salvaged}`,
|
|
1107
|
-
details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status, error: r.error.code },
|
|
1109
|
+
details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status, ...(carriedOutput ? { details: { replay: INCREMENT_REPLAY } } : {}), error: r.error.code },
|
|
1108
1110
|
isError: true,
|
|
1109
1111
|
};
|
|
1110
1112
|
}
|
|
@@ -1125,8 +1127,8 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
|
|
|
1125
1127
|
const displayStatus = (r.value.status === "exited" ? `exited(code ${r.value.exitCode})` : r.value.status) + timedOutClause;
|
|
1126
1128
|
const droppedNote = spoolDropNote(dropAcct) +
|
|
1127
1129
|
(rolledChars > 0 ? `[!] ${rolledChars} accumulated char(s) dropped from the middle during the blocked wait (memory bound)\n` : "");
|
|
1128
|
-
const body = `status: ${displayStatus}\n${droppedNote}---
|
|
1129
|
-
`---
|
|
1130
|
+
const body = `status: ${displayStatus}\n${droppedNote}--- ${stdoutHeadingOf(INCREMENT_REPLAY, droppedNote !== "")} ---\n${shape(stdout) || "(none)"}\n` +
|
|
1131
|
+
`--- ${REPLAY_FACE[INCREMENT_REPLAY].stderr} ---\n${shape(stderr) || "(none)"}`;
|
|
1130
1132
|
return {
|
|
1131
1133
|
content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
|
|
1132
1134
|
details: {
|
|
@@ -1139,6 +1141,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
|
|
|
1139
1141
|
details: {
|
|
1140
1142
|
status: r.value.status,
|
|
1141
1143
|
exitCode: r.value.exitCode,
|
|
1144
|
+
replay: INCREMENT_REPLAY,
|
|
1142
1145
|
bytesDroppedBeforeCursor: dropAcct.droppedBytes ?? 0,
|
|
1143
1146
|
...(r.value.timedOut === true ? { timedOut: true, ...(r.value.timeoutSec !== undefined ? { timeoutSec: r.value.timeoutSec } : {}) } : {}),
|
|
1144
1147
|
},
|
package/dist/core/task-spec.d.ts
CHANGED
|
@@ -783,9 +783,12 @@ export interface TaskSpec {
|
|
|
783
783
|
/**
|
|
784
784
|
* design/199 件B — TASK-layer ADDITIONS to the built-in sensitive-path READ deny set
|
|
785
785
|
* ({@link import("../tools/fs/read-deny.js").READ_FACE_DEFAULT_DENY_ENTRIES}). Judged by the
|
|
786
|
-
* structured read faces (Read/Grep/Glob/RepoMap, their traversals, the
|
|
787
|
-
*
|
|
788
|
-
* {@link
|
|
786
|
+
* structured read faces (Read/Grep/Glob/RepoMap, their traversals, the compaction attachment reader) and
|
|
787
|
+
* by the shell READ STATION under every doctrine — the full shell (`Bash`, `Monitor`, whatever
|
|
788
|
+
* {@link shellGate} says) and the `bash_readonly` shell mounted by {@link handsReadOnly} alike: a
|
|
789
|
+
* deny-listed shell read is REFUSED at the gate (`deniedBy: "read_boundary"`), never asked, with the same
|
|
790
|
+
* sentence the structured faces refuse with — one deny judge for every read face — in BOTH containment
|
|
791
|
+
* modes. Add-only at
|
|
789
792
|
* the task layer: entries here UNION with the active built-ins and the deployment's
|
|
790
793
|
* {@link RunnerDeps.readDenyPatterns}; `[]` ≡ absent (union identity); NOTHING at the task layer
|
|
791
794
|
* can remove a built-in — the one removal channel is the DEPLOYMENT's built-in configuration
|
|
@@ -873,8 +876,12 @@ export interface TaskSpec {
|
|
|
873
876
|
* - `"always"`: every `bash` command tightens to an `irreversible_ask` durable suspend (fail-closed; the
|
|
874
877
|
* correct default for an unattended/supervisor deployment with no classifier).
|
|
875
878
|
* - `"classify"`: mark `bash` `irreversibility:"maybe"` with {@link import("../tools/fs/index.js").bashReversibilityProbe}
|
|
876
|
-
* — a provably-benign single allowlisted read-only command (no operators)
|
|
877
|
-
* (writes, egress, pipes, unknown commands) tightens to ask.
|
|
879
|
+
* — the SHAPE classifier alone: a provably-benign single allowlisted read-only command (no operators)
|
|
880
|
+
* auto-allows; everything else (writes, egress, pipes, unknown commands) tightens to ask. WHERE a command
|
|
881
|
+
* reads is not this doctrine's question under any value: the gate's shell read station judges every
|
|
882
|
+
* shell call's read candidates against the deny set and the containment roots (a deny-listed read is
|
|
883
|
+
* refused, an out-of-root read is a mandated ask) whether `shellGate` is `"off"`, `"classify"` or
|
|
884
|
+
* `"always"`. An egress command (e.g. `curl`) suspends under
|
|
878
885
|
* the IRREVERSIBLE axis (the shell mark is irreversibility, not egress) — the kind is non-budgetable either
|
|
879
886
|
* way, so the budget resolver still never auto-approves it; the axis label is informational.
|
|
880
887
|
* NOT unconditional: a POSITIVE per-tool mark on the shell tool keeps its seat — an explicit
|
|
@@ -154,6 +154,7 @@ export const ENGINE_CARDS = Object.freeze({
|
|
|
154
154
|
a2a: "src/core/a2a.ts",
|
|
155
155
|
path_not_in_root: "src/tools/fs/safety.ts",
|
|
156
156
|
read_path_denied: "src/tools/fs/safety.ts",
|
|
157
|
+
readonly_out_of_root: "src/tools/fs/shell-read-check.ts",
|
|
157
158
|
});
|
|
158
159
|
const byId = new Map();
|
|
159
160
|
for (const e of TOOL_CATALOG_ENTRIES) {
|