@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.
- package/CHANGELOG.md +24 -0
- package/dist/bundle/chunks/{anthropic-messages-M235FJQV.js → anthropic-messages-ODSFJCXR.js} +1 -1
- package/dist/bundle/chunks/{args-FBNJOJSR.js → args-SWKJAYQD.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-YQRTQ7H6.js → azure-openai-responses-REGXS4XO.js} +1 -1
- package/dist/bundle/chunks/bedrock-converse-stream.js +1 -1
- package/dist/bundle/chunks/chunk-2XF6W6F6.js +410 -0
- package/dist/bundle/chunks/{chunk-DMFAVE5N.js → chunk-4QVORFQ2.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZXHLH4MW.js → chunk-5W3DTM2J.js} +1 -1
- package/dist/bundle/chunks/{chunk-QREKAXKX.js → chunk-6GAMRAVM.js} +5 -5
- package/dist/bundle/chunks/{chunk-MSHRIZGT.js → chunk-7IZH2KQX.js} +3 -3
- package/dist/bundle/chunks/{chunk-PH3JDRB2.js → chunk-ARSENVDY.js} +1 -1
- package/dist/bundle/chunks/{chunk-5KZH6EYQ.js → chunk-CNDPCYNT.js} +1 -1
- package/dist/bundle/chunks/{chunk-DYWIFF2Q.js → chunk-CYOHFREV.js} +1 -1
- package/dist/bundle/chunks/{chunk-3LPWILUZ.js → chunk-DNDZPKIQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-ZLIVZQDY.js → chunk-EQNUL2WB.js} +1 -1
- package/dist/bundle/chunks/{chunk-5FQMNHXV.js → chunk-ESNSRUW4.js} +1 -1
- package/dist/bundle/chunks/{chunk-LO5MYLGF.js → chunk-FQDARFHT.js} +1 -1
- package/dist/bundle/chunks/{chunk-7KOUQSBX.js → chunk-GPPOPK76.js} +3 -3
- package/dist/bundle/chunks/chunk-I2WFLKZ2.js +5 -0
- package/dist/bundle/chunks/{chunk-I7QWTWVZ.js → chunk-IHKHCWWG.js} +1 -1
- package/dist/bundle/chunks/{chunk-MY4ZD3EW.js → chunk-IY4G44MN.js} +1 -1
- package/dist/bundle/chunks/{chunk-OQ3WDMB3.js → chunk-JH7MVRNJ.js} +1 -1
- package/dist/bundle/chunks/chunk-KBSFTXAT.js +2 -0
- package/dist/bundle/chunks/{chunk-UW6M3EXY.js → chunk-KRO63T4Q.js} +1 -1
- package/dist/bundle/chunks/{chunk-J5FCH7TI.js → chunk-KTDIIF2F.js} +1 -1
- package/dist/bundle/chunks/{chunk-3WP5RFQR.js → chunk-NDNIMC5W.js} +1 -1
- package/dist/bundle/chunks/{chunk-CDQPA4FP.js → chunk-ODRGPC5S.js} +1 -1
- package/dist/bundle/chunks/{chunk-3A3XCLNH.js → chunk-PDG4XWO2.js} +1 -1
- package/dist/bundle/chunks/{chunk-QYFOHFPS.js → chunk-PPECSSWX.js} +1 -1
- package/dist/bundle/chunks/{chunk-5LWUUXMH.js → chunk-PY4ZQHTN.js} +1 -1
- package/dist/bundle/chunks/{chunk-EZP6AYW2.js → chunk-RPW3XQW3.js} +22 -22
- package/dist/bundle/chunks/{chunk-SOXQ42VS.js → chunk-RQ5JTXUC.js} +1 -1
- package/dist/bundle/chunks/{chunk-JFQUWEN7.js → chunk-TQ67PB7U.js} +2 -2
- package/dist/bundle/chunks/{chunk-6U3TBFBA.js → chunk-UJPG4ROT.js} +1 -1
- package/dist/bundle/chunks/{chunk-UDM7VO6O.js → chunk-XM36IFP3.js} +1 -1
- package/dist/bundle/chunks/{chunk-XQ45JMGN.js → chunk-Z6BREXV3.js} +1 -1
- package/dist/bundle/chunks/{chunk-ADQAPDPV.js → chunk-ZP6JJDYV.js} +1 -1
- package/dist/bundle/chunks/{cli-main-55EQA5PS.js → cli-main-Q2GA53XM.js} +1 -1
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-ZJQGIIRC.js → cloudflare-workers-ai-system-one-WDHT7BBV.js} +1 -1
- package/dist/bundle/chunks/cursor-agent.js +1 -1
- package/dist/bundle/chunks/devin-agent.js +1 -1
- package/dist/bundle/chunks/{dispatch-C2IBP5VV.js → dispatch-5R3TYAYB.js} +3 -3
- package/dist/bundle/chunks/{google-generative-ai-5G4RIQA6.js → google-generative-ai-FHYFXKLA.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-ALD4P27O.js → google-vertex-5DWE5U5K.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-PEIKGCKZ.js → help-fast-path-FETLIF3W.js} +1 -1
- package/dist/bundle/chunks/{host-command-657VCQ34.js → host-command-XEB7SHT3.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-OV5XDOJK.js → host-session-command-FFN52LXJ.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-MLXSYK2H.js → interactive-mode-5DEKY3YW.js} +1 -1
- package/dist/bundle/chunks/{mistral-conversations-RAE4K2XY.js → mistral-conversations-I2PI7DDF.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-774MACJU.js → multi-session-host-OPR5VBFO.js} +3 -3
- package/dist/bundle/chunks/{openai-codex-responses-3CW4R7TG.js → openai-codex-responses-WSEX6SH7.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-5UIJXQUF.js → openai-completions-YCBHTBSS.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-2BO44MRW.js → openai-responses-GXNSQF4A.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-MZHVCX2S.js → package-manager-cli-W55E3LHP.js} +1 -1
- package/dist/bundle/chunks/{pi-messages-RF6GER4R.js → pi-messages-O6GISIRW.js} +1 -1
- package/dist/bundle/chunks/{role-ETSFSDTS.js → role-SHIZQKK4.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-ZMQB4GPO.js → rotation-stream-2PCZWNKF.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-UJXOIZCW.js → rpc-mode-IDUJTSYS.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-KA7LFFNK.js → schedule-command-B7RRSANU.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-MFWXUR7Z.js → session-control-endpoint-LMZH5JLH.js} +1 -1
- package/dist/bundle/chunks/{session-picker-BP33NRSL.js → session-picker-DTGBOXDR.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +41 -41
- package/dist/bundle/chunks/{typesafe-system-one-GNNZ5O5U.js → typesafe-system-one-XGYJJVSE.js} +1 -1
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session.d.ts +11 -1
- package/dist/core/agent-session.js +19 -2
- package/dist/core/extensions/builtin/compaction/index.js +7 -1
- package/dist/core/extensions/builtin/herdr/index.js +9 -5
- package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.d.ts +6 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-issuer.js +25 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.d.ts +3 -3
- package/dist/core/extensions/builtin/mcp/auth/oauth-provider.js +50 -19
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +4 -3
- package/dist/core/extensions/builtin/mcp/auth/oauth.js +19 -12
- package/dist/core/extensions/builtin/mcp/modern/dispatch.js +1 -0
- package/dist/core/extensions/builtin/mcp/modern/parameter-headers.d.ts +4 -1
- package/dist/core/extensions/builtin/mcp/modern/parameter-headers.js +33 -35
- package/dist/core/extensions/builtin/mcp/modern/request.js +3 -0
- package/dist/core/extensions/builtin/mcp/modern/transport.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/modern/transport.js +4 -2
- package/dist/core/extensions/builtin/terminal/extension-state.js +2 -1
- package/dist/core/extensions/builtin/vertex-model-garden/auth.d.ts +9 -0
- package/dist/core/extensions/builtin/vertex-model-garden/auth.js +17 -1
- package/dist/core/extensions/builtin/websearch/websearch/providers/google.d.ts +6 -0
- package/dist/core/extensions/builtin/websearch/websearch/providers/google.js +6 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/lifecycle-children.d.ts +3 -1
- package/dist/core/lifecycle-children.js +79 -15
- package/dist/core/lifecycle-record.js +1 -24
- package/dist/core/lifecycle-session-binding.js +2 -1
- package/dist/core/lifecycle-wait-items.d.ts +17 -0
- package/dist/core/lifecycle-wait-items.js +72 -0
- package/dist/core/model-runtime.js +6 -1
- package/dist/core/provider-api-key-auth.d.ts +6 -1
- package/dist/core/provider-api-key-auth.js +11 -4
- package/dist/core/provider-composer.d.ts +8 -1
- package/dist/core/provider-composer.js +7 -3
- package/dist/core/sdk.js +4 -1
- package/dist/core/session-activity.js +2 -0
- package/dist/core/session-lifecycle-types.d.ts +20 -4
- package/dist/core/session-lifecycle.js +35 -4
- package/dist/core/structured-session-lane.d.ts +33 -0
- package/dist/core/structured-session-lane.js +62 -0
- package/dist/core/work-custody-record.d.ts +2 -1
- package/dist/core/work-custody-record.js +2 -0
- package/dist/core/work-obligations.d.ts +1 -1
- package/dist/core/work-obligations.js +8 -2
- package/dist/modes/json-event.d.ts +79 -1
- package/dist/modes/json-event.js +79 -1
- package/dist/modes/rpc/compact-content-index.d.ts +85 -0
- package/dist/modes/rpc/compact-content-index.js +254 -0
- package/dist/modes/rpc/compact-content-store.d.ts +6 -43
- package/dist/modes/rpc/compact-content-store.js +22 -157
- package/dist/modes/rpc/compact-index-pages.d.ts +12 -0
- package/dist/modes/rpc/compact-index-pages.js +31 -0
- package/dist/modes/rpc/compact-session-state.d.ts +11 -39
- package/dist/modes/rpc/compact-session-state.js +17 -26
- package/dist/modes/rpc/compact-spool-dir.d.ts +4 -1
- package/dist/modes/rpc/compact-spool-dir.js +41 -8
- package/dist/modes/rpc/compact-spool-storage.d.ts +53 -0
- package/dist/modes/rpc/compact-spool-storage.js +162 -0
- package/dist/modes/rpc/compact-storage-io.d.ts +2 -1
- package/dist/modes/rpc/compact-storage-io.js +7 -1
- package/dist/modes/rpc/compact-transition.d.ts +41 -0
- package/dist/modes/rpc/compact-transition.js +12 -0
- package/dist/modes/rpc/connection-handler.d.ts +2 -0
- package/dist/modes/rpc/connection-handler.js +12 -6
- package/dist/modes/rpc/host-ensure-start.js +5 -3
- package/dist/modes/rpc/security-presentations.js +2 -1
- package/dist/modes/rpc/session-binding.js +10 -0
- package/dist/modes/rpc/session-event-writer.d.ts +2 -0
- package/dist/modes/rpc/session-event-writer.js +2 -0
- package/dist/modes/rpc/session-record-stream.d.ts +3 -3
- package/dist/modes/rpc/session-record-stream.js +3 -3
- package/dist/modes/rpc/session-structured-lanes.d.ts +12 -0
- package/dist/modes/rpc/session-structured-lanes.js +24 -0
- package/dist/modes/rpc/session-worker-client.js +6 -5
- package/dist/modes/rpc/structured-session-records.d.ts +12 -0
- package/dist/modes/rpc/structured-session-records.js +88 -0
- package/docs/extensions.md +7 -1
- package/docs/rpc.md +17 -2
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-GQJYNFMO.js +0 -2
- package/dist/bundle/chunks/chunk-VICDRG3J.js +0 -410
- 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
|
-
|
|
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
|
-
|
|
1708
|
-
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
command
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
writer.acceptWorkerGrantResponse(sessionId,
|
|
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
|
package/docs/extensions.md
CHANGED
|
@@ -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.
|
|
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.
|