@code-yeongyu/senpi 2026.10.26-1 → 2026.10.26-2

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 (148) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/bundle/chunks/{anthropic-messages-M235FJQV.js → anthropic-messages-ODSFJCXR.js} +1 -1
  3. package/dist/bundle/chunks/{args-FBNJOJSR.js → args-SWKJAYQD.js} +1 -1
  4. package/dist/bundle/chunks/{azure-openai-responses-YQRTQ7H6.js → azure-openai-responses-REGXS4XO.js} +1 -1
  5. package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
  6. package/dist/bundle/chunks/chunk-2XF6W6F6.js +410 -0
  7. package/dist/bundle/chunks/{chunk-DMFAVE5N.js → chunk-4QVORFQ2.js} +1 -1
  8. package/dist/bundle/chunks/{chunk-ZXHLH4MW.js → chunk-5W3DTM2J.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-QREKAXKX.js → chunk-6GAMRAVM.js} +5 -5
  10. package/dist/bundle/chunks/{chunk-MSHRIZGT.js → chunk-7IZH2KQX.js} +3 -3
  11. package/dist/bundle/chunks/{chunk-PH3JDRB2.js → chunk-ARSENVDY.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-5KZH6EYQ.js → chunk-CNDPCYNT.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-DYWIFF2Q.js → chunk-CYOHFREV.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-3LPWILUZ.js → chunk-DNDZPKIQ.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-ZLIVZQDY.js → chunk-EQNUL2WB.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-5FQMNHXV.js → chunk-ESNSRUW4.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-LO5MYLGF.js → chunk-FQDARFHT.js} +1 -1
  18. package/dist/bundle/chunks/{chunk-7KOUQSBX.js → chunk-GPPOPK76.js} +3 -3
  19. package/dist/bundle/chunks/chunk-I2WFLKZ2.js +5 -0
  20. package/dist/bundle/chunks/{chunk-I7QWTWVZ.js → chunk-IHKHCWWG.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-MY4ZD3EW.js → chunk-IY4G44MN.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-OQ3WDMB3.js → chunk-JH7MVRNJ.js} +1 -1
  23. package/dist/bundle/chunks/chunk-KBSFTXAT.js +2 -0
  24. package/dist/bundle/chunks/{chunk-UW6M3EXY.js → chunk-KRO63T4Q.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-J5FCH7TI.js → chunk-KTDIIF2F.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-3WP5RFQR.js → chunk-NDNIMC5W.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-CDQPA4FP.js → chunk-ODRGPC5S.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-3A3XCLNH.js → chunk-PDG4XWO2.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-QYFOHFPS.js → chunk-PPECSSWX.js} +1 -1
  30. package/dist/bundle/chunks/{chunk-5LWUUXMH.js → chunk-PY4ZQHTN.js} +1 -1
  31. package/dist/bundle/chunks/{chunk-EZP6AYW2.js → chunk-RPW3XQW3.js} +22 -22
  32. package/dist/bundle/chunks/{chunk-SOXQ42VS.js → chunk-RQ5JTXUC.js} +1 -1
  33. package/dist/bundle/chunks/{chunk-JFQUWEN7.js → chunk-TQ67PB7U.js} +2 -2
  34. package/dist/bundle/chunks/{chunk-6U3TBFBA.js → chunk-UJPG4ROT.js} +1 -1
  35. package/dist/bundle/chunks/{chunk-UDM7VO6O.js → chunk-XM36IFP3.js} +1 -1
  36. package/dist/bundle/chunks/{chunk-XQ45JMGN.js → chunk-Z6BREXV3.js} +1 -1
  37. package/dist/bundle/chunks/{chunk-ADQAPDPV.js → chunk-ZP6JJDYV.js} +1 -1
  38. package/dist/bundle/chunks/{cli-main-55EQA5PS.js → cli-main-Q2GA53XM.js} +1 -1
  39. package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-ZJQGIIRC.js → cloudflare-workers-ai-system-one-WDHT7BBV.js} +1 -1
  40. package/dist/bundle/chunks/cursor-agent.js +1 -1
  41. package/dist/bundle/chunks/devin-agent.js +1 -1
  42. package/dist/bundle/chunks/{dispatch-C2IBP5VV.js → dispatch-5R3TYAYB.js} +3 -3
  43. package/dist/bundle/chunks/{google-generative-ai-5G4RIQA6.js → google-generative-ai-FHYFXKLA.js} +1 -1
  44. package/dist/bundle/chunks/{google-vertex-ALD4P27O.js → google-vertex-5DWE5U5K.js} +1 -1
  45. package/dist/bundle/chunks/{help-fast-path-PEIKGCKZ.js → help-fast-path-FETLIF3W.js} +1 -1
  46. package/dist/bundle/chunks/{host-command-657VCQ34.js → host-command-XEB7SHT3.js} +1 -1
  47. package/dist/bundle/chunks/{host-session-command-OV5XDOJK.js → host-session-command-FFN52LXJ.js} +1 -1
  48. package/dist/bundle/chunks/{interactive-mode-MLXSYK2H.js → interactive-mode-5DEKY3YW.js} +1 -1
  49. package/dist/bundle/chunks/{mistral-conversations-RAE4K2XY.js → mistral-conversations-I2PI7DDF.js} +1 -1
  50. package/dist/bundle/chunks/{multi-session-host-774MACJU.js → multi-session-host-OPR5VBFO.js} +3 -3
  51. package/dist/bundle/chunks/{openai-codex-responses-3CW4R7TG.js → openai-codex-responses-WSEX6SH7.js} +1 -1
  52. package/dist/bundle/chunks/{openai-completions-5UIJXQUF.js → openai-completions-YCBHTBSS.js} +1 -1
  53. package/dist/bundle/chunks/{openai-responses-2BO44MRW.js → openai-responses-GXNSQF4A.js} +1 -1
  54. package/dist/bundle/chunks/{package-manager-cli-MZHVCX2S.js → package-manager-cli-W55E3LHP.js} +1 -1
  55. package/dist/bundle/chunks/{pi-messages-RF6GER4R.js → pi-messages-O6GISIRW.js} +1 -1
  56. package/dist/bundle/chunks/{role-ETSFSDTS.js → role-SHIZQKK4.js} +1 -1
  57. package/dist/bundle/chunks/{rotation-stream-ZMQB4GPO.js → rotation-stream-2PCZWNKF.js} +1 -1
  58. package/dist/bundle/chunks/{rpc-mode-UJXOIZCW.js → rpc-mode-IDUJTSYS.js} +1 -1
  59. package/dist/bundle/chunks/{schedule-command-KA7LFFNK.js → schedule-command-B7RRSANU.js} +1 -1
  60. package/dist/bundle/chunks/{session-control-endpoint-MFWXUR7Z.js → session-control-endpoint-LMZH5JLH.js} +1 -1
  61. package/dist/bundle/chunks/{session-picker-BP33NRSL.js → session-picker-DTGBOXDR.js} +1 -1
  62. package/dist/bundle/chunks/session-worker.js +41 -41
  63. package/dist/bundle/chunks/{typesafe-system-one-GNNZ5O5U.js → typesafe-system-one-XGYJJVSE.js} +1 -1
  64. package/dist/bundle/cli.js +2 -2
  65. package/dist/bundle/index.js +1 -1
  66. package/dist/bundle/rpc-entry.js +1 -1
  67. package/dist/bundle/runtime-manifest.json +1 -1
  68. package/dist/core/agent-session.d.ts +11 -1
  69. package/dist/core/agent-session.js +19 -2
  70. package/dist/core/extensions/builtin/compaction/index.js +7 -1
  71. package/dist/core/extensions/builtin/herdr/index.js +9 -5
  72. package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.d.ts +6 -0
  73. package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.js +25 -0
  74. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +3 -3
  75. package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +50 -19
  76. package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +4 -3
  77. package/dist/core/extensions/builtin/mcp/auth/oauth.js +19 -12
  78. package/dist/core/extensions/builtin/mcp/modern/dispatch.js +1 -0
  79. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +4 -1
  80. package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +33 -35
  81. package/dist/core/extensions/builtin/mcp/modern/request.js +3 -0
  82. package/dist/core/extensions/builtin/mcp/modern/transport.d.ts +2 -0
  83. package/dist/core/extensions/builtin/mcp/modern/transport.js +4 -2
  84. package/dist/core/extensions/builtin/terminal/extension-state.js +2 -1
  85. package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +9 -0
  86. package/dist/core/extensions/builtin/vertex-model-garden/auth.js +17 -1
  87. package/dist/core/extensions/builtin/websearch/websearch/providers/google.d.ts +6 -0
  88. package/dist/core/extensions/builtin/websearch/websearch/providers/google.js +6 -1
  89. package/dist/core/extensions/types.d.ts +2 -0
  90. package/dist/core/lifecycle-children.d.ts +3 -1
  91. package/dist/core/lifecycle-children.js +79 -15
  92. package/dist/core/lifecycle-record.js +1 -24
  93. package/dist/core/lifecycle-session-binding.js +2 -1
  94. package/dist/core/lifecycle-wait-items.d.ts +17 -0
  95. package/dist/core/lifecycle-wait-items.js +72 -0
  96. package/dist/core/model-runtime.js +6 -1
  97. package/dist/core/provider-api-key-auth.d.ts +6 -1
  98. package/dist/core/provider-api-key-auth.js +11 -4
  99. package/dist/core/provider-composer.d.ts +8 -1
  100. package/dist/core/provider-composer.js +7 -3
  101. package/dist/core/sdk.js +4 -1
  102. package/dist/core/session-activity.js +2 -0
  103. package/dist/core/session-lifecycle-types.d.ts +20 -4
  104. package/dist/core/session-lifecycle.js +35 -4
  105. package/dist/core/structured-session-lane.d.ts +33 -0
  106. package/dist/core/structured-session-lane.js +62 -0
  107. package/dist/core/work-custody-record.d.ts +2 -1
  108. package/dist/core/work-custody-record.js +2 -0
  109. package/dist/core/work-obligations.d.ts +1 -1
  110. package/dist/core/work-obligations.js +8 -2
  111. package/dist/modes/json-event.d.ts +79 -1
  112. package/dist/modes/json-event.js +79 -1
  113. package/dist/modes/rpc/compact-content-index.d.ts +85 -0
  114. package/dist/modes/rpc/compact-content-index.js +254 -0
  115. package/dist/modes/rpc/compact-content-store.d.ts +6 -43
  116. package/dist/modes/rpc/compact-content-store.js +22 -157
  117. package/dist/modes/rpc/compact-index-pages.d.ts +12 -0
  118. package/dist/modes/rpc/compact-index-pages.js +31 -0
  119. package/dist/modes/rpc/compact-session-state.d.ts +11 -39
  120. package/dist/modes/rpc/compact-session-state.js +17 -26
  121. package/dist/modes/rpc/compact-spool-dir.d.ts +4 -1
  122. package/dist/modes/rpc/compact-spool-dir.js +41 -8
  123. package/dist/modes/rpc/compact-spool-storage.d.ts +53 -0
  124. package/dist/modes/rpc/compact-spool-storage.js +162 -0
  125. package/dist/modes/rpc/compact-storage-io.d.ts +2 -1
  126. package/dist/modes/rpc/compact-storage-io.js +7 -1
  127. package/dist/modes/rpc/compact-transition.d.ts +41 -0
  128. package/dist/modes/rpc/compact-transition.js +12 -0
  129. package/dist/modes/rpc/connection-handler.d.ts +2 -0
  130. package/dist/modes/rpc/connection-handler.js +12 -6
  131. package/dist/modes/rpc/host-ensure-start.js +5 -3
  132. package/dist/modes/rpc/security-presentations.js +2 -1
  133. package/dist/modes/rpc/session-binding.js +10 -0
  134. package/dist/modes/rpc/session-event-writer.d.ts +2 -0
  135. package/dist/modes/rpc/session-event-writer.js +2 -0
  136. package/dist/modes/rpc/session-record-stream.d.ts +3 -3
  137. package/dist/modes/rpc/session-record-stream.js +3 -3
  138. package/dist/modes/rpc/session-structured-lanes.d.ts +12 -0
  139. package/dist/modes/rpc/session-structured-lanes.js +24 -0
  140. package/dist/modes/rpc/session-worker-client.js +6 -5
  141. package/dist/modes/rpc/structured-session-records.d.ts +12 -0
  142. package/dist/modes/rpc/structured-session-records.js +88 -0
  143. package/docs/extensions.md +7 -1
  144. package/docs/rpc.md +17 -2
  145. package/package.json +6 -6
  146. package/dist/bundle/chunks/chunk-GQJYNFMO.js +0 -2
  147. package/dist/bundle/chunks/chunk-VICDRG3J.js +0 -410
  148. package/dist/bundle/chunks/chunk-VL7O5PO4.js +0 -5
