pi-crew 0.10.2 → 0.10.4
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/AGENTS.md +2 -1
- package/CHANGELOG.md +249 -0
- package/README.md +5 -1
- package/dist/index.mjs +10844 -7250
- package/docs/architecture.md +4 -4
- package/docs/commands-reference.md +3 -0
- package/docs/publishing.md +15 -3
- package/install.mjs +90 -39
- package/package.json +9 -3
- package/schema.json +11 -0
- package/scripts/README.md +4 -3
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +7 -2
- package/skills/real-test-pi-crew/SKILL.md +428 -82
- package/src/config/config-merge.ts +11 -1
- package/src/config/config-validation.ts +40 -1
- package/src/config/config.ts +28 -6
- package/src/config/defaults.ts +35 -10
- package/src/config/env-vars.ts +27 -2
- package/src/config/migration-validator.ts +113 -0
- package/src/config/types.ts +36 -0
- package/src/extension/cross-extension-rpc.ts +3 -7
- package/src/extension/register.ts +13 -0
- package/src/extension/registration/lifecycle-handlers.ts +40 -9
- package/src/extension/registration/observability.ts +3 -7
- package/src/extension/registration/subagent-tools.ts +3 -7
- package/src/extension/registration/team-tool.ts +56 -12
- package/src/extension/registration/ui.ts +3 -8
- package/src/extension/registration/viewers.ts +3 -10
- package/src/extension/team-manager-command.ts +3 -7
- package/src/extension/team-tool/api/agent-control.ts +17 -10
- package/src/extension/team-tool/api/heartbeat.ts +4 -3
- package/src/extension/team-tool/api/mailbox.ts +33 -20
- package/src/extension/team-tool/api/plan-approval.ts +5 -5
- package/src/extension/team-tool/api/task-claims.ts +8 -7
- package/src/extension/team-tool/cancel.ts +6 -0
- package/src/extension/team-tool/doctor.ts +364 -7
- package/src/extension/team-tool/handle-settings.ts +23 -1
- package/src/extension/team-tool/inspect.ts +10 -2
- package/src/extension/team-tool/run.ts +3 -7
- package/src/extension/team-tool/status.ts +12 -0
- package/src/extension/team-tool.ts +41 -16
- package/src/hooks/registry.ts +62 -56
- package/src/prompt/inbox-poll.ts +90 -0
- package/src/prompt/message-tool.ts +166 -0
- package/src/prompt/prompt-runtime.ts +201 -18
- package/src/prompt/scratchpad-lifecycle.ts +3 -3
- package/src/prompt/surface-worker.ts +720 -0
- package/src/prompt/worker-events-channel.ts +49 -3
- package/src/runtime/async-runner.ts +29 -1
- package/src/runtime/background-runner.ts +43 -42
- package/src/runtime/broker/broker-issuer.ts +27 -2
- package/src/runtime/broker/crew-broker-tokens.ts +56 -4
- package/src/runtime/broker/crew-broker.ts +334 -443
- package/src/runtime/broker/delegate/delegate-event.ts +37 -0
- package/src/runtime/broker/mailbox-observer/mailbox-fanout.ts +59 -0
- package/src/runtime/broker/protocol/connection-state.ts +103 -0
- package/src/runtime/broker/protocol/events-replay.ts +68 -0
- package/src/runtime/broker/protocol/manifest-loader.ts +20 -0
- package/src/runtime/broker/protocol/msg-inbox.ts +69 -0
- package/src/runtime/broker/protocol/request-parsers.ts +175 -0
- package/src/runtime/broker/protocol/wait-auth.ts +46 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +23 -9
- package/src/runtime/child-pi/child-pi-streams.ts +9 -1
- package/src/runtime/child-pi/child-pi.ts +368 -5
- package/src/runtime/crew-agent-records.ts +13 -1
- package/src/runtime/dispatch-batch.ts +12 -1
- package/src/runtime/event-log-tail-source.ts +374 -0
- package/src/runtime/finalize-run.ts +19 -7
- package/src/runtime/foreground-control.ts +19 -6
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +6 -0
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -0
- package/src/runtime/goal-workflow/goal-loop-runner.ts +29 -27
- package/src/runtime/goal-workflow/goal-state-store.ts +3 -0
- package/src/runtime/heartbeat/heartbeat-watcher.ts +3 -3
- package/src/runtime/live-session/live-agent-manager.ts +34 -1
- package/src/runtime/live-session/live-control-realtime.ts +10 -0
- package/src/runtime/live-session/live-session-runtime.ts +47 -27
- package/src/runtime/manifest-cache.ts +128 -17
- package/src/runtime/model/pi-args.ts +59 -65
- package/src/runtime/output/sidechain-output.ts +61 -6
- package/src/runtime/plan-replan.ts +3 -0
- package/src/runtime/process/proc-stat.ts +46 -0
- package/src/runtime/process/zombie-scanner.ts +32 -19
- package/src/runtime/spawn-policy.ts +27 -41
- package/src/runtime/stale-reconciler.ts +28 -3
- package/src/runtime/supervisor-contact.ts +3 -0
- package/src/runtime/surface/degrade.ts +776 -0
- package/src/runtime/surface/herdr-provider.ts +546 -0
- package/src/runtime/surface/launch-script.ts +172 -0
- package/src/runtime/surface/resolve-surface.ts +274 -0
- package/src/runtime/surface/surface-provider.ts +129 -0
- package/src/runtime/surface/surface-spawn.ts +475 -0
- package/src/runtime/surface/tmux-provider.ts +400 -0
- package/src/runtime/task-runner/child-executor.ts +80 -0
- package/src/runtime/task-runner/post-execution.ts +57 -2
- package/src/runtime/task-runner/prompt-builder.ts +1 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +191 -56
- package/src/runtime/task-runner/state-helpers.ts +54 -30
- package/src/runtime/task-runner.ts +4 -2
- package/src/runtime/team-runner.ts +104 -3
- package/src/schema/config-schema.ts +24 -0
- package/src/state/atomic-write.ts +219 -40
- package/src/state/coordination/locks.ts +7 -5
- package/src/state/coordination/mailbox.ts +56 -10
- package/src/state/event-log/cursor.ts +413 -23
- package/src/state/event-log/event-log.ts +120 -113
- package/src/state/event-log/sequence-cache.ts +21 -3
- package/src/state/stores/ownership-map.ts +5 -4
- package/src/state/stores/plan-store.ts +12 -0
- package/src/state/stores/state-store.ts +103 -6
- package/src/state/types.ts +51 -0
- package/src/ui/inline-panel/agent-pane.ts +3 -0
- package/src/ui/powerbar-publisher.ts +3 -7
- package/src/ui/render-diff.ts +16 -8
- package/src/ui/run-action-dispatcher.ts +7 -10
- package/src/ui/run-dashboard.ts +87 -42
- package/src/ui/run-event-bus.ts +10 -1
- package/src/ui/run-snapshot-cache.ts +83 -35
- package/src/ui/settings-overlay.ts +4 -1
- package/src/ui/transcript-cache.ts +101 -13
- package/src/ui/transcript-viewer.ts +92 -24
- package/src/ui/widget/index.ts +32 -8
- package/src/utils/visual.ts +43 -0
- package/src/worktree/worktree-manager.ts +65 -4
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delegate-event.ts — Event recording for the broker delegate surface.
|
|
3
|
+
*
|
|
4
|
+
* Moved from crew-broker.ts (M4 / WI-4.1) — pure move, no behavior change.
|
|
5
|
+
* recordDelegateEvent uses ONLY the module-scoped event-log + error helpers;
|
|
6
|
+
* no broker state. Promoted to a top-level function so the class method is
|
|
7
|
+
* a 1-line delegation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { appendEventAsync } from "../../../state/event-log/event-log.ts";
|
|
11
|
+
import { logInternalError } from "../../../utils/internal-error.ts";
|
|
12
|
+
|
|
13
|
+
/** Fire-and-forget async append; an append failure is logged, never thrown
|
|
14
|
+
* (broker handlers must not block the event loop on the sync event-log lock).
|
|
15
|
+
* See `crew-broker.delegate.event` event scope conventions in ADR-5 §10. */
|
|
16
|
+
export function recordDelegateEvent(
|
|
17
|
+
manifest: { eventsPath: string; runId: string },
|
|
18
|
+
type:
|
|
19
|
+
| "delegate.requested"
|
|
20
|
+
| "delegate.admitted"
|
|
21
|
+
| "delegate.rejected"
|
|
22
|
+
| "delegate.completed"
|
|
23
|
+
| "delegate.timed_out"
|
|
24
|
+
| "delegate.rolled_up",
|
|
25
|
+
taskId: string,
|
|
26
|
+
data: Record<string, unknown>,
|
|
27
|
+
): void {
|
|
28
|
+
void appendEventAsync(manifest.eventsPath, {
|
|
29
|
+
type,
|
|
30
|
+
runId: manifest.runId,
|
|
31
|
+
taskId,
|
|
32
|
+
message: `${type}: ${JSON.stringify(data).slice(0, 200)}`,
|
|
33
|
+
data,
|
|
34
|
+
}).catch((err) =>
|
|
35
|
+
logInternalError("crew-broker.delegate.event", err instanceof Error ? err : new Error(String(err)), `runId=${manifest.runId}`),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mailbox-fanout.ts — Phase 1.3 mailbox-event fanout helper.
|
|
3
|
+
*
|
|
4
|
+
* Moved from crew-broker.ts (M4 / WI-4.1) — pure move, no behavior change.
|
|
5
|
+
* Operates only on the connectionsByRun index + writeOrQueue/enqueueFrame
|
|
6
|
+
* functions passed in. Promoted to a top-level function so the class
|
|
7
|
+
* method is a 1-line delegation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { MailboxMessage } from "../../../state/coordination/mailbox.ts";
|
|
11
|
+
import { encodeBrokerFrame } from "../../../utils/ndjson.ts";
|
|
12
|
+
import type { ServerConnection } from "../protocol/connection-state.ts";
|
|
13
|
+
|
|
14
|
+
export interface FanoutWriters {
|
|
15
|
+
writeOrQueue(conn: ServerConnection, buf: Buffer, force: boolean): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Phase 1.3: push a durable-appended mailbox message to any connected
|
|
19
|
+
* recipient for the message's run. Best-effort — silently skips
|
|
20
|
+
* recipients that are offline (they recover via msg.inbox). Never throws. */
|
|
21
|
+
export function fanoutMailboxMessage(
|
|
22
|
+
connectionsByRun: Map<string, Set<ServerConnection>>,
|
|
23
|
+
writers: FanoutWriters,
|
|
24
|
+
msg: MailboxMessage,
|
|
25
|
+
): void {
|
|
26
|
+
const set = connectionsByRun.get(msg.runId);
|
|
27
|
+
if (!set || set.size === 0) return;
|
|
28
|
+
// Recipient delivery dedup lives in src/prompt/prompt-runtime.ts and is
|
|
29
|
+
// keyed by the same message id in this mailbox event and the steering JSONL.
|
|
30
|
+
const eventFrame = encodeBrokerFrame({
|
|
31
|
+
event: "mailbox.message",
|
|
32
|
+
data: {
|
|
33
|
+
id: msg.id,
|
|
34
|
+
from: msg.from,
|
|
35
|
+
to: msg.to,
|
|
36
|
+
body: msg.body,
|
|
37
|
+
kind: msg.kind,
|
|
38
|
+
priority: msg.priority,
|
|
39
|
+
},
|
|
40
|
+
seq: 0, // mailbox messages don't carry a TeamEvent seq; dedup by msg.id
|
|
41
|
+
});
|
|
42
|
+
for (const conn of set) {
|
|
43
|
+
if (conn.closed || !conn.authed) continue;
|
|
44
|
+
// Recipient filter: deliver to the addressed task, or to all if 'all'.
|
|
45
|
+
// Task 5b (§15.2 wake): "parent"-addressed messages land in the
|
|
46
|
+
// run-level inbox, whose live consumer is the run's orchestrator
|
|
47
|
+
// connection (role from the orchestrator token — its taskId never
|
|
48
|
+
// equals "parent"), so without this branch the wake frame would be
|
|
49
|
+
// filtered out and the orchestrator would only see the message on
|
|
50
|
+
// its next inbox poll.
|
|
51
|
+
const isRecipient = !msg.to || msg.to === "all" || conn.taskId === msg.to || (msg.to === "parent" && conn.role === "orchestrator");
|
|
52
|
+
if (!isRecipient) continue;
|
|
53
|
+
try {
|
|
54
|
+
writers.writeOrQueue(conn, eventFrame, false);
|
|
55
|
+
} catch {
|
|
56
|
+
/* a slow/dead recipient must not break fanout to others */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connection-state.ts — Type definitions for crew-broker connection state
|
|
3
|
+
* and broker options.
|
|
4
|
+
*
|
|
5
|
+
* Moved from crew-broker.ts (M4 / WI-4.1) — pure move, no behavior change.
|
|
6
|
+
* These are pure types/interfaces used by the CrewBroker class but they
|
|
7
|
+
* contribute ~80 lines to the file's line count. Splitting them out
|
|
8
|
+
* contributes to the ≤2000-line gate (spec §5 M4 acceptance).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type * as net from "node:net";
|
|
12
|
+
import type { NdjsonDecoder } from "../../../utils/ndjson.ts";
|
|
13
|
+
import type { GrandchildSpawnInput, GrandchildSpawnResult } from "../../delegate-spawn.ts";
|
|
14
|
+
import type { WaitStatusCache } from "../wait-status-cache.ts";
|
|
15
|
+
|
|
16
|
+
export interface CrewBrokerOptions {
|
|
17
|
+
/** Root session ID used to derive the socket path. */
|
|
18
|
+
sessionId: string;
|
|
19
|
+
/** Pre-resolved socket path (skips re-derivation; useful for tests). */
|
|
20
|
+
socketPath?: string;
|
|
21
|
+
/** Frame cap in UTF-8 bytes. Default 256 KiB. */
|
|
22
|
+
maxFrameBytes?: number;
|
|
23
|
+
/** Per-connection outbound queue cap. Default 256. */
|
|
24
|
+
outboundQueueCap?: number;
|
|
25
|
+
/** Required: when false, start() is a no-op and the server never binds.
|
|
26
|
+
* Lets the lifecycle controller install the broker unconditionally and
|
|
27
|
+
* have a single kill switch. */
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
/** CWD for `loadRunManifestById` (Phase 1 msg.send / msg.inbox resolution).
|
|
30
|
+
* When omitted, manifest-touching methods return no-manifest errors. */
|
|
31
|
+
cwd?: string;
|
|
32
|
+
/** Optional test seam: override the `net` module (allows fake-server tests). */
|
|
33
|
+
netModule?: typeof net;
|
|
34
|
+
/** Optional test seam: inject a pre-configured WaitStatusCache (e.g. one
|
|
35
|
+
* wrapping a loader spy). Production uses a plain cache — see
|
|
36
|
+
* wait-status-cache.ts (R10-3). */
|
|
37
|
+
waitStatusCache?: WaitStatusCache;
|
|
38
|
+
/** WP-2/R2 (ADR-0 2026-08-17-waiting-producer-ask item 7): capability
|
|
39
|
+
* gate for the `wait.*` methods. DEFAULT FALSE — fail-closed. When not
|
|
40
|
+
* explicitly true, wait.request/wait.resolve are rejected with a
|
|
41
|
+
* `policy-disabled` error AND a `policy.action` event is appended to the
|
|
42
|
+
* run's events.jsonl (never silent). The production wiring threads
|
|
43
|
+
* `config.broker.waitMethodsEnabled` here; tests pass it explicitly. */
|
|
44
|
+
waitMethodsEnabled?: boolean;
|
|
45
|
+
/** T3/R5 (ADR-5 §10): capability gate for the `delegate` surface. DEFAULT
|
|
46
|
+
* TRUE since D8 (spec v0.7) — nested spawning is open out of the box; the
|
|
47
|
+
* broker still fail-closes when the flag is anything but true. The
|
|
48
|
+
* production wiring threads `config.nesting.enabled` (loadConfig layers
|
|
49
|
+
* DEFAULT_NESTING.enabled=true; a user `false` closes the surface); tests
|
|
50
|
+
* pass it explicitly. Rejections are NEVER silent (delegate.rejected). */
|
|
51
|
+
nestingEnabled?: boolean;
|
|
52
|
+
/** Optional override for the nested-slot budget size (config nesting.maxSlots). */
|
|
53
|
+
nestingMaxSlots?: number;
|
|
54
|
+
nestingMaxDepth?: number;
|
|
55
|
+
nestingTrustedEscalation?: boolean;
|
|
56
|
+
/** Global worker semaphore size, used to size the nested-slot budget. */
|
|
57
|
+
globalWorkerSemaphore?: number;
|
|
58
|
+
/** Test seam / alternative spawner for delegate grandchildren. Production
|
|
59
|
+
* uses spawnDelegateGrandchild (direct runChildPi call-site, ADR-5 §2). */
|
|
60
|
+
grandchildSpawner?: (input: GrandchildSpawnInput) => Promise<GrandchildSpawnResult>;
|
|
61
|
+
/** Resolved model catalog (canonical provider/id strings) for admission-time
|
|
62
|
+
* model validation (ADR-5 §7). When omitted, model validation is skipped
|
|
63
|
+
* (documented gap — the production wiring must always supply it). */
|
|
64
|
+
modelCatalog?: () => string[] | undefined;
|
|
65
|
+
/** ADR-5 §9: mirrors config limits.serializeOnPathOverlap for the workspace
|
|
66
|
+
* admission gate. Default false. */
|
|
67
|
+
serializeOnPathOverlap?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Per-connection server-side state. */
|
|
71
|
+
export interface ServerConnection {
|
|
72
|
+
socket: net.Socket;
|
|
73
|
+
decoder: NdjsonDecoder;
|
|
74
|
+
/** Whether the connection has completed `hello` successfully. */
|
|
75
|
+
authed: boolean;
|
|
76
|
+
/** Run id bound by hello. */
|
|
77
|
+
runId?: string;
|
|
78
|
+
/** Task id bound by hello. */
|
|
79
|
+
taskId?: string;
|
|
80
|
+
/** Role bound by hello: orchestrator can steer/msg-send; workers default. */
|
|
81
|
+
role?: "orchestrator" | "worker";
|
|
82
|
+
/** How the hello token matched the registry (ADR-0 item 6). Derived,
|
|
83
|
+
* non-secret metadata recorded at hello time so `wait.*` can reject a
|
|
84
|
+
* legacy bare-runId fallback match WITHOUT keeping the raw token on the
|
|
85
|
+
* connection (tokens stay confined to the heap-only registry). */
|
|
86
|
+
authMatchKind?: "compound" | "runId-fallback";
|
|
87
|
+
/** Task 10 fix round 2 (BUG #3): sha256 of the secret this connection
|
|
88
|
+
* authenticated with. Derived, one-way — never the plaintext token. The
|
|
89
|
+
* post-hello revocation check evaluates THIS digest so a revoke →
|
|
90
|
+
* re-issue window cannot let an old connection ride the freshly issued
|
|
91
|
+
* token for the same key. */
|
|
92
|
+
authedSecretHash?: string;
|
|
93
|
+
/** Outbound queue of encoded frames awaiting drain. */
|
|
94
|
+
outbound: Buffer[];
|
|
95
|
+
/** Set when the queue has hit the cap and a frame was dropped. */
|
|
96
|
+
needsResync: boolean;
|
|
97
|
+
/** Set when the connection is closing (idempotent). */
|
|
98
|
+
closed: boolean;
|
|
99
|
+
/** Timer for the hello deadline. */
|
|
100
|
+
helloTimer: NodeJS.Timeout | null;
|
|
101
|
+
/** Monotonic seq counter for outbound events (diagnostic). */
|
|
102
|
+
outboundSeq: number;
|
|
103
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* events-replay.ts — Phase 1.5 events.since handler (label corrected
|
|
3
|
+
* 2026-09-10 per review F4 — "Phase 2" is events.subscribe). Used by clients
|
|
4
|
+
* to resync after a missed live frame (e.g. after a queue overflow or
|
|
5
|
+
* reconnect).
|
|
6
|
+
*
|
|
7
|
+
* Moved from crew-broker.ts (M4 / WI-4.1) — pure move, no behavior change.
|
|
8
|
+
* Operates only on the connection + a writers adapter (sendError/sendResult).
|
|
9
|
+
* Promoted to a top-level function so the class method is a 1-line delegation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readEventsCursor } from "../../../state/event-log/event-log.ts";
|
|
13
|
+
import { loadRunManifestById } from "../../../state/stores/state-store.ts";
|
|
14
|
+
import type { ServerConnection } from "./connection-state.ts";
|
|
15
|
+
|
|
16
|
+
export interface EventWriterHelpers {
|
|
17
|
+
sendError(conn: ServerConnection, id: string, code: string, message: string): void;
|
|
18
|
+
sendResult(conn: ServerConnection, id: string, result: unknown): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Phase 2: events.since — page-replay handler. Returns events with
|
|
22
|
+
* seq > sinceSeq from the durable log, capped to `limit` (default 1000). */
|
|
23
|
+
export async function handleEventsSince(
|
|
24
|
+
conn: ServerConnection,
|
|
25
|
+
id: string,
|
|
26
|
+
params: unknown,
|
|
27
|
+
helpers: EventWriterHelpers,
|
|
28
|
+
cwd: string | undefined,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
if (!conn.runId) {
|
|
31
|
+
helpers.sendError(conn, id, "auth", "not authed");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!cwd) {
|
|
35
|
+
helpers.sendError(conn, id, "no-manifest", "broker has no cwd configured");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let eventsPath: string;
|
|
39
|
+
try {
|
|
40
|
+
const loaded = loadRunManifestById(cwd, conn.runId);
|
|
41
|
+
if (!loaded) {
|
|
42
|
+
helpers.sendError(conn, id, "no-manifest", `run '${conn.runId}' not found`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
eventsPath = loaded.manifest.eventsPath;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
helpers.sendError(conn, id, "no-manifest", (err as Error).message);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const v = params && typeof params === "object" && !Array.isArray(params) ? (params as Record<string, unknown>) : {};
|
|
51
|
+
const sinceSeq = typeof v.sinceSeq === "number" && Number.isFinite(v.sinceSeq) ? Math.max(0, Math.floor(v.sinceSeq)) : 0;
|
|
52
|
+
const limit = typeof v.limit === "number" && Number.isFinite(v.limit) ? Math.min(Math.max(1, Math.floor(v.limit)), 1000) : 1000;
|
|
53
|
+
try {
|
|
54
|
+
const result = readEventsCursor(eventsPath, { sinceSeq, limit });
|
|
55
|
+
// hasMore is true iff the total filtered count exceeds the page we
|
|
56
|
+
// returned. When `total === events.length` we are at the exact end
|
|
57
|
+
// of the stream (caller will discover this on the next call when
|
|
58
|
+
// `nextSeq` is unchanged from `sinceSeq`).
|
|
59
|
+
const hasMore = result.total > result.events.length;
|
|
60
|
+
helpers.sendResult(conn, id, {
|
|
61
|
+
events: result.events,
|
|
62
|
+
nextSeq: result.nextSeq,
|
|
63
|
+
hasMore,
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
helpers.sendError(conn, id, "replay-failed", (err as Error).message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* manifest-loader.ts — Helper for loading run manifests during hello.
|
|
3
|
+
*
|
|
4
|
+
* Moved from crew-broker.ts (M4 / WI-4.1) — pure move, no behavior change.
|
|
5
|
+
* The CrewBroker.loadRunForHello method delegated entirely to
|
|
6
|
+
* loadRunManifestById after one safety check (cwd absence); the helper is
|
|
7
|
+
* now a top-level function the class method delegates to in 1 line.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { loadRunManifestById } from "../../../state/stores/state-store.ts";
|
|
11
|
+
import type { TeamRunManifest, TeamTaskState } from "../../../state/types.ts";
|
|
12
|
+
|
|
13
|
+
export function loadRunForHello(cwd: string | undefined, runId: string): { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined {
|
|
14
|
+
if (!cwd) return undefined;
|
|
15
|
+
try {
|
|
16
|
+
return loadRunManifestById(cwd, runId) ?? undefined;
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* msg-inbox.ts — Phase 1.2 msg.inbox broker handler (Phase 1.1 = msg.send;
|
|
3
|
+
* label corrected 2026-09-10 per review F3 — it was mislabeled 1.1 at extraction).
|
|
4
|
+
*
|
|
5
|
+
* Moved from crew-broker.ts (M4 / WI-4.1) — pure move, no behavior change.
|
|
6
|
+
* Operates on a connection + writers adapter + cwd. Top-level function so
|
|
7
|
+
* the class method is a 1-line delegation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readMailbox } from "../../../state/coordination/mailbox.ts";
|
|
11
|
+
import { loadRunManifestById } from "../../../state/stores/state-store.ts";
|
|
12
|
+
import type { ServerConnection } from "./connection-state.ts";
|
|
13
|
+
import { parseMsgInboxParams } from "./request-parsers.ts";
|
|
14
|
+
|
|
15
|
+
export interface MsgInboxHelpers {
|
|
16
|
+
sendError(conn: ServerConnection, id: string, code: string, message: string): void;
|
|
17
|
+
sendResult(conn: ServerConnection, id: string, result: unknown): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Phase 1.1: read the durable mailbox for the addressed task (or run-level
|
|
21
|
+
* "inbox" lane) with cursor/limit pagination. Returns messages whose status
|
|
22
|
+
* is NOT yet `acknowledged`; callers should ack after processing. */
|
|
23
|
+
export async function handleMsgInbox(
|
|
24
|
+
conn: ServerConnection,
|
|
25
|
+
id: string,
|
|
26
|
+
params: unknown,
|
|
27
|
+
helpers: MsgInboxHelpers,
|
|
28
|
+
cwd: string | undefined,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
if (!conn.runId) {
|
|
31
|
+
helpers.sendError(conn, id, "auth", "not authed");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const parsed = parseMsgInboxParams(params);
|
|
35
|
+
if (!parsed) {
|
|
36
|
+
helpers.sendError(conn, id, "bad-params", "msg.inbox: invalid params");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!cwd) {
|
|
40
|
+
helpers.sendError(conn, id, "no-manifest", "broker has no cwd configured");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let manifest: Parameters<typeof readMailbox>[0];
|
|
44
|
+
try {
|
|
45
|
+
const loaded = loadRunManifestById(cwd, conn.runId);
|
|
46
|
+
if (!loaded) {
|
|
47
|
+
helpers.sendError(conn, id, "no-manifest", `run '${conn.runId}' not found`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
manifest = loaded.manifest;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
helpers.sendError(conn, id, "no-manifest", (err as Error).message);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const limit = Math.min(Math.max(parsed.limit ?? 100, 1), 1000);
|
|
56
|
+
const taskId = conn.taskId ?? undefined;
|
|
57
|
+
const all = readMailbox(manifest, "inbox", taskId);
|
|
58
|
+
const filtered = all.filter((m) => m.status !== "acknowledged");
|
|
59
|
+
const offset = parsed.cursor ? Number.parseInt(parsed.cursor, 10) || 0 : 0;
|
|
60
|
+
const page = filtered.slice(offset, offset + limit);
|
|
61
|
+
const nextOffset = offset + page.length;
|
|
62
|
+
const hasMore = nextOffset < filtered.length;
|
|
63
|
+
helpers.sendResult(conn, id, {
|
|
64
|
+
messages: page,
|
|
65
|
+
nextCursor: hasMore ? String(nextOffset) : undefined,
|
|
66
|
+
hasMore,
|
|
67
|
+
total: filtered.length,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* request-parsers.ts — Protocol-level parameter parsers and type guards for
|
|
3
|
+
* the crew-broker wire protocol.
|
|
4
|
+
*
|
|
5
|
+
* Moved from crew-broker.ts (M4 / WI-4.1) — pure move, no behavior change.
|
|
6
|
+
* Every parser rejects malformed frames at the boundary so handlers see only
|
|
7
|
+
* typed params. Method-name charset check (^[a-zA-Z][a-zA-Z0-9._-]{0,63}$)
|
|
8
|
+
* guards against control chars / oversized names reaching the dispatcher.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { MailboxMessageKind, MailboxMessagePriority } from "../../../state/coordination/mailbox.ts";
|
|
12
|
+
|
|
13
|
+
/** Protocol version negotiated at `hello` time. Bump on breaking change. */
|
|
14
|
+
export const BROKER_PROTOCOL = 1;
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Type guards (no `any`)
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export function isRequestObject(value: unknown): value is { id: string; method: string; params: unknown } {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
22
|
+
const v = value as Record<string, unknown>;
|
|
23
|
+
if (typeof v.id !== "string" || v.id.length === 0 || v.id.length > 256) return false;
|
|
24
|
+
if (typeof v.method !== "string" || v.method.length === 0 || v.method.length > 64) return false;
|
|
25
|
+
// Method names are restricted to a small safe charset. This guards against
|
|
26
|
+
// odd inputs (control chars, very long names) reaching the dispatcher.
|
|
27
|
+
if (!/^[a-zA-Z][a-zA-Z0-9._-]{0,63}$/.test(v.method)) return false;
|
|
28
|
+
// params may be anything (validated per-method), but not undefined-shaped.
|
|
29
|
+
return "params" in v;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isHelloParams(value: unknown): value is {
|
|
33
|
+
protocol: number;
|
|
34
|
+
runId: string;
|
|
35
|
+
taskId: string;
|
|
36
|
+
token: string;
|
|
37
|
+
role?: string;
|
|
38
|
+
} {
|
|
39
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
40
|
+
const v = value as Record<string, unknown>;
|
|
41
|
+
if (v.protocol !== BROKER_PROTOCOL) {
|
|
42
|
+
// Force exact-type comparison (must be the number 1, not "1").
|
|
43
|
+
if (typeof v.protocol !== "number" || !Number.isInteger(v.protocol)) return false;
|
|
44
|
+
}
|
|
45
|
+
if (typeof v.runId !== "string" || v.runId.length === 0 || v.runId.length > 256) return false;
|
|
46
|
+
if (typeof v.taskId !== "string" || v.taskId.length === 0 || v.taskId.length > 256) return false;
|
|
47
|
+
if (typeof v.token !== "string" || v.token.length === 0 || v.token.length > 256) return false;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Phase 1 parameter parsers (module-level; no `any`)
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
export interface MsgSendParams {
|
|
56
|
+
to: string | string[] | "all";
|
|
57
|
+
body: unknown;
|
|
58
|
+
kind?: MailboxMessageKind;
|
|
59
|
+
priority?: MailboxMessagePriority;
|
|
60
|
+
replyTo?: string;
|
|
61
|
+
/** Task 5b (§15.2): short subject echoed into the worker.message wake
|
|
62
|
+
* event (bounded like the tool-side MSG_SUBJECT_MAX_CHARS). */
|
|
63
|
+
subject?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseMsgSendParams(value: unknown): MsgSendParams | undefined {
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
68
|
+
const v = value as Record<string, unknown>;
|
|
69
|
+
const to = v.to;
|
|
70
|
+
if (typeof to !== "string" && !Array.isArray(to)) return undefined;
|
|
71
|
+
if (Array.isArray(to) && !to.every((s) => typeof s === "string" && s.length > 0)) return undefined;
|
|
72
|
+
if (typeof to === "string" && to.length === 0) return undefined;
|
|
73
|
+
if (v.body === undefined) return undefined;
|
|
74
|
+
const kind = v.kind as MailboxMessageKind | undefined;
|
|
75
|
+
if (kind !== undefined && !["message", "notify", "steer", "follow-up", "response", "group_join"].includes(kind)) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const priority = v.priority as MailboxMessagePriority | undefined;
|
|
79
|
+
if (priority !== undefined && !["urgent", "normal", "low"].includes(priority)) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const replyTo = typeof v.replyTo === "string" ? v.replyTo : undefined;
|
|
83
|
+
const subject = typeof v.subject === "string" && v.subject.length > 0 && v.subject.length <= 256 ? v.subject : undefined;
|
|
84
|
+
return { to: to as string | string[] | "all", body: v.body, kind, priority, replyTo, subject };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface MsgInboxParams {
|
|
88
|
+
limit?: number;
|
|
89
|
+
cursor?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseMsgInboxParams(value: unknown): MsgInboxParams | undefined {
|
|
93
|
+
if (value === undefined || value === null) return { limit: 100, cursor: undefined };
|
|
94
|
+
if (typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
95
|
+
const v = value as Record<string, unknown>;
|
|
96
|
+
const limit = v.limit;
|
|
97
|
+
if (limit !== undefined && (typeof limit !== "number" || !Number.isFinite(limit) || limit < 1)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const cursor = v.cursor;
|
|
101
|
+
if (cursor !== undefined && typeof cursor !== "string") return undefined;
|
|
102
|
+
return { limit: limit as number | undefined, cursor: cursor as string | undefined };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function safeStringify(value: unknown): string {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.stringify(value) ?? "{}";
|
|
108
|
+
} catch {
|
|
109
|
+
return "{}";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// WP-2/R2 wait.* parameter parsers (ADR-0 2026-08-17-waiting-producer-ask)
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
/** Server-side ceiling for the ask deadline (ADR P2-7): worker-controlled
|
|
118
|
+
* timeoutSec may NEVER exceed 1h — an unbounded timeout would pin slots and
|
|
119
|
+
* amplify I/O. Applied as deadline = now + min(timeoutSec, 3600). */
|
|
120
|
+
export const WAIT_REQUEST_TIMEOUT_SEC_MAX = 3600;
|
|
121
|
+
/** Default ask timeout when the caller omits timeoutSec (ADR item 1). */
|
|
122
|
+
export const WAIT_REQUEST_TIMEOUT_SEC_DEFAULT = 600;
|
|
123
|
+
/** Bounded question payload (defense-in-depth under the 256 KiB frame cap). */
|
|
124
|
+
export const WAIT_QUESTION_MAX_CHARS = 8192;
|
|
125
|
+
/** Bounded answer-choice list: at most 16 options, 256 chars each. */
|
|
126
|
+
export const WAIT_OPTIONS_MAX = 16;
|
|
127
|
+
export const WAIT_OPTION_MAX_CHARS = 256;
|
|
128
|
+
|
|
129
|
+
export interface WaitRequestParams {
|
|
130
|
+
to: string;
|
|
131
|
+
question: string;
|
|
132
|
+
options?: string[];
|
|
133
|
+
timeoutSec?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function parseWaitRequestParams(value: unknown): WaitRequestParams | undefined {
|
|
137
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
138
|
+
const v = value as Record<string, unknown>;
|
|
139
|
+
if (typeof v.to !== "string" || v.to.length === 0 || v.to.length > 256) return undefined;
|
|
140
|
+
if (typeof v.question !== "string" || v.question.length === 0 || v.question.length > WAIT_QUESTION_MAX_CHARS) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
let options: string[] | undefined;
|
|
144
|
+
if (v.options !== undefined) {
|
|
145
|
+
if (!Array.isArray(v.options) || v.options.length === 0 || v.options.length > WAIT_OPTIONS_MAX) return undefined;
|
|
146
|
+
for (const o of v.options) {
|
|
147
|
+
if (typeof o !== "string" || o.length === 0 || o.length > WAIT_OPTION_MAX_CHARS) return undefined;
|
|
148
|
+
}
|
|
149
|
+
options = v.options as string[];
|
|
150
|
+
}
|
|
151
|
+
// timeoutSec is clamped server-side in the handler (max 3600); the parser
|
|
152
|
+
// only rejects non-finite values. Non-positive values clamp to 1s.
|
|
153
|
+
if (v.timeoutSec !== undefined && (typeof v.timeoutSec !== "number" || !Number.isFinite(v.timeoutSec))) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
to: v.to,
|
|
158
|
+
question: v.question,
|
|
159
|
+
options,
|
|
160
|
+
timeoutSec: v.timeoutSec as number | undefined,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface WaitResolveParams {
|
|
165
|
+
to: string;
|
|
166
|
+
questionId: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function parseWaitResolveParams(value: unknown): WaitResolveParams | undefined {
|
|
170
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
171
|
+
const v = value as Record<string, unknown>;
|
|
172
|
+
if (typeof v.to !== "string" || v.to.length === 0 || v.to.length > 256) return undefined;
|
|
173
|
+
if (typeof v.questionId !== "string" || v.questionId.length === 0 || v.questionId.length > 128) return undefined;
|
|
174
|
+
return { to: v.to, questionId: v.questionId };
|
|
175
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wait-auth.ts — Auth + policy-rejection helpers for the broker wait.*
|
|
3
|
+
* surface.
|
|
4
|
+
*
|
|
5
|
+
* Moved from crew-broker.ts (M4 / WI-4.1) — pure move, no behavior change.
|
|
6
|
+
* waitAuthError is pure (operates only on conn metadata). recordWaitPolicyRejection
|
|
7
|
+
* uses module-scoped event-log + internal-error helpers — same as the class
|
|
8
|
+
* method did. Both are now top-level functions; the class delegates to them.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { appendEventAsync } from "../../../state/event-log/event-log.ts";
|
|
12
|
+
import { logInternalError } from "../../../utils/internal-error.ts";
|
|
13
|
+
import type { ServerConnection } from "./connection-state.ts";
|
|
14
|
+
|
|
15
|
+
/** Auth failure for the wait.* methods: worker role + task-scoped (compound)
|
|
16
|
+
* token ONLY (ADR-0 2026-08-17 item 6). Legacy bare-runId fallback match is
|
|
17
|
+
* REJECTED with a migrate hint; orchestrator token is rejected by role. */
|
|
18
|
+
export function waitAuthError(conn: ServerConnection): { code: string; message: string } | null {
|
|
19
|
+
if (conn.role !== "worker" || conn.authMatchKind === undefined) {
|
|
20
|
+
return { code: "forbidden", message: "wait.* requires a worker task-scoped token" };
|
|
21
|
+
}
|
|
22
|
+
if (conn.authMatchKind !== "compound") {
|
|
23
|
+
return {
|
|
24
|
+
code: "forbidden",
|
|
25
|
+
message: "wait.* requires a task-scoped token; re-dispatch with PI_CREW_BROKER_TASK_ID",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** ADR-0 item 7: a disabled-gate rejection MUST leave a durable trace in
|
|
32
|
+
* events.jsonl. The gate fails CLOSED but never SILENTLY. Fire-and-forget
|
|
33
|
+
* async append (broker handlers must not block the event loop on the sync
|
|
34
|
+
* event-log lock); an append failure is logged, never thrown. */
|
|
35
|
+
export function recordWaitPolicyRejection(manifest: { eventsPath: string; runId: string }, taskId: string, method: string): void {
|
|
36
|
+
const runId = manifest.runId;
|
|
37
|
+
void appendEventAsync(manifest.eventsPath, {
|
|
38
|
+
type: "policy.action",
|
|
39
|
+
runId,
|
|
40
|
+
taskId,
|
|
41
|
+
message: `${method} rejected: waitMethodsEnabled=false (fail-closed)`,
|
|
42
|
+
data: { action: method, reason: "wait-methods-disabled", policy: "broker.waitMethodsEnabled=false" },
|
|
43
|
+
}).catch((err) =>
|
|
44
|
+
logInternalError("crew-broker.wait.policy-event", err instanceof Error ? err : new Error(String(err)), `runId=${runId}`),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -237,6 +237,13 @@ export function buildFinalChildPiSpawnOptions(
|
|
|
237
237
|
export interface SpawnContext {
|
|
238
238
|
/** The command + args returned by getPiSpawnCommand. */
|
|
239
239
|
spawnSpec: ReturnType<typeof getPiSpawnCommand>;
|
|
240
|
+
/**
|
|
241
|
+
* RAW worker argv as produced by buildPiWorkerArgs (BEFORE any binary/script
|
|
242
|
+
* wrapping) — the surface spawn branch re-resolves the command line after
|
|
243
|
+
* stripping `--mode json -p` (spec §5.2: surface variant differs ONLY in run
|
|
244
|
+
* mode), so it needs the untouched argument list.
|
|
245
|
+
*/
|
|
246
|
+
builtArgs: string[];
|
|
240
247
|
/** The merged env (process.env + built.env) to pass to spawn(). */
|
|
241
248
|
mergedEnv: NodeJS.ProcessEnv;
|
|
242
249
|
/** Temp dir created by buildPiWorkerArgs (caller must clean up after spawn). */
|
|
@@ -288,15 +295,21 @@ export function prepareSpawnContext(
|
|
|
288
295
|
// design (S-6), which would leave the ask tool dead-on-arrival there.
|
|
289
296
|
// Control-namespace keys → pass assertOnlyControlEnvKeys.
|
|
290
297
|
built.env.PI_CREW_ASK_ENABLED = "1"; // dormant gate (worker conditional registerTool)
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
298
|
+
// D9/§15.2: the worker-side `message` tool env is UNCONDITIONAL for EVERY
|
|
299
|
+
// role and depth, like ask/delegate. The env gate is UX/hygiene, NOT the
|
|
300
|
+
// security boundary: the broker re-checks `from` (always the authenticated
|
|
301
|
+
// taskId) + `to` (parent/sibling/group) + kind (notify|message) from the
|
|
302
|
+
// connection identity, so an env flag alone cannot forge a sender.
|
|
303
|
+
// Control-namespace key → assertOnlyControlEnvKeys.
|
|
304
|
+
built.env.PI_CREW_MSG_ENABLED = "1"; // dormant gate (worker conditional registerTool)
|
|
305
|
+
// T3/R5 (ADR-5 §1, D8): the worker-side `delegate` tool env is now
|
|
306
|
+
// UNCONDITIONAL for EVERY role and depth — read-only/analyst roles get the
|
|
307
|
+
// tool too. The env gate is UX/hygiene, NOT the security boundary:
|
|
308
|
+
// broker admission re-checks depth + nested-slot budget from the task
|
|
309
|
+
// RECORD (spawn-policy.ts), and the spawn-side checkCrewDepth cap stops
|
|
310
|
+
// depth ≥ maxDepth children from even starting. Control-namespace key →
|
|
311
|
+
// assertOnlyControlEnvKeys.
|
|
312
|
+
built.env.PI_CREW_DELEGATE_ENABLED = "1"; // dormant gate (worker conditional registerTool)
|
|
300
313
|
// stateRoot from the spawn manifest: ChildPiRunInput threads
|
|
301
314
|
// manifest.eventsPath unconditionally (child-executor / background-runner)
|
|
302
315
|
// and the state store pins eventsPath === <stateRoot>/events.jsonl
|
|
@@ -386,6 +399,7 @@ export function prepareSpawnContext(
|
|
|
386
399
|
kind: "ready",
|
|
387
400
|
ctx: {
|
|
388
401
|
spawnSpec,
|
|
402
|
+
builtArgs: built.args,
|
|
389
403
|
mergedEnv: { ...process.env, ...built.env },
|
|
390
404
|
tempDir: built.tempDir,
|
|
391
405
|
builtEnv: built.env,
|
|
@@ -69,7 +69,15 @@ function compactContentPart(part: unknown): unknown | undefined {
|
|
|
69
69
|
return undefined;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Compact one child-pi JSON event into the bounded record shape stored in
|
|
74
|
+
* agent transcripts / per-agent event logs. Shared by TWO producers that must
|
|
75
|
+
* stay byte-compatible (the consumer, agent-transcript.ts, parses both):
|
|
76
|
+
* 1. the host-side stdout funnel (headless workers), and
|
|
77
|
+
* 2. the worker-side surface recorder (S2-T8 — surface panes have no
|
|
78
|
+
* stdout JSON stream, so the worker records its own events).
|
|
79
|
+
*/
|
|
80
|
+
export function compactChildPiEvent(event: unknown): unknown | undefined {
|
|
73
81
|
const record = asRecord(event);
|
|
74
82
|
if (!record) return undefined;
|
|
75
83
|
if (record.type === "message_update") return undefined;
|