@byok-sdk/client 0.13.0 → 0.14.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.
Files changed (39) hide show
  1. package/README.md +80 -0
  2. package/dist/adapters/claude/process-client.d.ts +24 -0
  3. package/dist/adapters/codex/process-runner.d.ts +46 -1
  4. package/dist/adapters/index.js +359 -28
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/adapters/pi/events.d.ts +1 -1
  7. package/dist/adapters/pi/rpc-client.d.ts +47 -1
  8. package/dist/adapters/pi/subagents-policy-extension.js +1 -1
  9. package/dist/adapters/pi/subagents-policy-extension.js.map +1 -1
  10. package/dist/adapters/pi/team-interaction-extension.d.ts +24 -0
  11. package/dist/adapters/pi/team-interaction-extension.js +81 -0
  12. package/dist/adapters/pi/team-interaction-extension.js.map +1 -0
  13. package/dist/adapters/process-tree.d.ts +74 -4
  14. package/dist/adapters/provider-credential-environment.d.ts +1 -1
  15. package/dist/adapters/win32-job-object.d.ts +101 -0
  16. package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
  17. package/dist/bin/byok-agent-message-mcp.js.map +1 -1
  18. package/dist/bin/byok-agent-team-mcp.js.map +1 -1
  19. package/dist/bin/byok-agent.js +9305 -7988
  20. package/dist/bin/byok-agent.js.map +1 -1
  21. package/dist/bin/byok-approval-mcp.js.map +1 -1
  22. package/dist/bin/commands/team-pi-relay.d.ts +24 -0
  23. package/dist/bin/commands/team-relay.d.ts +11 -0
  24. package/dist/bin/team-codex-relay.d.ts +32 -0
  25. package/dist/bin/team-notification-relay.d.ts +40 -0
  26. package/dist/bin/team-pi-session.d.ts +59 -0
  27. package/dist/daemon/agent-egress-policy.d.ts +8 -0
  28. package/dist/daemon/connection-manager.d.ts +6 -100
  29. package/dist/daemon/control-protocol.d.ts +2 -0
  30. package/dist/daemon/create-daemon.d.ts +32 -0
  31. package/dist/daemon/event-spill.d.ts +90 -0
  32. package/dist/daemon/journal/journal.d.ts +15 -3
  33. package/dist/daemon/journal/sqlite-journal.d.ts +7 -2
  34. package/dist/daemon/long-poll-transport.d.ts +2 -50
  35. package/dist/daemon/task-runner.d.ts +13 -0
  36. package/dist/daemon/team-workspace.d.ts +9 -0
  37. package/dist/index.js +897 -241
  38. package/dist/index.js.map +1 -1
  39. package/package.json +8 -5