@@ -0,0 +1,162 @@
1
+ import { join } from "node:path";
2
+ import { createSpoolDir, removeSpoolDir } from "./compact-spool-dir.js";
3
+ import { CompactStorageReadError, describeCause, } from "./compact-storage-io.js";
4
+ /**
5
+ * The spool file path of compact current-state storage (senpi#4272, R3.C6.4): one serial
6
+ * asynchronous job chain, the private epoch directory, spool file writes, reads and reclamation,
7
+ * and the failure ledger. `CompactContentStore` adds leaf ownership and spilling on top.
8
+ */
9
+ export const RECLAIM_WORK_ALLOWANCE_BYTES = 1024 * 1024;
10
+ const RECLAIM_RETRY_DELAYS_MS = [100, 400, 1_600, 6_400, 25_600];
11
+ const FAILURE_HISTORY = 32;
12
+ export class CompactSpoolStorage {
13
+ constructor(dir, io) {
14
+ this.failures = [];
15
+ this.nextFile = 1;
16
+ this.chain = Promise.resolve();
17
+ this.queued = 0;
18
+ this.spillQueued = false;
19
+ this.halted = false;
20
+ this.retired = false;
21
+ this.pendingIoCopyBytes = 0;
22
+ this.spillBytesWritten = 0;
23
+ this.reclaimDebtBytes = 0;
24
+ this.dir = dir;
25
+ this.io = io;
26
+ }
27
+ /** Resolves once every queued storage job, including the ones they queued, has completed. */
28
+ async idle() {
29
+ while (this.queued > 0)
30
+ await this.chain;
31
+ }
32
+ /**
33
+ * Invalidates spilling, joins owned I/O, then removes the epoch directory only when this store
34
+ * created it and it still passes the ownership checks; otherwise rejects with `CompactSpoolRefusedError`.
35
+ */
36
+ async retire() {
37
+ this.retired = true;
38
+ await this.idle();
39
+ const token = this.spoolToken;
40
+ this.spoolToken = undefined;
41
+ if (token !== undefined)
42
+ await removeSpoolDir(this.dir, token);
43
+ }
44
+ /** Reads a committed file and rejects a missing, unreadable or short one. */
45
+ async readFile(path, bytes, operation) {
46
+ let data;
47
+ try {
48
+ data = await this.io.readFile(path);
49
+ }
50
+ catch (cause) {
51
+ throw this.readFailure(operation, path, cause);
52
+ }
53
+ if (data.byteLength !== bytes) {
54
+ throw this.readFailure(operation, path, new Error(`expected ${bytes} bytes, found ${data.byteLength}`));
55
+ }
56
+ return data;
57
+ }
58
+ /** Writes one new spool file; undefined means the write failed and was reported (a spill write also halts spilling). */
59
+ async writeFile(data, operation, retainedInMemoryBytes) {
60
+ const path = await this.ensureDir(retainedInMemoryBytes);
61
+ if (path === undefined)
62
+ return undefined;
63
+ const file = join(path, `${operation === "spill_write" ? "leaf" : "page"}-${this.nextFile++}.bin`);
64
+ this.pendingIoCopyBytes += data.byteLength;
65
+ try {
66
+ await this.io.writeFile(file, data);
67
+ this.spillBytesWritten += data.byteLength;
68
+ return file;
69
+ }
70
+ catch (cause) {
71
+ try {
72
+ this.fail(operation, file, cause, retainedInMemoryBytes);
73
+ }
74
+ finally {
75
+ this.discard(file, data.byteLength);
76
+ }
77
+ return undefined;
78
+ }
79
+ finally {
80
+ this.pendingIoCopyBytes -= data.byteLength;
81
+ }
82
+ }
83
+ /**
84
+ * An unreferenced (or partially written) spool file: reclamation debt until its unlink completes or
85
+ * finds it gone. Files dropped after retirement started belong to the directory retirement removes.
86
+ */
87
+ discard(path, bytes) {
88
+ if (this.retired)
89
+ return;
90
+ this.reclaimDebtBytes += bytes;
91
+ this.enqueue("reclaim", () => this.reclaim(path, bytes, 0));
92
+ }
93
+ /** A failed unlink keeps its debt and is retried with bounded backoff outside the job chain. */
94
+ async reclaim(path, bytes, attempt) {
95
+ try {
96
+ await this.io.unlink(path);
97
+ }
98
+ catch (cause) {
99
+ if (cause.code !== "ENOENT") {
100
+ this.fail("reclaim", path, cause, 0);
101
+ const delay = RECLAIM_RETRY_DELAYS_MS[attempt];
102
+ if (delay !== undefined) {
103
+ setTimeout(() => {
104
+ if (this.retired)
105
+ this.reclaimDebtBytes -= bytes;
106
+ else
107
+ this.enqueue("reclaim", () => this.reclaim(path, bytes, attempt + 1));
108
+ }, delay).unref?.();
109
+ }
110
+ return;
111
+ }
112
+ }
113
+ this.reclaimDebtBytes -= bytes;
114
+ this.scheduleSpill();
115
+ }
116
+ get spillAllowed() {
117
+ return !this.halted && !this.retired;
118
+ }
119
+ /** True while a storage failure or unreclaimed debt above the work allowance holds spilling back. */
120
+ get spillStopped() {
121
+ return this.halted || this.reclaimDebtBytes > RECLAIM_WORK_ALLOWANCE_BYTES;
122
+ }
123
+ enqueue(operation, job) {
124
+ this.queued++;
125
+ this.chain = this.chain
126
+ .then(job)
127
+ .catch((cause) => {
128
+ if (operation === "spill_write")
129
+ this.spillQueued = false;
130
+ this.fail(operation, this.dir, cause, 0);
131
+ })
132
+ .finally(() => {
133
+ this.queued--;
134
+ });
135
+ }
136
+ /** Creates the private epoch directory once; a refusal or failure halts spilling and is reported. */
137
+ ensureDir(retained) {
138
+ this.dirReady ??= createSpoolDir(this.dir).then((token) => {
139
+ this.spoolToken = token;
140
+ return this.dir;
141
+ }, (cause) => {
142
+ this.fail("spool_create", this.dir, cause, retained);
143
+ return undefined;
144
+ });
145
+ return this.dirReady;
146
+ }
147
+ readFailure(operation, path, cause) {
148
+ this.fail(operation, path, cause, 0);
149
+ return new CompactStorageReadError(operation, path, cause);
150
+ }
151
+ fail(operation, path, cause, retained) {
152
+ if (operation === "spill_write" || operation === "spool_create")
153
+ this.halted = true;
154
+ this.record({ operation, path, cause: describeCause(cause), retainedInMemoryBytes: retained });
155
+ }
156
+ record(failure) {
157
+ this.failures.push(failure);
158
+ if (this.failures.length > FAILURE_HISTORY)
159
+ this.failures.shift();
160
+ }
161
+ }
162
+ //# sourceMappingURL=compact-spool-storage.js.map
@@ -2,7 +2,7 @@
2
2
  * The asynchronous file seam of compact current-state storage (senpi#4272) and the failures it
3
3
  * reports. Every spool access goes through `CompactStorageIo`; nothing here blocks the host loop.
4
4
  */
5
- export type CompactStorageOperation = "spool_create" | "spill_write" | "leaf_read" | "reclaim";
5
+ export type CompactStorageOperation = "spool_create" | "spill_write" | "page_write" | "leaf_read" | "page_read" | "reclaim";
6
6
  export interface CompactStorageFailure {
7
7
  readonly operation: CompactStorageOperation;
8
8
  readonly path: string;
@@ -17,6 +17,7 @@ export interface CompactStorageIo {
17
17
  unlink(path: string): Promise<void>;
18
18
  }
19
19
  export declare const nodeCompactStorageIo: CompactStorageIo;
20
+ /** Never throws: a host-supplied cause that cannot be described must not abort the failure report. */
20
21
  export declare function describeCause(cause: unknown): string;
21
22
  /** Committed storage that can no longer be read: a real error, never empty content. */
22
23
  export declare class CompactStorageReadError extends Error {
@@ -4,8 +4,14 @@ export const nodeCompactStorageIo = {
4
4
  readFile: (path) => readFile(path),
5
5
  unlink: (path) => unlink(path),
6
6
  };
7
+ /** Never throws: a host-supplied cause that cannot be described must not abort the failure report. */
7
8
  export function describeCause(cause) {
8
- return cause instanceof Error ? cause.message : String(cause);
9
+ try {
10
+ return cause instanceof Error ? cause.message : String(cause);
11
+ }
12
+ catch {
13
+ return "unprintable failure cause";
14
+ }
9
15
  }
10
16
  /** Committed storage that can no longer be read: a real error, never empty content. */
11
17
  export class CompactStorageReadError extends Error {
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The typed transitions compact current state accepts (senpi#4272, R3.C6.4) and the typed
3
+ * rejection `CompactSessionState.accept` throws when one fails validation.
4
+ */
5
+ export type CompactBlockKind = "text" | "thinking" | "tool_arguments";
6
+ export type CompactPersistence = "pending" | "appended" | "failed";
7
+ export interface BlockAddress {
8
+ readonly messageId: string;
9
+ readonly contentIndex: number;
10
+ }
11
+ export type CompactTransition = (BlockAddress & {
12
+ readonly type: "block_start";
13
+ readonly kind: CompactBlockKind;
14
+ readonly seed: string;
15
+ /**
16
+ * The whole seed when later deltas replay it from offset 0, or 0 when they continue after it. Partial
17
+ * coverage is refused: the uncovered seed tail would make every later delta overlap or leave a gap.
18
+ */
19
+ readonly coveredCodeUnits: number;
20
+ }) | (BlockAddress & {
21
+ readonly type: "block_delta";
22
+ readonly offset: number;
23
+ readonly text: string;
24
+ }) | (BlockAddress & {
25
+ readonly type: "block_replace";
26
+ readonly text: string;
27
+ }) | {
28
+ readonly type: "message_status";
29
+ readonly messageId: string;
30
+ readonly persistence: CompactPersistence;
31
+ } | {
32
+ readonly type: "message_remove";
33
+ readonly messageId: string;
34
+ };
35
+ type CompactRejection = "unknown_block" | "duplicate_block" | "invalid_coverage" | "non_contiguous" | "conflicting_overlap" | "unknown_message";
36
+ export declare class CompactTransitionError extends Error {
37
+ readonly reason: CompactRejection;
38
+ constructor(reason: CompactRejection, detail: string);
39
+ }
40
+ export {};
41
+ //# sourceMappingURL=compact-transition.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The typed transitions compact current state accepts (senpi#4272, R3.C6.4) and the typed
3
+ * rejection `CompactSessionState.accept` throws when one fails validation.
4
+ */
5
+ export class CompactTransitionError extends Error {
6
+ constructor(reason, detail) {
7
+ super(`compact transition rejected (${reason}): ${detail}`);
8
+ this.name = "CompactTransitionError";
9
+ this.reason = reason;
10
+ }
11
+ }
12
+ //# sourceMappingURL=compact-transition.js.map
@@ -50,6 +50,8 @@ export interface RpcConnectionSink {
50
50
  writeRaw(chunk: string): void;
51
51
  /** Shared bindings accept correlated records synchronously, before JSONL buffering. */
52
52
  writeRecord?(record: object): void;
53
+ /** The internal structured lane (senpi#4272): seeded records and block controls, never a legacy destination. */
54
+ writeStructured?(record: object): void;
53
55
  waitForBackpressure(): Promise<void>;
54
56
  /** Tears the transport down once its event queue can no longer deliver (overflow, write failure). */
55
57
  close?(): void;
@@ -47,7 +47,7 @@ import { runAbort, runGetEffectStatus, runQueuedInputChange, runSetModel, runSet
47
47
  import { EffectLedger } from "./effect-ledger.js";
48
48
  import { EFFECT_LEDGER_CAPABILITY } from "./effect-record.js";
49
49
  import { createRpcEventOutputBuffer } from "./event-output-buffer.js";
50
- import { settleExtensionUiResponse } from "./extension-ui-response.js";
50
+ import { answeredUiRequestId, settleExtensionUiResponse } from "./extension-ui-response.js";
51
51
  import { answerGetEntries } from "./get-entries-command.js";
52
52
  import { runHoldInput } from "./held-input-commands.js";
53
53
  import { HostSessionControl } from "./host-session-control.js";
@@ -64,6 +64,7 @@ import { runPauseForRun, runResumeForRun } from "./run-hold-commands.js";
64
64
  import { availableModelsData, interruptRunningTurn } from "./session-control-actions-data.js";
65
65
  import { SessionExtensionUiRequests, sessionDialogRequests } from "./session-extension-ui-requests.js";
66
66
  import { subscribeSessionRecords } from "./session-record-stream.js";
67
+ import { subscribeStructuredSessionRecords } from "./structured-session-records.js";
67
68
  import { RpcSessionRelocation } from "./session-relocation.js";
68
69
  import { answerHostResponse, isHostResponse, relocationChannel, } from "./session-relocation-channel.js";
69
70
  export { buildRpcSessionState } from "./rpc-session-state.js";
@@ -190,6 +191,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
190
191
  // Set by the first command that asks this session for a turn; `set_retry_fallback` is refused after it.
191
192
  let turnRequested = false;
192
193
  let unsubscribe;
194
+ let unsubscribeStructured;
193
195
  let unsubscribeBackpressure;
194
196
  let unsubscribeLoadedSurfaces;
195
197
  let unsubscribeExtensionEvents;
@@ -762,6 +764,10 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
762
764
  unsubscribe = subscribeSessionRecords(session, outputEvent, (abortSource) => {
763
765
  lastAbortSource = abortSource;
764
766
  });
767
+ unsubscribeStructured?.();
768
+ unsubscribeStructured = sink.writeStructured
769
+ ? subscribeStructuredSessionRecords(session, (record) => sink.writeStructured?.(tagSessionRecord(record)))
770
+ : undefined;
765
771
  unsubscribeBackpressure = session.agent.subscribe(async () => {
766
772
  await waitForRpcBackpressure();
767
773
  });
@@ -1704,15 +1710,13 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1704
1710
  parsed !== null &&
1705
1711
  "type" in parsed &&
1706
1712
  parsed.type === "extension_ui_response") {
1707
- if (!options.workerSecurityBridge &&
1708
- "id" in parsed &&
1709
- typeof parsed.id === "string" &&
1710
- grantRequests.has(parsed.id)) {
1713
+ const requestId = answeredUiRequestId(parsed);
1714
+ if (!options.workerSecurityBridge && requestId !== undefined && grantRequests.has(requestId)) {
1711
1715
  const sender = options.clientInfo?.connectionId();
1712
1716
  const capable = sender === undefined
1713
1717
  ? clientCapabilities?.includes("grant_select") === true
1714
1718
  : options.clientInfo?.hasCapability?.(sender, "grant_select") === true;
1715
- if (grantRequests.get(parsed.id) !== sender || !capable)
1719
+ if (grantRequests.get(requestId) !== sender || !capable)
1716
1720
  return;
1717
1721
  }
1718
1722
  const reply = settleExtensionUiResponse(parsed, {
@@ -1767,10 +1771,12 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
1767
1771
  pendingExtensionRequests.close();
1768
1772
  unsubscribeProviderAccountEvents();
1769
1773
  unsubscribe?.();
1774
+ unsubscribeStructured?.();
1770
1775
  unsubscribeBackpressure?.();
1771
1776
  unsubscribeLoadedSurfaces?.();
1772
1777
  unsubscribeExtensionEvents?.();
1773
1778
  unsubscribe = undefined;
1779
+ unsubscribeStructured = undefined;
1774
1780
  unsubscribeBackpressure = undefined;
1775
1781
  unsubscribeLoadedSurfaces = undefined;
1776
1782
  unsubscribeExtensionEvents = undefined;
@@ -16,7 +16,7 @@ import { keepOwnerPipe, OWNER_WATCH_FD } from "./host-lifecycle-launch.js";
16
16
  import { pollProtocolInfo } from "./host-readiness.js";
17
17
  import { initialHostEnvironment } from "./host-spawn-environment.js";
18
18
  import { hostLaunchProfile } from "./protocol-identity.js";
19
- import { createSocketSecret, socketSecretPath } from "./socket-transport.js";
19
+ import { ensureSocketSecret, socketSecretPath } from "./socket-transport.js";
20
20
  import { relayPresenterBootstrap } from "./mcp-presenter-bootstrap.js";
21
21
  export const DEFAULT_READINESS_TIMEOUT_MS = 10_000;
22
22
  export { DEFAULT_STOP_TIMEOUT_MS };
@@ -38,9 +38,11 @@ export async function startHost(paths, socket, options, generation = 0) {
38
38
  reason,
39
39
  });
40
40
  // The settings file must exist before the supervisor reads it at boot, so it
41
- // records the policy before the spawn instead of beside the pidfile.
41
+ // records the policy before the spawn instead of beside the pidfile. A valid public
42
+ // secret belongs to the endpoint, not to this start: clients that already read it
43
+ // derive the pipe name from it, so a start reuses it and never rotates it.
42
44
  if (process.platform === "win32")
43
- await createSocketSecret(socketSecretPath(socket));
45
+ await ensureSocketSecret(socketSecretPath(socket));
44
46
  await writeHostSettings(paths, {
45
47
  socket,
46
48
  capabilities: PINNED_HOST_CLIENT_CAPABILITIES,
@@ -109,7 +109,8 @@ export class RpcSecurityPresentations {
109
109
  if (pending.connection !== connection ||
110
110
  (connection !== undefined && !this.fanout.connectionHas(connection, "grant_select")))
111
111
  return false;
112
- state?.grants.delete(requestId);
112
+ // The entry stays until the worker reports the grant settled (settleGrant): a frame the worker
113
+ // refuses leaves the grant pending, and it must stay answerable only by its recipient.
113
114
  return true;
114
115
  }
115
116
  settleGrant(sessionId, requestId) {
@@ -1,6 +1,7 @@
1
1
  import { bindToProviderScope, runWithProviderScope } from "@earendil-works/pi-ai/node/provider-scope";
2
2
  import { createRpcConnectionHandler, } from "./connection-handler.js";
3
3
  import { createToolAttributionSpans } from "./session-attribution.js";
4
+ import { structuredLaneEnabled } from "./structured-session-records.js";
4
5
  /**
5
6
  * Creates the extension UI bridge and subscriptions within the entry's provider
6
7
  * scope. The classic handler remains the single source of command semantics.
@@ -54,8 +55,17 @@ export async function createRpcSessionBinding(sessionId, entry, writer, requestC
54
55
  return dropAfterClose();
55
56
  writeInScope(record);
56
57
  };
58
+ const writeStructuredInScope = bindToProviderScope((record) => writer.structuredLanes.accept(sessionId, record));
57
59
  const taggedSink = {
58
60
  writeRecord,
61
+ ...(structuredLaneEnabled()
62
+ ? {
63
+ writeStructured: (record) => {
64
+ if (entry.scope.state !== "closed")
65
+ writeStructuredInScope(record);
66
+ },
67
+ }
68
+ : {}),
59
69
  // A late raw reply after the scope closed is dropped like any record (senpi#2871); while the scope is
60
70
  // open, records arrive only through writeRecord, so a raw write is a wiring bug.
61
71
  writeRaw: () => {
@@ -2,6 +2,7 @@ import type { ExtensionUIContext, SessionKind } from "../../core/extensions/type
2
2
  import { type McpFormBinding } from "../../core/mcp-form-types.ts";
3
3
  import { McpPresenterAuthority, type McpPresenterConnection } from "./mcp-presenter-authority.ts";
4
4
  import type { SecurityNoticeSnapshot, SecurityNoticeState } from "../../core/security-notices.ts";
5
+ import { SessionStructuredLanes } from "./session-structured-lanes.ts";
5
6
  import { type MediaPersister } from "./media-placeholders.ts";
6
7
  import type { RpcHostLifecycleEvent, RpcOpenQueuedEvent, RpcSessionClosedEvent, RpcSessionClosedReason, RpcSessionParkedEvent } from "./rpc-types.ts";
7
8
  import { type SessionEventWriterConnection } from "./session-event-fanout.ts";
@@ -23,6 +24,7 @@ export declare const MAX_SHARED_STDIO_QUEUE_RECORDS = 4096;
23
24
  * scheduling boundary and violate D9.
24
25
  */
25
26
  export declare class SessionEventWriter {
27
+ readonly structuredLanes: SessionStructuredLanes;
26
28
  private presenterAuthority?;
27
29
  private readonly presenterConnections;
28
30
  private readonly presenterBindings;
@@ -3,6 +3,7 @@ import { bindPrivateMcpForms } from "../../core/mcp-form-types.js";
3
3
  import { McpPresenterAuthority } from "./mcp-presenter-authority.js";
4
4
  import { MEDIA_PLACEHOLDERS_CAPABILITY } from "./custom-capability.js";
5
5
  import { normalizeJsonValue } from "./json-normal-value.js";
6
+ import { SessionStructuredLanes } from "./session-structured-lanes.js";
6
7
  import { serializeJsonLine } from "./jsonl.js";
7
8
  import { omitNormalInlineMedia } from "./media-placeholders.js";
8
9
  import { RpcSecurityPresentations } from "./security-presentations.js";
@@ -141,6 +142,7 @@ export class SessionEventWriter {
141
142
  return this.reservedCloseBytes;
142
143
  }
143
144
  constructor(writeRaw, waitOrSchedule, scheduleFlush) {
145
+ this.structuredLanes = new SessionStructuredLanes();
144
146
  this.presenterConnections = new Map();
145
147
  this.presenterBindings = new Map();
146
148
  this.records = new SessionRecordAccounting();
@@ -7,7 +7,7 @@
7
7
  * reads the same records from either.
8
8
  */
9
9
  import type { AgentAbortSource } from "../../core/agent-abort-provenance.ts";
10
- import type { AgentSession } from "../../core/agent-session.ts";
11
- /** Subscribes `output` to the session's records; returns the unsubscribe. */
12
- export declare function subscribeSessionRecords(session: AgentSession, output: (record: object) => void, onAbortSource?: (source: AgentAbortSource) => void): () => void;
10
+ import type { AgentSession, AgentSessionEventListener } from "../../core/agent-session.ts";
11
+ /** Subscribes `output` to the session's records (from `source`, by default `session.subscribe`); returns the unsubscribe. */
12
+ export declare function subscribeSessionRecords(session: AgentSession, output: (record: object) => void, onAbortSource?: (source: AgentAbortSource) => void, source?: (listener: AgentSessionEventListener) => () => void): () => void;
13
13
  //# sourceMappingURL=session-record-stream.d.ts.map
@@ -1,10 +1,10 @@
1
1
  import { clientFacingIdentity } from "./client-admission-record.js";
2
2
  import { ClientAdmissions } from "./client-admissions.js";
3
3
  import { ClientMessageEvents } from "./client-message-events.js";
4
- /** Subscribes `output` to the session's records; returns the unsubscribe. */
5
- export function subscribeSessionRecords(session, output, onAbortSource = () => { }) {
4
+ /** Subscribes `output` to the session's records (from `source`, by default `session.subscribe`); returns the unsubscribe. */
5
+ export function subscribeSessionRecords(session, output, onAbortSource = () => { }, source = (listener) => session.subscribe(listener)) {
6
6
  const correlatedEvents = new ClientMessageEvents(output, () => ClientAdmissions.forSession(session).hasIdentities);
7
- return session.subscribe((event) => {
7
+ return source((event) => {
8
8
  if (event.type === "session_lifecycle") {
9
9
  // The connection adds the routing handle; classic stdio has no envelope.
10
10
  const { sessionId: _sessionId, ...record } = event;
@@ -0,0 +1,12 @@
1
+ type StructuredReceiver = (record: object) => void;
2
+ /**
3
+ * Per-session receivers of the internal structured lane (senpi#4272): the host-side owner of seeded records and
4
+ * block controls (S03/S04) registers here. Legacy sockets, stdout and the replay snapshot never read it.
5
+ */
6
+ export declare class SessionStructuredLanes {
7
+ private readonly receivers;
8
+ observe(sessionId: string, receiver: StructuredReceiver): () => void;
9
+ accept(sessionId: string, record: object): void;
10
+ }
11
+ export {};
12
+ //# sourceMappingURL=session-structured-lanes.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Per-session receivers of the internal structured lane (senpi#4272): the host-side owner of seeded records and
3
+ * block controls (S03/S04) registers here. Legacy sockets, stdout and the replay snapshot never read it.
4
+ */
5
+ export class SessionStructuredLanes {
6
+ constructor() {
7
+ this.receivers = new Map();
8
+ }
9
+ observe(sessionId, receiver) {
10
+ const receivers = this.receivers.get(sessionId) ?? new Set();
11
+ receivers.add(receiver);
12
+ this.receivers.set(sessionId, receivers);
13
+ return () => {
14
+ receivers.delete(receiver);
15
+ if (receivers.size === 0 && this.receivers.get(sessionId) === receivers)
16
+ this.receivers.delete(sessionId);
17
+ };
18
+ }
19
+ accept(sessionId, record) {
20
+ for (const receiver of [...(this.receivers.get(sessionId) ?? [])])
21
+ receiver(record);
22
+ }
23
+ }
24
+ //# sourceMappingURL=session-structured-lanes.js.map
@@ -5,6 +5,7 @@ import { ownProcessStartedAtMs } from "../../core/extensions/builtin/terminal/pr
5
5
  import { isSecurityNoticeState } from "../../core/security-notices.js";
6
6
  import { createWebViewBroker } from "../../core/webview/webview-broker.js";
7
7
  import { roleConnection } from "./connection-roles.js";
8
+ import { answeredUiRequestId } from "./extension-ui-response.js";
8
9
  import { subscriptionLimits } from "../../core/subscription-limits/limit-store.js";
9
10
  import { SessionWorkerRequests } from "./session-worker-requests.js";
10
11
  import { acknowledge, acknowledgeGrant } from "./session-worker-signals.js";
@@ -125,11 +126,11 @@ export class SessionWorkerClient {
125
126
  if (writer.handleMcpForm(command))
126
127
  return;
127
128
  const connection = writer.currentConnection();
128
- if ("type" in command &&
129
- command.type === "extension_ui_response" &&
130
- "id" in command &&
131
- typeof command.id === "string" &&
132
- writer.acceptWorkerGrantResponse(sessionId, command.id, connection) === false)
129
+ const grantAnswer = "type" in command && command.type === "extension_ui_response"
130
+ ? answeredUiRequestId(command)
131
+ : undefined;
132
+ if (grantAnswer !== undefined &&
133
+ writer.acceptWorkerGrantResponse(sessionId, grantAnswer, connection) === false)
133
134
  return;
134
135
  await this.request({
135
136
  type: "command",
@@ -0,0 +1,12 @@
1
+ import type { AgentSession } from "../../core/agent-session.ts";
2
+ /**
3
+ * The engine-socket compact-updates rollout flag (`<brand>_ENGINE_SOCKET_COMPACT_UPDATES`), explicit-true only.
4
+ * Off, no session holds a structured subscription and every destination keeps its existing bytes.
5
+ */
6
+ export declare function structuredLaneEnabled(): boolean;
7
+ /**
8
+ * The session's internal structured lane, correlated like its legacy records: seeded `toJsonEvent` projections of
9
+ * every session event plus the three block controls, in session order. Never a legacy destination.
10
+ */
11
+ export declare function subscribeStructuredSessionRecords(session: AgentSession, output: (record: object) => void): () => void;
12
+ //# sourceMappingURL=structured-session-records.d.ts.map
@@ -0,0 +1,88 @@
1
+ import { getAssistantCapture } from "@earendil-works/pi-ai";
2
+ import { envValue } from "../../core/brand.js";
3
+ import { toJsonEvent } from "../json-event.js";
4
+ import { subscribeSessionRecords } from "./session-record-stream.js";
5
+ /**
6
+ * The engine-socket compact-updates rollout flag (`<brand>_ENGINE_SOCKET_COMPACT_UPDATES`), explicit-true only.
7
+ * Off, no session holds a structured subscription and every destination keeps its existing bytes.
8
+ */
9
+ export function structuredLaneEnabled() {
10
+ const value = envValue("ENGINE_SOCKET_COMPACT_UPDATES");
11
+ return value === "1" || value === "true";
12
+ }
13
+ const SEEDS = { blockSeeds: 1 };
14
+ /**
15
+ * Admits a block's records only after its own seed or insert reached this subscriber, so a subscriber that attached
16
+ * mid-message gets no seed, delta or control for that message, never a wrong one. A started block whose next record
17
+ * has no capture, or that has no accepted end when the message ends, is invalid: its later records are dropped and
18
+ * the assistant `message_end` names it in `invalidatedBlocks`, so a reader rebuilds it from the terminal message.
19
+ * A terminal that rewrote blocks from `rewrittenFrom` on invalidates every block at or above that index, and a
20
+ * `message_end` with no interpretable terminal capture (cancel, idle timeout) invalidates every block of the message.
21
+ * Blocks are addressed by the capture's own index (`capture.contentIndex ?? event.contentIndex`), never a drifted public one.
22
+ */
23
+ class SeededBlockGate {
24
+ accept(record) {
25
+ if (record.type === "message_block_state" || record.type === "message_block_metadata")
26
+ return this.control(record, record.type === "message_block_state" ? record.phase : undefined);
27
+ if (record.type === "message_tool_arguments_reset")
28
+ return this.control(record);
29
+ if (record.type === "message_start" && record.message.role === "assistant") {
30
+ const seeded = toJsonEvent(record, SEEDS);
31
+ this.blocks = seeded && new Map();
32
+ return seeded;
33
+ }
34
+ if (record.type === "message_end" && record.message.role === "assistant")
35
+ return this.end(record);
36
+ if (record.type !== "message_update")
37
+ return toJsonEvent(record, SEEDS);
38
+ if (!this.blocks)
39
+ return undefined;
40
+ const seeded = toJsonEvent(record, SEEDS);
41
+ const update = record.assistantMessageEvent;
42
+ if (!("contentIndex" in update))
43
+ return seeded;
44
+ const index = getAssistantCapture(record)?.contentIndex ?? update.contentIndex;
45
+ const starts = update.type === "text_start" || update.type === "thinking_start" || update.type === "toolcall_start";
46
+ if (!starts && this.blocks.get(index) !== "open")
47
+ return undefined;
48
+ const ends = update.type === "text_end" || update.type === "thinking_end" || update.type === "toolcall_end";
49
+ this.blocks.set(index, seeded ? (ends ? "ended" : "open") : "invalid");
50
+ return seeded;
51
+ }
52
+ control(record, phase) {
53
+ const state = this.blocks?.get(record.contentIndex);
54
+ if (!this.blocks || state === "invalid")
55
+ return undefined;
56
+ if (phase === "insert")
57
+ this.blocks.set(record.contentIndex, "open");
58
+ else if (state === undefined || (phase === "end" && state !== "open"))
59
+ return undefined;
60
+ else if (phase === "end")
61
+ this.blocks.set(record.contentIndex, "ended");
62
+ return record;
63
+ }
64
+ end(record) {
65
+ const capture = getAssistantCapture(record);
66
+ const rewrittenFrom = capture?.message === undefined ? 0 : (capture.rewrittenFrom ?? Number.POSITIVE_INFINITY);
67
+ const invalidatedBlocks = [...(this.blocks ?? [])].flatMap(([index, state]) => state === "ended" && index < rewrittenFrom ? [] : [index]);
68
+ this.blocks = undefined;
69
+ const seeded = toJsonEvent(record, SEEDS) ?? record;
70
+ return invalidatedBlocks.length === 0
71
+ ? seeded
72
+ : { ...seeded, invalidatedBlocks: invalidatedBlocks.sort((a, b) => a - b) };
73
+ }
74
+ }
75
+ /**
76
+ * The session's internal structured lane, correlated like its legacy records: seeded `toJsonEvent` projections of
77
+ * every session event plus the three block controls, in session order. Never a legacy destination.
78
+ */
79
+ export function subscribeStructuredSessionRecords(session, output) {
80
+ const gate = new SeededBlockGate();
81
+ return subscribeSessionRecords(session, output, undefined, (listener) => session.subscribeStructured((record) => {
82
+ const accepted = gate.accept(record);
83
+ // Correlation reads only `type`, `role` and outer fields, which seeded records and controls keep.
84
+ if (accepted)
85
+ listener(accepted);
86
+ }));
87
+ }
88
+ //# sourceMappingURL=structured-session-records.js.map
@@ -2170,7 +2170,13 @@ session-notice identity. Restart redelivers unconsumed queued messages once, but
2170
2170
  never replays a consumed result or reruns an interrupted tool.
2171
2171
 
2172
2172
  `pi.lifecycle.reportChild({ taskId, childSessionId, runEpoch, generation, state,
2173
- waitingOn?, terminal? })` persists runner-owned child state. Stale epoch/generation
2173
+ label?, since?, waitingOn?, terminal? })` persists runner-owned child state. `label`
2174
+ is a producer-authored name and `since` is the child's own state-entry time (ISO);
2175
+ an unparseable `since` is dropped. `waitingOn` takes the `SessionLifecycle.waitingOn`
2176
+ shape described in [RPC session lifecycle](rpc.md#session-lifecycle), named lists
2177
+ included; its counts stay authoritative and the child's human waits are not kept.
2178
+ A nonterminal report appears in the parent's `waitingOn.childReports` as
2179
+ `{ taskId, childSessionId, runEpoch, state, label?, since? }`. Stale epoch/generation
2174
2180
  pairs are ignored; a higher `runEpoch` clears the old terminal status. Terminal
2175
2181
  values are `completed`, `error`, `lost`, `interrupted`, and `cancelled`. Child UI
2176
2182
  requests remain automatically answered by their runners, not routed to a person.