@@ -0,0 +1,11 @@
1
+ import type { DaemonConfig } from '../../daemon/create-daemon';
2
+ /** One foreground owner per room. Stale locks are never guessed away or stolen. */
3
+ export declare function acquireTeamRelayLock(storeDir: string, workspaceId: string): Promise<() => Promise<void>>;
4
+ export declare function runTeamRelayCommand(input: {
5
+ config: DaemonConfig;
6
+ workspaceId: string;
7
+ bindingsFile: string;
8
+ codexBin: string;
9
+ maxNotifications: number;
10
+ signal: AbortSignal;
11
+ }): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { type TeamMemberLease } from '../daemon/team-workspace';
2
+ export interface CodexTeamBinding {
3
+ readonly context: string;
4
+ readonly lease: TeamMemberLease;
5
+ readonly threadId: string;
6
+ readonly endpoint: string;
7
+ readonly afterSeq: number;
8
+ }
9
+ export interface TeamNotificationSnapshot {
10
+ workspaceId: string;
11
+ memberId: string;
12
+ registryRevision: string;
13
+ expiresAt: string;
14
+ acknowledgedThroughSeq: number;
15
+ latestPeerSeq: number | null;
16
+ }
17
+ /** Local endpoints only. Never select a default daemon or infer a thread from its name. */
18
+ export declare function validateCodexRelayEndpoint(value: unknown): asserts value is string;
19
+ export declare function parseCodexTeamBinding(binding: unknown, workspaceId: string): CodexTeamBinding;
20
+ export declare function parseCodexTeamBindings(value: unknown, workspaceId: string): readonly CodexTeamBinding[];
21
+ export declare function loadPrivateTeamDocument(file: string): Promise<unknown>;
22
+ export declare function loadCodexTeamBindings(file: string, workspaceId: string): Promise<readonly CodexTeamBinding[]>;
23
+ export declare function codexTeamNotification(workspaceId: string, throughSeq: number): string;
24
+ /** The native queue receipt contract is qualified against this CLI version. */
25
+ export declare function preflightCodexRelay(codexBin: string, signal: AbortSignal): Promise<string>;
26
+ /** Only a confirmed exact-thread queue receipt advances this epoch's notification watermark. */
27
+ export declare function queueCodexTeamNotification(input: {
28
+ codexBin: string;
29
+ binding: CodexTeamBinding;
30
+ throughSeq: number;
31
+ signal: AbortSignal;
32
+ }): Promise<string>;
@@ -0,0 +1,40 @@
1
+ import type { TeamMemberLease } from '../daemon/team-workspace';
2
+ export interface TeamRelayBinding {
3
+ readonly context: string;
4
+ readonly lease: TeamMemberLease;
5
+ readonly afterSeq: number;
6
+ }
7
+ export type TeamRelayState = 'running' | 'paused' | 'stopped' | 'budget_exhausted' | 'failed';
8
+ export declare class TeamNotificationRelay<T extends TeamRelayBinding> {
9
+ private readonly options;
10
+ private state;
11
+ private attempts;
12
+ private error;
13
+ private readonly watermarks;
14
+ private pending;
15
+ private readonly abort;
16
+ constructor(options: {
17
+ bindings: readonly T[];
18
+ maxNotifications: number;
19
+ snapshot: (binding: T, afterSeq: number) => Promise<unknown>;
20
+ describe: (binding: T) => Record<string, string>;
21
+ ready?: (binding: T) => Promise<boolean>;
22
+ enqueue: (binding: T, throughSeq: number, signal: AbortSignal) => Promise<string>;
23
+ });
24
+ status(): {
25
+ state: TeamRelayState;
26
+ attempts: number;
27
+ maxNotifications: number;
28
+ error?: "queue_delivery_unknown" | "snapshot_failed" | undefined;
29
+ bindings: {
30
+ workspaceId: string;
31
+ memberId: string;
32
+ notifiedThroughSeq: number | undefined;
33
+ }[];
34
+ };
35
+ pause(): void;
36
+ resume(): void;
37
+ stop(): void;
38
+ tick(): Promise<void>;
39
+ private performTick;
40
+ }
@@ -0,0 +1,59 @@
1
+ export interface PiInteractionResponse {
2
+ sessionId: string;
3
+ requestId: string;
4
+ response: {
5
+ cancelled: true;
6
+ } | {
7
+ confirmed: boolean;
8
+ } | {
9
+ value: string;
10
+ };
11
+ }
12
+ export interface PiTeamSessionOptions {
13
+ workspaceId: string;
14
+ cwd: string;
15
+ sessionDir: string;
16
+ provider: string;
17
+ model: string;
18
+ systemPrompt: string;
19
+ mcpConfig: Record<string, unknown>;
20
+ extensionPaths?: readonly string[];
21
+ onEvent: (event: Record<string, unknown>) => void;
22
+ }
23
+ /** One owned RPC child; GUI replies never share a model-controlled tool channel. */
24
+ export declare class PiTeamSession {
25
+ private readonly options;
26
+ private client;
27
+ private sessionId;
28
+ private revision;
29
+ private phase;
30
+ private readonly interactions;
31
+ private readonly replying;
32
+ private stopping;
33
+ private active;
34
+ private pendingInputs;
35
+ private readonly privateFiles;
36
+ private constructor();
37
+ static start(options: PiTeamSessionOptions): Promise<PiTeamSession>;
38
+ status(): {
39
+ sessionId: string | undefined;
40
+ phase: "closed" | "failed" | "open" | "starting" | "waiting";
41
+ revision: number;
42
+ pendingUi: {
43
+ id: string | undefined;
44
+ method: unknown;
45
+ responding: boolean;
46
+ }[];
47
+ };
48
+ private fail;
49
+ private onFrame;
50
+ private onInteraction;
51
+ private request;
52
+ private state;
53
+ ready(): Promise<boolean>;
54
+ notify(throughSeq: number, signal: AbortSignal): Promise<string>;
55
+ sendInput(message: string): Promise<string>;
56
+ respond(input: PiInteractionResponse): Promise<void>;
57
+ drain(signal: AbortSignal): Promise<void>;
58
+ stop(): Promise<void>;
59
+ }
@@ -31,6 +31,14 @@ export declare function resolveAgentEgressPolicy(policy: AgentEgressPolicy | und
31
31
  * Default activity projection. Every retained string is SDK-authored; no
32
32
  * runtime trajectory, tool, prompt, environment, argv, path, or credential
33
33
  * value survives this transformation.
34
+ *
35
+ * Each case CONSTRUCTS a fresh event from SDK-authored literals rather than
36
+ * editing the incoming one, which is what makes the guarantee total rather
37
+ * than a list of fields someone remembered to strip. `spill` on
38
+ * `tool_use`/`tool_result` is covered by exactly that: a `BlobRef` is a
39
+ * readable locator for the omitted tool payload — content, not metadata — so
40
+ * it never survives a metadata-status projection, and neither do the byte
41
+ * counts that would leak the payload's size.
34
42
  */
35
43
  export declare function metadataStatusEvent(event: AgentEvent): AgentEvent;
36
44
  export declare function eventBytes(event: AgentEvent): number;
@@ -25,6 +25,11 @@ export interface ConnectionManagerOptions {
25
25
  */
26
26
  onEnvelope: (envelope: Envelope) => void | Promise<void>;
27
27
  onStateChange?: (state: ConnectionState) => void;
28
+ /** Await durable disposition before retiring the exact accepted/rejected bytes. */
29
+ onOutboundAccepted?: (envelopes: readonly Envelope[]) => Promise<void>;
30
+ onOutboundRejected?: (envelope: Envelope) => Promise<void>;
31
+ onOutboundQueued?: (envelope: Envelope) => void;
32
+ beforeOutboundPost?: (envelopes: readonly Envelope[]) => void;
28
33
  /** Backoff between failed long-poll HTTP attempts. Default 2s. */
29
34
  longPollRetryDelayMs?: number;
30
35
  /** Minimum delay before the next long-poll request after an empty (no-events) response. Default 250ms. */
@@ -334,106 +339,7 @@ export declare class ConnectionManager {
334
339
  /** Design A: eagerly advance the in-memory delivery watermark — called for every `task.*` envelope `deliver()` admits past dedup, regardless of transport or of whether its handler has even started yet. */
335
340
  private noteDelivered;
336
341
  private process;
337
- /**
338
- * M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
339
- * for a batch entry it could not parse into a known `Envelope` at all (an
340
- * unrecognized message type (see `long-poll-transport.ts`'s own doc
341
- * comment on `parseLooseEventsPollResponse`) but which still carried a numeric,
342
- * task-class envelope-level `seq` (the caller only invokes this for a
343
- * `task.`-prefixed type — see `long-poll-transport.ts`'s own
344
- * `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
345
- * here at all, mirroring F2's "conn.* is never cursor-tracked" rule).
346
- * There is no real `Envelope` to hand to a handler — a genuinely
347
- * unrecognized type has nothing this build could ever act on.
348
- *
349
- * GATEKEEPER-CAUGHT REGRESSION (fixed here): this used to call
350
- * `advanceCursor(seq)` DIRECTLY, synchronously, the instant a skip was
351
- * detected in `LongPollClient.loop()`'s per-entry for-loop. That is NOT
352
- * "instantaneous and race-free" the way the previous version of this
353
- * comment claimed — the hazard was never the skip racing against itself,
354
- * it was the skip racing AHEAD of an EARLIER real envelope in the SAME
355
- * batch that is still in flight on `processingChain` (`deliver()`, above,
356
- * only ever CHAINS `process()` onto that promise chain — it never awaits
357
- * it before returning). Concretely, batch `[real seq1, unknown seq2]`:
358
- * `deliver(seq1)` chains `process(seq1)` but returns immediately without
359
- * running it; the for-loop then reaches `seq2` and (pre-fix) called
360
- * `advanceCursor(2)` synchronously, BEFORE `process(seq1)` had even
361
- * started, let alone failed. If `seq1`'s handler then failed,
362
- * `stalledAtSeq` became 1 — but the durable cursor was already 2, so
363
- * `dedupWatermark()` returned 2, and every future redelivery of seq1 was
364
- * dedup-dropped as "already past the cursor" forever: permanent envelope
365
- * loss, exactly the F3 bug class the whole `stalledAtSeq`/frozen-watermark
366
- * mechanism exists to prevent.
367
- *
368
- * Fix: the cursor-advancing half is now CHAINED onto `processingChain`
369
- * too, exactly like `process()`'s own post-handler bookkeeping — so it
370
- * only ever runs once every earlier envelope already queued ahead of it
371
- * has fully settled (success or failure), and can observe `stalledAtSeq`'s
372
- * REAL, up-to-date value rather than whatever it happened to be at the
373
- * instant the skip was first noticed. The guard mirrors `process()`'s own
374
- * success-path guard exactly: never advance past a still-unresolved
375
- * earlier failure, unless (degenerate, cannot really happen for a skip)
376
- * this exact seq IS the stalled one.
377
- *
378
- * `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
379
- * called immediately, unconditionally, regardless of `stalledAtSeq` —
380
- * matching `deliver()`'s own eager, unconditional call for a real
381
- * envelope: its only job is "don't re-dispatch something already handed off,"
382
- * independent of outcome, and that property does not depend on FIFO
383
- * ordering the way the DURABLE cursor does.
384
- *
385
- * Deliberately NO top-level `dedupWatermark() <= seq` early-return before
386
- * queuing the chained callback (an earlier draft of this fix had one, and
387
- * it was itself subtly wrong): `deliveredSeq` can already reflect a seq
388
- * from the FIRST time it was ever seen, while the DURABLE cursor is still
389
- * behind it because a stall intervened before that seq's chained
390
- * advancement ran — a pre-check keyed on `deliveredSeq` would then
391
- * wrongly treat a LATER redelivery of the same seq (arriving once the
392
- * stall has since cleared) as "already accounted for" and never queue
393
- * another attempt, permanently stranding the cursor one seq short. Always
394
- * queuing is safe and cheap: `advanceCursor`'s own `seq <= this.cursor`
395
- * guard already makes a genuinely-redundant call a no-op, so there is no
396
- * correctness reason to short-circuit earlier, only a (here, unnecessary)
397
- * micro-optimization one.
398
- */
399
- private noteSkippedSeq;
400
- /**
401
- * Finding R1 (cross-model re-review — was NOT-CLOSED against F1):
402
- * `LongPollClient` calls this for a batch entry whose `type` it
403
- * recognized but whose payload failed schema validation
404
- * ({@link EnvelopeValidationError}) — a genuine delivery failure at that
405
- * seq, unlike `noteSkippedSeq`'s forward-compat case. Deliberately mirrors
406
- * `process()`'s own catch block (`if (tracked && this.stalledAtSeq ===
407
- * undefined) this.stalledAtSeq = envelope.seq;`) as closely as possible:
408
- * the SAME "only the lowest unresolved failure holds the stall" rule, the
409
- * SAME resulting freeze of `dedupWatermark()` at the durable cursor
410
- * (protocol §9 keeps this seq alive), and — because it's the SAME
411
- * `stalledAtSeq` field `process()`'s own post-success guard already
412
- * checks — anything ELSE delivered after this seq (same batch or a later
413
- * one) is automatically held back from advancing the cursor too, with
414
- * zero changes needed to `process()` itself.
415
- *
416
- * Chained onto `processingChain` for exactly the reason `noteSkippedSeq`
417
- * documents for its own identical chaining (see that method's sibling
418
- * doc comment on `LongPollClient`, "GATEKEEPER-CAUGHT REGRESSION"): an
419
- * EARLIER real envelope in the SAME batch may still be in flight on that
420
- * FIFO chain when this is called (`deliver()` only ever chains
421
- * `process()` onto it, never awaits before returning) — mutating
422
- * `stalledAtSeq` synchronously here could race ahead of that still-
423
- * unresolved earlier envelope. Chaining instead guarantees this only
424
- * takes effect once every earlier-queued envelope has already settled,
425
- * and reads `stalledAtSeq`'s real, up-to-date value rather than whatever
426
- * it happened to be the instant the failure was first noticed.
427
- *
428
- * No `noteDelivered` call here (contrast `noteSkippedSeq`, which does
429
- * call it): a validation-failed entry never becomes a real `Envelope` and
430
- * never reaches `deliver()`, so it was never "delivered" in the eager
431
- * in-memory-watermark sense that field tracks — there is nothing for it
432
- * to eagerly mark. Once a corrected redelivery of this exact seq DOES
433
- * arrive as a real envelope, it flows through the ordinary `deliver()`
434
- * path (which calls `noteDelivered` itself) and, on success, clears the
435
- * stall via `process()`'s own existing logic — no special-casing needed.
436
- */
342
+ /** Serialized with handler completion, so invalid work cannot be acked by later success. */
437
343
  private noteValidationFailure;
438
344
  private advanceCursor;
439
345
  private quarantineRejectedOutbound;
@@ -465,5 +465,7 @@ export declare function parseTeamWorkspaceJoinParams(value: unknown): TeamWorksp
465
465
  export declare function parseTeamContextParams(value: unknown): TeamContextParams | undefined;
466
466
  export declare function parseTeamMessagePostParams(value: unknown): TeamMessagePostParams | undefined;
467
467
  export declare function parseTeamMessageReadParams(value: unknown): TeamMessageReadParams | undefined;
468
+ /** Exact local operator RPC; the shared read-parameter shape has no model identity fields. */
469
+ export declare function parseTeamNotificationSnapshotParams(value: unknown): TeamMessageReadParams | undefined;
468
470
  export declare function parseTeamMessageAckParams(value: unknown): TeamMessageAckParams | undefined;
469
471
  export declare function parseTeamMessageInspectParams(value: unknown): TeamMessageInspectParams | undefined;
@@ -307,6 +307,36 @@ export interface DaemonConfig {
307
307
  * explicitly instead to opt out of enforcement altogether.
308
308
  */
309
309
  maxTaskOutputBytes?: number;
310
+ /**
311
+ * Per-EVENT inline ceiling (default {@link DEFAULT_MAX_INLINE_EVENT_BYTES},
312
+ * 64 KiB) for the two `AgentEvent` fields a runtime authors freely:
313
+ * `tool_use.input` and `tool_result.output`. An event whose serialization
314
+ * exceeds this leaves `TaskRunner.pump` with that field replaced by a
315
+ * UTF-8-safe head/tail preview (`{ preview: { head, tail } }`) and an
316
+ * additive `spill` descriptor; the full JSON serialization is uploaded to
317
+ * the blob plane under an idempotent, content-addressed key, and
318
+ * `spill.blob` is where a consumer reads it back. If the upload fails the
319
+ * preview still ships, carrying `spill.unstoredReason` instead — omission
320
+ * is always described, never silent.
321
+ *
322
+ * This is a per-event bound, orthogonal to `maxTaskOutputBytes` (a
323
+ * whole-task total, counted AFTER spilling) and to
324
+ * `progressBatch.maxBatchBytes` (a per-batch wire budget).
325
+ *
326
+ * **Consumer contract:** `spill`'s presence is the only signal that the
327
+ * inline field is a preview. A consumer that renders `tool_result.output`
328
+ * without checking `spill` renders a truncation as the whole result.
329
+ *
330
+ * Must be a positive safe integer of at least
331
+ * {@link MIN_MAX_INLINE_EVENT_BYTES} (4096) — below that a legitimate
332
+ * `spill` descriptor no longer fits inside the cap it exists to enforce.
333
+ * Anything else (0, negative, non-integer, `NaN`,
334
+ * `Number.POSITIVE_INFINITY`) is a config validation error thrown
335
+ * synchronously from `createDaemonWithAdapters`/`createDaemon`; there is
336
+ * no opt-out, because "unbounded event" is exactly the state this exists
337
+ * to prevent.
338
+ */
339
+ maxInlineEventBytes?: number;
310
340
  /**
311
341
  * Host-owned batching policy for normalized `task.progress` events.
312
342
  * `maxBatchBytes`, when set, measures exactly the UTF-8 bytes of
@@ -559,6 +589,8 @@ export interface Daemon {
559
589
  }
560
590
  /** Internal seam so tests can substitute stub adapters / faster batch and long-poll timing. `createDaemonWithAdapters` (which takes this) is also the real entry point for products supplying a hand-built adapter set `createDaemon` can't construct on its own — e.g. custom adapter options, or an adapter that REPLACES a bundled runtime's implementation under the same id. Honest limit: an adapter id outside `pi`/`claude`/`codex` cannot pass wire validation today — `RuntimeIdSchema` (`@byok-sdk/protocol`) is a closed `z.enum(['pi', 'claude', 'codex'])`, and `isRuntimeId` filtering below (see `detectRuntimes`) drops any detected adapter outside that set before it ever reaches a wire-visible field. A genuinely fourth/namespaced runtime id is a future protocol change, not something this seam enables today. */
561
591
  export interface DaemonOverrides {
592
+ /** Test-only synchronous kill points; never supplied by production configuration. */
593
+ executionRecoveryFault?: (step: 'terminal:before-send' | 'terminal:queued' | 'outbound:before-post' | 'outbound:after-ack') => void;
562
594
  /** M4 Phase 3: overrides `TaskRunner`'s default out-of-band approval wait (`DEFAULT_APPROVAL_TIMEOUT_MS`, 10 minutes) before an unanswered `requestApproval` force-resolves as a fail-closed rejection. */
563
595
  approvalTimeoutMs?: number;
564
596
  /** Finding F5: overrides for the control-socket shutdown path's own bounded waits — see `TaskRunner.shutdownTask`'s and `ConnectionManager.stop`'s own doc comments. Both default to 5s; neither affects an ordinary (non-shutdown-RPC) `daemon.stop()` call. */
@@ -0,0 +1,90 @@
1
+ import type { AgentEvent } from '@byok-sdk/protocol';
2
+ import type { BlobResolver } from './blob-client';
3
+ /**
4
+ * Default per-event inline ceiling (64 KiB) for the two `AgentEvent` variants
5
+ * that carry runtime-authored payloads (`tool_use.input`,
6
+ * `tool_result.output`) — see `DaemonConfig.maxInlineEventBytes`
7
+ * (`create-daemon.ts`) for the host-facing contract.
8
+ *
9
+ * 64 KiB is the same threshold `sendArtifact` already uses to decide inline
10
+ * vs. blob for an artifact (`MAX_INLINE_ARTIFACT_BYTES`): one number for "a
11
+ * payload this daemon is willing to put on the activity wire".
12
+ */
13
+ export declare const DEFAULT_MAX_INLINE_EVENT_BYTES: number;
14
+ /**
15
+ * Smallest accepted `maxInlineEventBytes`. Below this a legitimate spill
16
+ * descriptor (`field` + byte counts + a `BlobRef` whose `blobId` is chosen by
17
+ * the server, plus the event's own `type`/`tool`/`toolCallId`) stops fitting
18
+ * inside the cap it is supposed to keep the event under, which would turn a
19
+ * host's configuration mistake into a per-event runtime invariant failure.
20
+ * Enforced up front at `DaemonConfig` validation, never here.
21
+ */
22
+ export declare const MIN_MAX_INLINE_EVENT_BYTES = 4096;
23
+ /**
24
+ * Worst-case JSON cost of the `,"spill":{…}` fragment when the descriptor
25
+ * carries an `unstoredReason` rather than a server-chosen `BlobRef`.
26
+ *
27
+ * Every part is bounded by construction, which a `BlobRef` is not — its
28
+ * `blobId` is an arbitrary-length server-chosen string:
29
+ *
30
+ * ```
31
+ * ,"spill": 9
32
+ * { 1
33
+ * "field":"output", 17 ("output" is the longer of the two)
34
+ * "totalBytes":<=16 digits>, 30
35
+ * "omittedBytes":<=16 digits>, 32 (never exceeds totalBytes)
36
+ * "contentType":"application/json", 33 (a module constant)
37
+ * "unstoredReason": 17
38
+ * "<=512 bytes of escaped reason>" 514 (MAX_UNSTORED_REASON_BYTES + 2 quotes)
39
+ * } 1
40
+ * ----
41
+ * 654
42
+ * ```
43
+ *
44
+ * Rounded up to 768 so digit-count growth cannot invalidate it. Because the
45
+ * event is spread first and `spill` written last, a bounded event is EXACTLY
46
+ * the empty-preview skeleton plus this fragment — so refusing to spill unless
47
+ * `skeleton + MAX_SPILL_DESCRIPTOR_BYTES <= maxInlineBytes` is what makes the
48
+ * final cap check unreachable rather than merely unlikely. `event-spill.test.ts`
49
+ * asserts this against a maximally escaping reason instead of trusting the
50
+ * comment.
51
+ */
52
+ export declare const MAX_SPILL_DESCRIPTOR_BYTES = 768;
53
+ export interface EventSpillDeps {
54
+ /** Effective inline ceiling for this daemon; already validated at the `DaemonConfig` layer. */
55
+ maxInlineBytes: number;
56
+ /** Only the upload half of `BlobResolver` is needed, so a test double stays minimal. */
57
+ blobClient: Pick<BlobResolver, 'uploadArtifact'>;
58
+ /** Scopes the upload's idempotency key to the task that produced the event. */
59
+ taskId: string;
60
+ /** Task lifecycle authority — aborting it stops the spill upload with the rest of the task's blob I/O. */
61
+ signal?: AbortSignal;
62
+ /** Diagnostic seam. Called only on a path that loses information (upload failure, or an event this policy cannot bound). */
63
+ log?: (message: string) => void;
64
+ }
65
+ /**
66
+ * Bound one normalized `AgentEvent` at the daemon's ingestion boundary
67
+ * (`TaskRunner.pump`).
68
+ *
69
+ * An event whose serialized form already fits `maxInlineBytes` is returned
70
+ * **as the same object reference** — the overwhelming majority of events pay
71
+ * exactly one `JSON.stringify` and nothing else, and no downstream identity
72
+ * comparison changes meaning.
73
+ *
74
+ * An oversized `tool_use` / `tool_result` has its runtime-authored field
75
+ * (`input` / `output`) uploaded to the blob plane in full and REPLACED inline
76
+ * by `{ preview: { head, tail } }`, with an additive `spill` descriptor
77
+ * carrying either the resulting `BlobRef` or a bounded `unstoredReason`. The
78
+ * replacement is *measured* against the cap, never assumed to fit: the
79
+ * preview budget is whatever is left after the rest of the event and the
80
+ * real descriptor, and it is shrunk until `JSON.stringify(result)` actually
81
+ * fits (JSON escaping can cost several bytes per source character, so the
82
+ * byte budget alone is not a bound).
83
+ *
84
+ * Storage failure is never silent and never fatal: the preview still ships,
85
+ * `unstoredReason` says why the omitted bytes are unreadable, and `log` is
86
+ * called. The runtime's own transcript still holds the content, so failing
87
+ * the task over a telemetry upload would trade a real result for an
88
+ * observability problem.
89
+ */
90
+ export declare function spillOversizedEvent(event: AgentEvent, deps: EventSpillDeps): Promise<AgentEvent>;
@@ -124,18 +124,21 @@ export interface LocalTransitionRecord {
124
124
  /** Whether the cloud has confirmed the terminal this daemon produced (§12.7.3's "terminal 生成后、truth 写入前" window). */
125
125
  export type TerminalTruthState = 'pending' | 'confirmed' | 'failed';
126
126
  /**
127
- * A task's terminal, as it exists locally. The PAYLOAD is not stored — only
128
- * its hash, plus enough retry state to know whether the cloud has taken it.
127
+ * One immutable canonical terminal and its delivery projection. Bytes are the
128
+ * replay authority; the hash is checked against them, never used as a substitute.
129
129
  */
130
130
  export interface LocalTerminalRecord {
131
131
  readonly taskId: string;
132
- readonly terminalType: 'complete' | 'failed' | 'cancelled';
132
+ readonly terminalType: 'complete' | 'failed' | 'cancelled' | 'declined';
133
+ readonly bytes: string;
133
134
  readonly payloadHash: string;
134
135
  readonly truthState: TerminalTruthState;
135
136
  /** How many times delivery to the cloud has been attempted. */
136
137
  readonly attempt: number;
137
138
  readonly lastError?: string;
138
139
  readonly recordedAt: string;
140
+ /** Committed atomically with the original interruption report. */
141
+ readonly recovery?: RecoveryOutcome;
139
142
  }
140
143
  /** A task the journal knows about that has no terminal and no recovery marker — i.e. one this daemon was in the middle of when it stopped. */
141
144
  export interface RecoverableTask {
@@ -147,6 +150,7 @@ export interface RecoverableTask {
147
150
  readonly claimedRuntime?: string;
148
151
  readonly workspaceRef?: string;
149
152
  readonly updatedAt: string;
153
+ readonly envelopeBytes: string;
150
154
  }
151
155
  /**
152
156
  * What recovery decided about a task. `interrupted` is the honest default for
@@ -262,6 +266,14 @@ export interface LocalTaskJournal {
262
266
  recordTransition(record: LocalTransitionRecord): Promise<void>;
263
267
  /** Record (or update the retry state of) a task's terminal. Idempotent by task id: a replay with the same payload hash is a no-op beyond retry bookkeeping. */
264
268
  recordTerminal(record: LocalTerminalRecord): Promise<void>;
269
+ /** Exact original terminal bytes, including rejected records, until acknowledged. */
270
+ listPendingTerminals(identity: JournalIdentity): Promise<LocalTerminalRecord[]>;
271
+ /** A successful authenticated transport disposition, bound to the original bytes. */
272
+ confirmTerminal(taskId: string, payloadHash: string): Promise<void>;
273
+ rejectTerminal(taskId: string, payloadHash: string, reason: string): Promise<void>;
274
+ /** Includes old interruption markers without reports; they must not hide pending work. */
275
+ listRecoveryTasks(identity: JournalIdentity): Promise<RecoverableTask[]>;
276
+ readTask(taskId: string, identity: JournalIdentity): Promise<RecoverableTask | undefined>;
265
277
  /** Tasks with no terminal and no recovery marker — what this daemon was in the middle of when it last stopped. */
266
278
  listRecoverable(): Promise<RecoverableTask[]>;
267
279
  /** Close out one recoverable task by writing its recovery marker. Never deletes; a marked row is on §12.7.2.1's never-auto-delete list. */
@@ -1,4 +1,4 @@
1
- import { type AdmissionRecord, type CategoryUsage, type CleanableCategory, type CleanupCandidate, type CleanupResult, type CompactOptions, type CompactResult, type JournalReceipt, type LocalStorageUsage, type LocalTaskJournal, type LocalTerminalRecord, type LocalTransitionRecord, type RecoverableTask, type RecoveryOutcome, type ReceivedEnvelopeRecord, type StorageCategory } from './journal';
1
+ import { type JournalIdentity, type AdmissionRecord, type CategoryUsage, type CleanableCategory, type CleanupCandidate, type CleanupResult, type CompactOptions, type CompactResult, type JournalReceipt, type LocalStorageUsage, type LocalTaskJournal, type LocalTerminalRecord, type LocalTransitionRecord, type RecoverableTask, type RecoveryOutcome, type ReceivedEnvelopeRecord, type StorageCategory } from './journal';
2
2
  import { JournalHandleCleanupError, type JournalOpenFaultSeam } from './sqlite-support';
3
3
  export { JournalHandleCleanupError };
4
4
  /** The single database file, per §12.7.2's "建议单库 `<storeDir>/daemon.db`". */
@@ -24,7 +24,7 @@ export declare const DEFAULT_JOURNAL_BUSY_TIMEOUT_MS = 5000;
24
24
  * (`util/secure-dir.ts`): a seam the production path never supplies, exercised
25
25
  * from any host.
26
26
  */
27
- export type JournalFaultStep = 'append:before-begin' | 'append:after-envelope' | 'append:after-task' | 'append:after-receipt' | 'append:before-commit' | 'admission:before-commit' | 'transition:before-commit' | 'terminal:before-commit' | 'recovery:before-commit' | 'cleanup:before-commit' | 'prune:before-commit';
27
+ export type JournalFaultStep = 'append:before-begin' | 'append:after-envelope' | 'append:after-task' | 'append:after-receipt' | 'append:before-commit' | 'append:after-commit' | 'admission:before-commit' | 'transition:before-commit' | 'terminal:before-commit' | 'terminal:after-commit' | 'recovery:after-commit' | 'confirm:before-commit' | 'confirm:after-commit' | 'recovery:before-commit' | 'cleanup:before-commit' | 'prune:before-commit';
28
28
  export interface JournalFaultSeam {
29
29
  /** Throw to simulate a crash or IO error at exactly this step. Return normally to proceed. */
30
30
  onStep?(step: JournalFaultStep): void;
@@ -79,6 +79,11 @@ export declare class SqliteLocalTaskJournal implements LocalTaskJournal {
79
79
  * first fact stands.
80
80
  */
81
81
  recordTerminal(record: LocalTerminalRecord): Promise<void>;
82
+ readTask(taskId: string, identity: JournalIdentity): Promise<RecoverableTask | undefined>;
83
+ listRecoveryTasks(identity: JournalIdentity): Promise<RecoverableTask[]>;
84
+ listPendingTerminals(identity: JournalIdentity): Promise<LocalTerminalRecord[]>;
85
+ confirmTerminal(taskId: string, payloadHash: string): Promise<void>;
86
+ rejectTerminal(taskId: string, payloadHash: string, reason: string): Promise<void>;
82
87
  /**
83
88
  * What this daemon was in the middle of: a task whose offer envelope is
84
89
  * durable, that has no terminal, that was not declined, and that recovery
@@ -49,56 +49,8 @@ export interface LongPollClientOptions {
49
49
  onRevoked?: () => void;
50
50
  /** Called when the server cannot replay the durable cursor supplied to this poll. */
51
51
  onReplayCursorTooOld?: (error: ReplayCursorTooOldError) => void;
52
- /**
53
- * M4 Phase 4 (version-negotiation drill fix), scope narrowed by finding F1:
54
- * called ONLY for a batch entry that failed to parse because its `type`
55
- * is entirely unrecognized (`parseMessage` throwing
56
- * {@link UnknownMessageTypeError}) and which still carries a
57
- * numeric envelope-level `seq` AND a recognizably task-class `type` (a
58
- * `task.` prefix — see `extractSkippableSeq`'s own doc comment for why a
59
- * `conn.*`-shaped or type-less entry is deliberately excluded, mirroring
60
- * F2's "conn.* is never cursor-tracked" rule), so the caller can advance
61
- * its cursor/watermark past it even though there is no real `Envelope` to
62
- * hand to `onEnvelope`. Without this, a persistently-redelivered
63
- * unrecognized-type entry (the real server retains and redelivers an
64
- * un-acked envelope, protocol §9) would keep reappearing at the same
65
- * cursor position forever.
66
- *
67
- * Finding F1: a RECOGNIZED type that fails schema validation
68
- * ({@link EnvelopeValidationError} — e.g. a `task.offer` whose
69
- * `PermissionPolicy` rejects an unknown constraint) is deliberately NOT
70
- * reported here. That failure is a genuinely malformed control message,
71
- * not forward-compat tolerance — forwarding its `seq` here would
72
- * permanently ack a message the daemon never actually understood (the
73
- * server would stop redelivering it, silently stranding whatever it was
74
- * offering). This callback being scoped to `UnknownMessageTypeError` only
75
- * preserves the no-silent-permanent-ack property. Optional
76
- * only for constructor/test convenience — `ConnectionManager` always
77
- * supplies it.
78
- */
79
- onSkippedSeq?: (seq: number) => void;
80
- /**
81
- * Finding R1 (cross-model re-review — the F1 fix alone was NOT-CLOSED):
82
- * called for a batch entry whose `type` WAS recognized but whose payload
83
- * failed schema validation ({@link EnvelopeValidationError}) — a genuine
84
- * delivery failure at that specific seq, not forward-compat tolerance
85
- * (contrast {@link onSkippedSeq}, which is scoped to the opposite case,
86
- * an entirely unrecognized type). F1's own fix — simply not forwarding
87
- * this seq to `onSkippedSeq` — turned out to be insufficient on its own:
88
- * a LATER valid envelope in the same or a later batch would still
89
- * silently advance the durable cursor PAST this seq once its own handler
90
- * succeeded, since nothing had told `ConnectionManager` this seq needed
91
- * the same stall treatment a thrown handler failure already gets — an
92
- * INDIRECT permanent ack, one hop removed from the exact bug F1 set out
93
- * to fix. `ConnectionManager` (`noteValidationFailure`) engages
94
- * `stalledAtSeq` for this seq the same way `process()`'s own catch block
95
- * does for a real thrown handler — freezing `dedupWatermark()` at the
96
- * durable cursor (so the server's retain-and-redeliver semantics,
97
- * protocol §9, keep this seq alive) and, via that SAME existing
98
- * machinery, holding back the cursor for anything else delivered after it
99
- * in the same batch too, exactly as a real handler failure already would.
100
- * Optional only for constructor/test convenience — `ConnectionManager`
101
- * always supplies it.
52
+ /** Unknown or malformed executable messages have no durable disposition.
53
+ * Freeze their sequence; a later valid message must not acknowledge them.
102
54
  */
103
55
  onValidationFailedSeq?: (seq: number) => void;
104
56
  /**
@@ -228,6 +228,8 @@ export interface TaskRunnerDeps {
228
228
  agentSessionHandoffs?: AgentSessionHandoffStore;
229
229
  deviceId: string;
230
230
  send: (envelope: Envelope) => void;
231
+ /** Fsync the execution commitment before claim/runtime side effects. */
232
+ beforeClaim?: (taskId: string, runtime: string) => Promise<void>;
231
233
  blobClient: BlobResolver;
232
234
  batcherOptions?: ProgressBatcherOptions;
233
235
  /**
@@ -330,6 +332,15 @@ export interface TaskRunnerDeps {
330
332
  * interface (`shutdownInterruptTimeoutMs`, `approvalTimeoutMs`).
331
333
  */
332
334
  maxTaskOutputBytes?: number;
335
+ /**
336
+ * Per-event inline ceiling for `tool_use.input` / `tool_result.output` —
337
+ * see `DaemonConfig.maxInlineEventBytes` (`create-daemon.ts`) for the full
338
+ * contract and {@link DEFAULT_MAX_INLINE_EVENT_BYTES} for the default.
339
+ * Validated (positive safe integer, at least
340
+ * `MIN_MAX_INLINE_EVENT_BYTES`) at the `DaemonConfig` layer, not here —
341
+ * this seam trusts its caller, same as `maxTaskOutputBytes` above.
342
+ */
343
+ maxInlineEventBytes?: number;
333
344
  /**
334
345
  * M4 (additive-minor, `task.approval_resolved`): the capabilities advertised
335
346
  * by the CURRENT transport's server (`conn.ack` on WS, the latest successful
@@ -569,6 +580,8 @@ export declare class TaskRunner {
569
580
  usesAgentEgress(taskId: string): boolean;
570
581
  /** M5 batch-3 (workstream 2): effective `maxTaskOutputBytes` cap for this daemon — see {@link DEFAULT_MAX_TASK_OUTPUT_BYTES}'s own doc comment. */
571
582
  private get maxTaskOutputBytes();
583
+ /** Effective per-event inline ceiling for this daemon — see `DaemonConfig.maxInlineEventBytes`. */
584
+ private get maxInlineEventBytes();
572
585
  /** WP0: effective per-canonical-Agent-home Attempt cap — see {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}. */
573
586
  private get maxConcurrentMutableSessionsPerAgentHome();
574
587
  /**
@@ -191,6 +191,15 @@ export declare class LocalTeamWorkspace {
191
191
  expiresAt: string;
192
192
  }>>;
193
193
  postMessage(input: TeamPostMessageInput): Promise<TeamMessageAcceptedReceipt>;
194
+ /** Metadata-only operator notification view. Never advances delivery or acknowledgement. */
195
+ notificationSnapshot(input: TeamReadMessagesInput): Promise<{
196
+ workspaceId: string;
197
+ memberId: string;
198
+ registryRevision: TeamWorkspaceRevision;
199
+ expiresAt: string;
200
+ acknowledgedThroughSeq: number;
201
+ latestPeerSeq: number | null;
202
+ }>;
194
203
  readMessages(input: TeamReadMessagesInput): Promise<TeamReadMessagesResult>;
195
204
  ackMessages(input: TeamAckMessagesInput): Promise<TeamAckReceipt>;
196
205
  private resolveLease;