@code-yeongyu/senpi 2026.10.25-4 → 2026.10.26-1
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 +23 -0
- package/dist/bundle/chunks/{anthropic-messages-WBASBTIW.js → anthropic-messages-M235FJQV.js} +3 -3
- package/dist/bundle/chunks/{args-B444OQBK.js → args-FBNJOJSR.js} +1 -1
- package/dist/bundle/chunks/azure-openai-responses-YQRTQ7H6.js +2 -0
- package/dist/bundle/chunks/bedrock-converse-stream.js +3 -3
- package/dist/bundle/chunks/{chunk-4MOZXOJX.js → chunk-3A3XCLNH.js} +1 -1
- package/dist/bundle/chunks/{chunk-E6D4RIT7.js → chunk-3LPWILUZ.js} +2 -2
- package/dist/bundle/chunks/{chunk-3BTN337S.js → chunk-3WP5RFQR.js} +1 -1
- package/dist/bundle/chunks/{chunk-2RB5EL4X.js → chunk-5FQMNHXV.js} +1 -1
- package/dist/bundle/chunks/{chunk-STMXIBXF.js → chunk-5KZH6EYQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-4P7WARQZ.js → chunk-5LWUUXMH.js} +1 -1
- package/dist/bundle/chunks/{chunk-5JJDR6XI.js → chunk-6U3TBFBA.js} +1 -1
- package/dist/bundle/chunks/{chunk-BWA4FVFW.js → chunk-7KOUQSBX.js} +2 -2
- package/dist/bundle/chunks/{chunk-7CNOCJCN.js → chunk-ADQAPDPV.js} +1 -1
- package/dist/bundle/chunks/{chunk-ITGYN22E.js → chunk-CDQPA4FP.js} +1 -1
- package/dist/bundle/chunks/{chunk-WDPQ6CZJ.js → chunk-DMFAVE5N.js} +1 -1
- package/dist/bundle/chunks/{chunk-XF3LJC3O.js → chunk-DYWIFF2Q.js} +1 -1
- package/dist/bundle/chunks/{chunk-C2MQ4QXN.js → chunk-EZP6AYW2.js} +35 -29
- package/dist/bundle/chunks/chunk-GQJYNFMO.js +2 -0
- package/dist/bundle/chunks/{chunk-SBGIXMX3.js → chunk-GWGNVYPB.js} +1 -1
- package/dist/bundle/chunks/{chunk-KKD4CYCI.js → chunk-H4WJ4LY3.js} +1 -1
- package/dist/bundle/chunks/{chunk-V2UYXTRR.js → chunk-HWTD4QF3.js} +1 -1
- package/dist/bundle/chunks/{chunk-WD6WBYC5.js → chunk-I7QWTWVZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-BZ7726QO.js → chunk-J2YAYTB2.js} +1 -1
- package/dist/bundle/chunks/{chunk-TTU2RNVX.js → chunk-J5FCH7TI.js} +1 -1
- package/dist/bundle/chunks/chunk-JFQUWEN7.js +13 -0
- package/dist/bundle/chunks/{chunk-KNVNKDER.js → chunk-L2O6K3SJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-4AOMNOGY.js → chunk-LO5MYLGF.js} +1 -1
- package/dist/bundle/chunks/{chunk-N7IN46Q5.js → chunk-MSHRIZGT.js} +8 -6
- package/dist/bundle/chunks/{chunk-T36LJ6MW.js → chunk-MY4ZD3EW.js} +1 -1
- package/dist/bundle/chunks/{chunk-UFJ2K7Q7.js → chunk-OQ3WDMB3.js} +1 -1
- package/dist/bundle/chunks/{chunk-7GZUFOM3.js → chunk-PH3JDRB2.js} +1 -1
- package/dist/bundle/chunks/{chunk-E6WYH3ZL.js → chunk-QREKAXKX.js} +7 -5
- package/dist/bundle/chunks/{chunk-3QCYBOKN.js → chunk-QYFOHFPS.js} +1 -1
- package/dist/bundle/chunks/{chunk-JMOUU75H.js → chunk-SOXQ42VS.js} +1 -1
- package/dist/bundle/chunks/{chunk-FHXR4ZAC.js → chunk-UDM7VO6O.js} +1 -1
- package/dist/bundle/chunks/{chunk-OLCDIITS.js → chunk-UW6M3EXY.js} +1 -1
- package/dist/bundle/chunks/chunk-VICDRG3J.js +410 -0
- package/dist/bundle/chunks/chunk-VL7O5PO4.js +5 -0
- package/dist/bundle/chunks/{chunk-PT6JJUN2.js → chunk-XMNPK472.js} +1 -1
- package/dist/bundle/chunks/{chunk-GSSD3R5X.js → chunk-XQ45JMGN.js} +1 -1
- package/dist/bundle/chunks/{client-WTYEICLH.js → chunk-Y3TMJV3J.js} +3 -3
- package/dist/bundle/chunks/{chunk-KXB5WIS5.js → chunk-Z6SAQ5O3.js} +1 -1
- package/dist/bundle/chunks/{chunk-LOIF6PHI.js → chunk-ZAUVY6ZC.js} +3 -3
- package/dist/bundle/chunks/{chunk-E4LBKOZG.js → chunk-ZLIVZQDY.js} +4 -4
- package/dist/bundle/chunks/{chunk-6AOVUTUR.js → chunk-ZXHLH4MW.js} +2 -2
- package/dist/bundle/chunks/cli-main-55EQA5PS.js +3 -0
- package/dist/bundle/chunks/client-LOS6GSEW.js +2 -0
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-K5H6BJP3.js → cloudflare-workers-ai-system-one-ZJQGIIRC.js} +1 -1
- package/dist/bundle/chunks/cursor-agent.js +3 -3
- package/dist/bundle/chunks/devin-agent.js +3 -3
- package/dist/bundle/chunks/dispatch-C2IBP5VV.js +9 -0
- package/dist/bundle/chunks/google-generative-ai-5G4RIQA6.js +2 -0
- package/dist/bundle/chunks/google-vertex-ALD4P27O.js +2 -0
- package/dist/bundle/chunks/{help-fast-path-3Z26DQSV.js → help-fast-path-PEIKGCKZ.js} +1 -1
- package/dist/bundle/chunks/host-command-657VCQ34.js +2 -0
- package/dist/bundle/chunks/{host-lifecycle-3VHHIGDL.js → host-lifecycle-HX3EGRRW.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +2 -2
- package/dist/bundle/chunks/{host-session-command-56XF6ACS.js → host-session-command-OV5XDOJK.js} +1 -1
- package/dist/bundle/chunks/{host-socket-request-QL2WEZTN.js → host-socket-request-K4LCUOUD.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-MLXSYK2H.js +2 -0
- package/dist/bundle/chunks/{mcp-presenter-bootstrap-XUSCQVRC.js → mcp-presenter-bootstrap-FOMKF4TU.js} +1 -1
- package/dist/bundle/chunks/{mistral-conversations-DH44NOYV.js → mistral-conversations-RAE4K2XY.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-Z2CW4SDM.js → multi-session-host-774MACJU.js} +5 -6
- package/dist/bundle/chunks/{openai-codex-responses-SERAFKAS.js → openai-codex-responses-3CW4R7TG.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-BZB5O45A.js → openai-completions-5UIJXQUF.js} +1 -1
- package/dist/bundle/chunks/openai-responses-2BO44MRW.js +3 -0
- package/dist/bundle/chunks/{package-manager-cli-N55U6PEO.js → package-manager-cli-MZHVCX2S.js} +1 -1
- package/dist/bundle/chunks/{pi-messages-VXATX45B.js → pi-messages-RF6GER4R.js} +2 -2
- package/dist/bundle/chunks/{role-JVSBNKGE.js → role-ETSFSDTS.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-CBGCBYHV.js → rotation-stream-ZMQB4GPO.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-UJXOIZCW.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-AE3C6EQV.js → rpc-monitor-source-DRZXJHTL.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-VS7CZC2J.js → schedule-command-KA7LFFNK.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-MFWXUR7Z.js +3 -0
- package/dist/bundle/chunks/{session-picker-E4KUJDLO.js → session-picker-BP33NRSL.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +199 -190
- package/dist/bundle/chunks/{typesafe-system-one-P75WJ767.js → typesafe-system-one-GNNZ5O5U.js} +1 -1
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.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/cli/host-command-args.d.ts +1 -0
- package/dist/cli/host-command-args.js +5 -0
- package/dist/cli/host-command.js +7 -1
- package/dist/cli.js +21 -3
- package/dist/core/extensions/builtin/mcp/catalog-cache.d.ts +2 -1
- package/dist/core/extensions/builtin/mcp/catalog-cache.js +19 -6
- package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -2
- package/dist/core/extensions/builtin/mcp/catalog.js +20 -3
- package/dist/core/extensions/builtin/mcp/expose/call.d.ts +2 -1
- package/dist/core/extensions/builtin/mcp/expose/call.js +2 -1
- package/dist/core/extensions/builtin/mcp/expose/register.js +1 -1
- package/dist/core/extensions/builtin/mcp/expose/schema-compat.d.ts +2 -2
- package/dist/core/extensions/builtin/mcp/expose/schema-compat.js +11 -1
- package/dist/core/extensions/builtin/mcp/invocation.js +16 -2
- package/dist/core/extensions/builtin/mcp/modern/request.d.ts +1 -1
- package/dist/core/extensions/builtin/mcp/modern/request.js +1 -1
- package/dist/core/extensions/builtin/mcp/modern/sdk-bridge.js +18 -3
- package/dist/core/extensions/builtin/mcp/modern/stdio-probe.js +23 -0
- package/dist/core/extensions/builtin/mcp/modern/subscriptions.d.ts +14 -0
- package/dist/core/extensions/builtin/mcp/modern/subscriptions.js +226 -0
- package/dist/core/extensions/builtin/mcp/modern/tool-schema.d.ts +13 -0
- package/dist/core/extensions/builtin/mcp/modern/tool-schema.js +75 -0
- package/dist/core/extensions/builtin/mcp/modern/tools.d.ts +107 -0
- package/dist/core/extensions/builtin/mcp/modern/tools.js +53 -0
- package/dist/core/extensions/builtin/mcp/resources.js +4 -0
- package/dist/core/extensions/builtin/mcp/tool-client.d.ts +13 -0
- package/dist/core/extensions/builtin/mcp/tool-client.js +17 -0
- package/dist/core/extensions/builtin/mcp/tool-contracts.d.ts +19 -0
- package/dist/core/extensions/builtin/mcp/tool-contracts.js +2 -0
- package/dist/core/extensions/builtin/mcp/wrap.js +2 -2
- package/dist/core/extensions/builtin/permission-system/bash-command-segments.d.ts +37 -0
- package/dist/core/extensions/builtin/permission-system/bash-command-segments.js +899 -0
- package/dist/core/extensions/builtin/permission-system/parsers.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/parsers.js +44 -10
- package/dist/modes/interactive/session-control-commands.js +21 -4
- package/dist/modes/interactive/session-control-rpc-commands.d.ts +1 -1
- package/dist/modes/interactive/session-control-rpc-commands.js +19 -0
- package/dist/modes/rpc/compact-content-store.d.ts +96 -0
- package/dist/modes/rpc/compact-content-store.js +269 -0
- package/dist/modes/rpc/compact-session-state.d.ts +111 -0
- package/dist/modes/rpc/compact-session-state.js +206 -0
- package/dist/modes/rpc/compact-spool-dir.d.ts +5 -0
- package/dist/modes/rpc/compact-spool-dir.js +61 -0
- package/dist/modes/rpc/compact-storage-io.d.ts +27 -0
- package/dist/modes/rpc/compact-storage-io.js +19 -0
- package/dist/modes/rpc/connection-handler.d.ts +2 -0
- package/dist/modes/rpc/connection-handler.js +39 -7
- package/dist/modes/rpc/connection-question-bridge.d.ts +2 -1
- package/dist/modes/rpc/connection-question-bridge.js +6 -2
- package/dist/modes/rpc/extension-ui-response.d.ts +7 -2
- package/dist/modes/rpc/extension-ui-response.js +27 -7
- package/dist/modes/rpc/get-snapshot.d.ts +18 -0
- package/dist/modes/rpc/get-snapshot.js +43 -0
- package/dist/modes/rpc/host-capabilities.js +3 -0
- package/dist/modes/rpc/host-ensure-start.js +1 -0
- package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
- package/dist/modes/rpc/host-ensure.js +1 -0
- package/dist/modes/rpc/host-handoff.d.ts +2 -0
- package/dist/modes/rpc/host-handover-request.d.ts +1 -1
- package/dist/modes/rpc/host-handover-request.js +4 -3
- package/dist/modes/rpc/host-handover-wire.js +6 -0
- package/dist/modes/rpc/host-idle-handover.d.ts +2 -0
- package/dist/modes/rpc/host-idle-handover.js +3 -1
- package/dist/modes/rpc/host-observe-request.js +1 -0
- package/dist/modes/rpc/host-runner.d.ts +1 -0
- package/dist/modes/rpc/host-runner.js +9 -7
- package/dist/modes/rpc/host-successor.js +4 -0
- package/dist/modes/rpc/json-normal-value.d.ts +6 -0
- package/dist/modes/rpc/json-normal-value.js +100 -0
- package/dist/modes/rpc/mcp-presenter-bootstrap.js +5 -0
- package/dist/modes/rpc/media-placeholders.d.ts +9 -3
- package/dist/modes/rpc/media-placeholders.js +12 -4
- package/dist/modes/rpc/rpc-host-command-types.d.ts +2 -0
- package/dist/modes/rpc/rpc-question-types.d.ts +1 -0
- package/dist/modes/rpc/rpc-read-response-types.d.ts +7 -0
- package/dist/modes/rpc/rpc-session-command-types.d.ts +6 -0
- package/dist/modes/rpc/rpc-session-state-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-session-state.js +2 -0
- package/dist/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/modes/rpc/session-attachment-gate.js +1 -0
- package/dist/modes/rpc/session-binding.js +21 -18
- package/dist/modes/rpc/session-command-activity.js +1 -0
- package/dist/modes/rpc/session-event-fanout.d.ts +1 -1
- package/dist/modes/rpc/session-event-fanout.js +17 -9
- package/dist/modes/rpc/session-event-writer.js +22 -5
- package/dist/modes/rpc/session-extension-ui-requests.d.ts +2 -1
- package/dist/modes/rpc/session-extension-ui-requests.js +15 -10
- package/dist/modes/rpc/session-held.js +1 -0
- package/dist/modes/rpc/session-status-stream.d.ts +2 -1
- package/dist/modes/rpc/session-status-stream.js +8 -2
- package/dist/modes/rpc/session-status-subscriptions.d.ts +2 -1
- package/dist/modes/rpc/session-status-subscriptions.js +4 -0
- package/dist/modes/rpc/session-worker.js +5 -2
- package/dist/modes/rpc/snapshot-types.d.ts +29 -0
- package/dist/modes/rpc/snapshot-types.js +2 -0
- package/dist/modes/rpc/ui-answer-ledger.d.ts +13 -0
- package/dist/modes/rpc/ui-answer-ledger.js +34 -0
- package/dist/senpi +21 -3
- package/docs/mcp.md +20 -0
- package/docs/rpc.md +44 -0
- package/package.json +7 -6
- package/dist/bundle/chunks/azure-openai-responses-7I45DI72.js +0 -2
- package/dist/bundle/chunks/chunk-DMI55M2N.js +0 -2
- package/dist/bundle/chunks/chunk-DODH3N6U.js +0 -410
- package/dist/bundle/chunks/chunk-J37GRED2.js +0 -5
- package/dist/bundle/chunks/chunk-TBU6VYHN.js +0 -13
- package/dist/bundle/chunks/cli-main-YYH3IBC6.js +0 -3
- package/dist/bundle/chunks/dispatch-MXG5YWLV.js +0 -9
- package/dist/bundle/chunks/google-generative-ai-UM5Y7T6B.js +0 -2
- package/dist/bundle/chunks/google-vertex-OKU2VQHL.js +0 -2
- package/dist/bundle/chunks/host-command-TDLLHXVI.js +0 -2
- package/dist/bundle/chunks/interactive-mode-OEPULLZK.js +0 -2
- package/dist/bundle/chunks/openai-responses-TNHQ6SJS.js +0 -3
- package/dist/bundle/chunks/rpc-mode-ETXQWEB2.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-4LEIDIVI.js +0 -3
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { COMPACT_LEAF_CODE_UNITS, CompactContentStore, } from "./compact-content-store.js";
|
|
2
|
+
class CompactTransitionError extends Error {
|
|
3
|
+
constructor(reason, detail) {
|
|
4
|
+
super(`compact transition rejected (${reason}): ${detail}`);
|
|
5
|
+
this.name = "CompactTransitionError";
|
|
6
|
+
this.reason = reason;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const byContentIndex = ([a], [b]) => a - b;
|
|
10
|
+
export class CompactSessionState {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.settled = new Map();
|
|
13
|
+
this.active = new Map();
|
|
14
|
+
this.sequence = 0;
|
|
15
|
+
this.frozenTailBytes = 0;
|
|
16
|
+
this.store = new CompactContentStore(options);
|
|
17
|
+
}
|
|
18
|
+
get acceptedSequence() {
|
|
19
|
+
return this.sequence;
|
|
20
|
+
}
|
|
21
|
+
accept(transition) {
|
|
22
|
+
switch (transition.type) {
|
|
23
|
+
case "block_start":
|
|
24
|
+
this.start(transition);
|
|
25
|
+
break;
|
|
26
|
+
case "block_delta":
|
|
27
|
+
this.delta(this.block(transition), transition.offset, transition.text);
|
|
28
|
+
break;
|
|
29
|
+
case "block_replace":
|
|
30
|
+
this.replace(this.block(transition), transition.text);
|
|
31
|
+
break;
|
|
32
|
+
case "message_status":
|
|
33
|
+
this.setPersistence(transition.messageId, transition.persistence);
|
|
34
|
+
break;
|
|
35
|
+
case "message_remove":
|
|
36
|
+
this.remove(transition.messageId);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
return ++this.sequence;
|
|
40
|
+
}
|
|
41
|
+
freeze() {
|
|
42
|
+
const active = new Map();
|
|
43
|
+
for (const [messageId, blocks] of this.active)
|
|
44
|
+
active.set(messageId, this.seal(blocks, false));
|
|
45
|
+
const settled = new Map(this.settled);
|
|
46
|
+
for (const blocks of active.values())
|
|
47
|
+
this.pin(blocks, 1);
|
|
48
|
+
for (const row of settled.values())
|
|
49
|
+
this.pin(row.blocks, 1);
|
|
50
|
+
return { sequence: this.sequence, active, settled };
|
|
51
|
+
}
|
|
52
|
+
release(frozen) {
|
|
53
|
+
for (const blocks of frozen.active.values())
|
|
54
|
+
this.pin(blocks, -1);
|
|
55
|
+
for (const row of frozen.settled.values())
|
|
56
|
+
this.pin(row.blocks, -1);
|
|
57
|
+
}
|
|
58
|
+
/** Rejects with the storage error when committed content is gone; never returns empty content for it. */
|
|
59
|
+
async read(frozen) {
|
|
60
|
+
const messages = [];
|
|
61
|
+
for (const [messageId, row] of frozen.settled) {
|
|
62
|
+
messages.push({ messageId, persistence: row.persistence, blocks: await this.texts(row.blocks) });
|
|
63
|
+
}
|
|
64
|
+
for (const [messageId, blocks] of frozen.active) {
|
|
65
|
+
messages.push({ messageId, persistence: undefined, blocks: await this.texts(blocks) });
|
|
66
|
+
}
|
|
67
|
+
return { sequence: frozen.sequence, messages };
|
|
68
|
+
}
|
|
69
|
+
storageStatus() {
|
|
70
|
+
let residentTailBytes = 0;
|
|
71
|
+
let hotWitnessBytes = 0;
|
|
72
|
+
for (const blocks of this.active.values()) {
|
|
73
|
+
for (const block of blocks.values()) {
|
|
74
|
+
residentTailBytes += block.tail.length * 2;
|
|
75
|
+
hotWitnessBytes += block.witness.length * 2;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...this.store.status(),
|
|
80
|
+
residentTailBytes,
|
|
81
|
+
frozenTailBytes: this.frozenTailBytes,
|
|
82
|
+
hotWitnessBytes,
|
|
83
|
+
residentSettledRows: this.settled.size,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
idle() {
|
|
87
|
+
return this.store.idle();
|
|
88
|
+
}
|
|
89
|
+
retire() {
|
|
90
|
+
return this.store.retire();
|
|
91
|
+
}
|
|
92
|
+
start(t) {
|
|
93
|
+
if (this.active.get(t.messageId)?.has(t.contentIndex) || this.settled.has(t.messageId)) {
|
|
94
|
+
throw new CompactTransitionError("duplicate_block", `${t.messageId}#${t.contentIndex}`);
|
|
95
|
+
}
|
|
96
|
+
const covered = t.coveredCodeUnits;
|
|
97
|
+
if (!Number.isInteger(covered) || (covered !== 0 && covered !== t.seed.length)) {
|
|
98
|
+
throw new CompactTransitionError("invalid_coverage", `${covered} of ${t.seed.length} seed code units`);
|
|
99
|
+
}
|
|
100
|
+
const emitted = covered > 0 ? 0 : t.seed.length;
|
|
101
|
+
const block = { kind: t.kind, leaves: [], tail: "", units: 0, emitted, covered, witness: "" };
|
|
102
|
+
block.witness = t.seed.slice(0, covered);
|
|
103
|
+
this.append(block, t.seed);
|
|
104
|
+
const blocks = this.active.get(t.messageId) ?? new Map();
|
|
105
|
+
blocks.set(t.contentIndex, block);
|
|
106
|
+
this.active.set(t.messageId, blocks);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Appends only the new suffix. Overlap exists only while a fully covered seed is being replayed
|
|
110
|
+
* (`emitted < covered == units`), so it always lies inside the producer-declared coverage.
|
|
111
|
+
*/
|
|
112
|
+
delta(block, offset, text) {
|
|
113
|
+
if (offset !== block.emitted) {
|
|
114
|
+
throw new CompactTransitionError("non_contiguous", `offset ${offset}, expected ${block.emitted}`);
|
|
115
|
+
}
|
|
116
|
+
const overlapEnd = Math.min(offset + text.length, block.units);
|
|
117
|
+
const overlap = Math.max(0, overlapEnd - offset);
|
|
118
|
+
if (overlap > 0 && text.slice(0, overlap) !== block.witness.slice(offset, overlapEnd)) {
|
|
119
|
+
throw new CompactTransitionError("conflicting_overlap", `[${offset}, ${overlapEnd})`);
|
|
120
|
+
}
|
|
121
|
+
this.append(block, text.slice(overlap));
|
|
122
|
+
block.emitted = offset + text.length;
|
|
123
|
+
if (block.emitted >= block.covered)
|
|
124
|
+
block.witness = "";
|
|
125
|
+
}
|
|
126
|
+
replace(block, text) {
|
|
127
|
+
for (const id of block.leaves.splice(0))
|
|
128
|
+
this.store.adjust(id, -1, 0);
|
|
129
|
+
block.tail = "";
|
|
130
|
+
block.units = 0;
|
|
131
|
+
block.covered = 0;
|
|
132
|
+
block.witness = "";
|
|
133
|
+
this.append(block, text);
|
|
134
|
+
block.emitted = block.units;
|
|
135
|
+
}
|
|
136
|
+
setPersistence(messageId, persistence) {
|
|
137
|
+
const blocks = this.active.get(messageId);
|
|
138
|
+
if (blocks) {
|
|
139
|
+
this.settled.set(messageId, { persistence, blocks: this.seal(blocks, true) });
|
|
140
|
+
this.active.delete(messageId);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const row = this.settled.get(messageId);
|
|
144
|
+
if (row) {
|
|
145
|
+
this.settled.set(messageId, { ...row, persistence });
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
throw new CompactTransitionError("unknown_message", messageId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
remove(messageId) {
|
|
152
|
+
const blocks = this.active.get(messageId);
|
|
153
|
+
const row = this.settled.get(messageId);
|
|
154
|
+
if (!blocks && !row)
|
|
155
|
+
throw new CompactTransitionError("unknown_message", messageId);
|
|
156
|
+
for (const block of blocks?.values() ?? [])
|
|
157
|
+
for (const id of block.leaves)
|
|
158
|
+
this.store.adjust(id, -1, 0);
|
|
159
|
+
for (const block of row?.blocks ?? [])
|
|
160
|
+
for (const id of block.leaves)
|
|
161
|
+
this.store.adjust(id, -1, 0);
|
|
162
|
+
this.active.delete(messageId);
|
|
163
|
+
this.settled.delete(messageId);
|
|
164
|
+
}
|
|
165
|
+
block(address) {
|
|
166
|
+
const block = this.active.get(address.messageId)?.get(address.contentIndex);
|
|
167
|
+
if (!block)
|
|
168
|
+
throw new CompactTransitionError("unknown_block", `${address.messageId}#${address.contentIndex}`);
|
|
169
|
+
return block;
|
|
170
|
+
}
|
|
171
|
+
append(block, text) {
|
|
172
|
+
block.units += text.length;
|
|
173
|
+
let rest = text;
|
|
174
|
+
while (block.tail.length + rest.length >= COMPACT_LEAF_CODE_UNITS) {
|
|
175
|
+
const take = COMPACT_LEAF_CODE_UNITS - block.tail.length;
|
|
176
|
+
block.leaves.push(this.store.createLeaf(block.tail + rest.slice(0, take)));
|
|
177
|
+
block.tail = "";
|
|
178
|
+
rest = rest.slice(take);
|
|
179
|
+
}
|
|
180
|
+
block.tail += rest;
|
|
181
|
+
}
|
|
182
|
+
/** Settling mints the final tail into a spillable leaf; a freeze of an active block shares the tail. */
|
|
183
|
+
seal(blocks, settle) {
|
|
184
|
+
return [...blocks].sort(byContentIndex).map(([contentIndex, block]) => {
|
|
185
|
+
if (settle && block.tail.length > 0) {
|
|
186
|
+
block.leaves.push(this.store.createLeaf(block.tail));
|
|
187
|
+
block.tail = "";
|
|
188
|
+
}
|
|
189
|
+
return { contentIndex, kind: block.kind, leaves: [...block.leaves], tail: block.tail };
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
pin(blocks, delta) {
|
|
193
|
+
for (const block of blocks) {
|
|
194
|
+
this.frozenTailBytes += delta * block.tail.length * 2;
|
|
195
|
+
for (const id of block.leaves)
|
|
196
|
+
this.store.adjust(id, 0, delta);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async texts(blocks) {
|
|
200
|
+
return Promise.all(blocks.map(async ({ contentIndex, kind, leaves, tail }) => {
|
|
201
|
+
const parts = await Promise.all(leaves.map((id) => this.store.readLeaf(id)));
|
|
202
|
+
return { contentIndex, kind, text: parts.join("") + tail };
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=compact-session-state.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Creates a fresh private epoch directory and returns the token its ownership marker holds. */
|
|
2
|
+
export declare function createSpoolDir(dir: string): Promise<string>;
|
|
3
|
+
/** Removes `dir` only while it is still the private, marked directory `createSpoolDir` made. */
|
|
4
|
+
export declare function removeSpoolDir(dir: string, token: string): Promise<void>;
|
|
5
|
+
//# sourceMappingURL=compact-spool-dir.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Ownership of the compact spool's epoch directory (senpi#4272). The store creates the directory
|
|
6
|
+
* itself (0700, never adopting an existing one) and writes a private marker into it; removal
|
|
7
|
+
* re-checks the directory and that marker without following symlinks, so retirement never deletes
|
|
8
|
+
* a directory this store did not create and still own.
|
|
9
|
+
*/
|
|
10
|
+
const MARKER = ".compact-spool-owner";
|
|
11
|
+
class CompactSpoolRefusedError extends Error {
|
|
12
|
+
constructor(reason, path) {
|
|
13
|
+
super(`compact spool directory refused (${reason}): ${path}`);
|
|
14
|
+
this.name = "CompactSpoolRefusedError";
|
|
15
|
+
this.reason = reason;
|
|
16
|
+
this.path = path;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Creates a fresh private epoch directory and returns the token its ownership marker holds. */
|
|
20
|
+
export async function createSpoolDir(dir) {
|
|
21
|
+
await mkdir(dirname(dir), { recursive: true, mode: 0o700 });
|
|
22
|
+
try {
|
|
23
|
+
await mkdir(dir, { mode: 0o700 });
|
|
24
|
+
}
|
|
25
|
+
catch (cause) {
|
|
26
|
+
if (cause.code === "EEXIST")
|
|
27
|
+
throw new CompactSpoolRefusedError("pre_existing", dir);
|
|
28
|
+
throw cause;
|
|
29
|
+
}
|
|
30
|
+
const token = randomUUID();
|
|
31
|
+
await writeFile(join(dir, MARKER), token, { flag: "wx", mode: 0o600 });
|
|
32
|
+
return token;
|
|
33
|
+
}
|
|
34
|
+
/** Removes `dir` only while it is still the private, marked directory `createSpoolDir` made. */
|
|
35
|
+
export async function removeSpoolDir(dir, token) {
|
|
36
|
+
const refusal = await refusalFor(dir, token);
|
|
37
|
+
if (refusal)
|
|
38
|
+
throw new CompactSpoolRefusedError(refusal, dir);
|
|
39
|
+
await rm(dir, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
async function refusalFor(dir, token) {
|
|
42
|
+
const owned = await lstat(dir);
|
|
43
|
+
if (owned.isSymbolicLink())
|
|
44
|
+
return "symlink";
|
|
45
|
+
if (!owned.isDirectory())
|
|
46
|
+
return "not_directory";
|
|
47
|
+
const markerPath = join(dir, MARKER);
|
|
48
|
+
const marker = await lstat(markerPath).catch(() => undefined);
|
|
49
|
+
if (!marker?.isFile() || (await readFile(markerPath, "utf8")) !== token)
|
|
50
|
+
return "marker_missing";
|
|
51
|
+
// Windows has no POSIX owner or mode bits; the marker token and the no-follow checks still apply.
|
|
52
|
+
const uid = process.getuid?.();
|
|
53
|
+
if (uid === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (owned.uid !== uid || marker.uid !== uid)
|
|
56
|
+
return "foreign_owner";
|
|
57
|
+
if (((owned.mode | marker.mode) & 0o077) !== 0)
|
|
58
|
+
return "wide_mode";
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=compact-spool-dir.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The asynchronous file seam of compact current-state storage (senpi#4272) and the failures it
|
|
3
|
+
* reports. Every spool access goes through `CompactStorageIo`; nothing here blocks the host loop.
|
|
4
|
+
*/
|
|
5
|
+
export type CompactStorageOperation = "spool_create" | "spill_write" | "leaf_read" | "reclaim";
|
|
6
|
+
export interface CompactStorageFailure {
|
|
7
|
+
readonly operation: CompactStorageOperation;
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly cause: string;
|
|
10
|
+
/** Accepted bytes this failure leaves resident in memory instead of on disk. */
|
|
11
|
+
readonly retainedInMemoryBytes: number;
|
|
12
|
+
}
|
|
13
|
+
/** Asynchronous file access the host supplies; the default is node's promise filesystem. */
|
|
14
|
+
export interface CompactStorageIo {
|
|
15
|
+
writeFile(path: string, data: Buffer): Promise<void>;
|
|
16
|
+
readFile(path: string): Promise<Buffer>;
|
|
17
|
+
unlink(path: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare const nodeCompactStorageIo: CompactStorageIo;
|
|
20
|
+
export declare function describeCause(cause: unknown): string;
|
|
21
|
+
/** Committed storage that can no longer be read: a real error, never empty content. */
|
|
22
|
+
export declare class CompactStorageReadError extends Error {
|
|
23
|
+
readonly operation: CompactStorageOperation;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
constructor(operation: CompactStorageOperation, path: string, cause: unknown);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=compact-storage-io.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
export const nodeCompactStorageIo = {
|
|
3
|
+
writeFile: (path, data) => writeFile(path, data, { flag: "wx", mode: 0o600 }),
|
|
4
|
+
readFile: (path) => readFile(path),
|
|
5
|
+
unlink: (path) => unlink(path),
|
|
6
|
+
};
|
|
7
|
+
export function describeCause(cause) {
|
|
8
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
9
|
+
}
|
|
10
|
+
/** Committed storage that can no longer be read: a real error, never empty content. */
|
|
11
|
+
export class CompactStorageReadError extends Error {
|
|
12
|
+
constructor(operation, path, cause) {
|
|
13
|
+
super(`compact storage ${operation} failed for ${path}: ${describeCause(cause)}`, { cause });
|
|
14
|
+
this.name = "CompactStorageReadError";
|
|
15
|
+
this.operation = operation;
|
|
16
|
+
this.path = path;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=compact-storage-io.js.map
|
|
@@ -48,6 +48,8 @@ export interface RpcConnectionOptions {
|
|
|
48
48
|
*/
|
|
49
49
|
export interface RpcConnectionSink {
|
|
50
50
|
writeRaw(chunk: string): void;
|
|
51
|
+
/** Shared bindings accept correlated records synchronously, before JSONL buffering. */
|
|
52
|
+
writeRecord?(record: object): void;
|
|
51
53
|
waitForBackpressure(): Promise<void>;
|
|
52
54
|
/** Tears the transport down once its event queue can no longer deliver (overflow, write failure). */
|
|
53
55
|
close?(): void;
|
|
@@ -57,6 +57,8 @@ import { protocolIdentity } from "./protocol-identity.js";
|
|
|
57
57
|
import { buildRpcCommandsForSession, createCommandsChangedEvent, rpcCommandListDigest } from "./rpc-command-surface.js";
|
|
58
58
|
import { rpcCommandPayloadError, rpcCommandShapeError, rpcMessageLengthError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
|
|
59
59
|
import { buildRpcSessionState } from "./rpc-session-state.js";
|
|
60
|
+
import { getSnapshot } from "./get-snapshot.js";
|
|
61
|
+
import { sessionUiAnswerLedger } from "./ui-answer-ledger.js";
|
|
60
62
|
import { RPC_ERROR_EVAL_CELL_NOT_FOUND, RPC_ERROR_EVAL_CELLS_UNAVAILABLE, RPC_ERROR_INVALID_ARGUMENT, RPC_ERROR_INVALID_EVAL_CELL_REQUEST, RPC_ERROR_MEDIA_NOT_FOUND, RPC_ERROR_MODEL_NOT_FOUND, STEER_WHEN_IDLE_CAPABILITY, USER_ACTIVITY_CAPABILITY, } from "./rpc-types.js";
|
|
61
63
|
import { runPauseForRun, runResumeForRun } from "./run-hold-commands.js";
|
|
62
64
|
import { availableModelsData, interruptRunningTurn } from "./session-control-actions-data.js";
|
|
@@ -197,7 +199,9 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
197
199
|
let loadedSurfacesDigest;
|
|
198
200
|
let rpcCommandsDigest;
|
|
199
201
|
let suppressLoadedSurfaceEvents = false;
|
|
200
|
-
const eventOutput =
|
|
202
|
+
const eventOutput = sink.writeRecord
|
|
203
|
+
? undefined
|
|
204
|
+
: createRpcEventOutputBuffer(sink.writeRaw, options.eventFlushScheduler);
|
|
201
205
|
const effectContext = () => ({
|
|
202
206
|
session,
|
|
203
207
|
by: { connectionId: options.clientInfo?.connectionId() ?? "local" },
|
|
@@ -205,10 +209,18 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
205
209
|
});
|
|
206
210
|
const tagSessionRecord = (value) => routingSessionId === undefined ? value : { ...value, sessionId: routingSessionId };
|
|
207
211
|
const output = (obj) => {
|
|
208
|
-
|
|
212
|
+
const record = tagSessionRecord(obj);
|
|
213
|
+
if (sink.writeRecord)
|
|
214
|
+
sink.writeRecord(record);
|
|
215
|
+
else
|
|
216
|
+
eventOutput?.writeImmediate(record);
|
|
209
217
|
};
|
|
210
218
|
const outputEvent = (event) => {
|
|
211
|
-
|
|
219
|
+
const record = tagSessionRecord(event);
|
|
220
|
+
if (sink.writeRecord)
|
|
221
|
+
sink.writeRecord(record);
|
|
222
|
+
else
|
|
223
|
+
eventOutput?.enqueueEvent(record);
|
|
212
224
|
};
|
|
213
225
|
const currentLoadedSurfaces = () => {
|
|
214
226
|
const extensions = loadedExtensions(session);
|
|
@@ -268,7 +280,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
268
280
|
});
|
|
269
281
|
});
|
|
270
282
|
const waitForRpcBackpressure = async () => {
|
|
271
|
-
eventOutput
|
|
283
|
+
eventOutput?.flushEvents();
|
|
272
284
|
await sink.waitForBackpressure();
|
|
273
285
|
};
|
|
274
286
|
const success = (id, command, data) => {
|
|
@@ -292,10 +304,15 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
292
304
|
};
|
|
293
305
|
};
|
|
294
306
|
// Pending extension UI requests waiting for response
|
|
295
|
-
const
|
|
307
|
+
const uiRequestChanged = (change) => {
|
|
308
|
+
session.refreshLifecycle();
|
|
309
|
+
if (change && routingSessionId !== undefined)
|
|
310
|
+
output(change);
|
|
311
|
+
};
|
|
312
|
+
const pendingExtensionRequests = new SessionExtensionUiRequests(uiRequestChanged);
|
|
296
313
|
const grantRequests = new Map();
|
|
297
314
|
sessionDialogRequests.set(session, pendingExtensionRequests);
|
|
298
|
-
const questions = new ConnectionQuestionBridge(output,
|
|
315
|
+
const questions = new ConnectionQuestionBridge(output, uiRequestChanged);
|
|
299
316
|
let shutdownRequested = false;
|
|
300
317
|
// In-flight OAuth logins, keyed by provider. login_start registers one; the
|
|
301
318
|
// login promise clears it on completion; login_cancel aborts it. The flow is
|
|
@@ -858,6 +875,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
858
875
|
capabilities: [
|
|
859
876
|
...new Set([
|
|
860
877
|
"multi_session",
|
|
878
|
+
"snapshot_cursor",
|
|
879
|
+
"ui_answer_ledger",
|
|
861
880
|
"session_lifecycle",
|
|
862
881
|
AUTO_TITLE_SESSIONS_CAPABILITY,
|
|
863
882
|
MEDIA_PLACEHOLDERS_CAPABILITY,
|
|
@@ -1080,6 +1099,18 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1080
1099
|
refreshGrantPresentation();
|
|
1081
1100
|
return success(id, "set_client_info");
|
|
1082
1101
|
}
|
|
1102
|
+
case "get_snapshot": {
|
|
1103
|
+
const snapshot = getSnapshot(session, command, {
|
|
1104
|
+
lastAbortSource,
|
|
1105
|
+
enqueue: (markerId) => {
|
|
1106
|
+
if (routingSessionId !== undefined)
|
|
1107
|
+
output({ type: "snapshot_marker", markerId });
|
|
1108
|
+
},
|
|
1109
|
+
});
|
|
1110
|
+
return "error" in snapshot
|
|
1111
|
+
? error(id, "get_snapshot", snapshot.error, RPC_ERROR_INVALID_ARGUMENT)
|
|
1112
|
+
: success(id, "get_snapshot", snapshot.data);
|
|
1113
|
+
}
|
|
1083
1114
|
case "get_state":
|
|
1084
1115
|
return success(id, "get_state", buildRpcSessionState(session, lastAbortSource));
|
|
1085
1116
|
case "request_self_settle":
|
|
@@ -1688,6 +1719,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1688
1719
|
questions,
|
|
1689
1720
|
dialogs: pendingExtensionRequests,
|
|
1690
1721
|
routed: routingSessionId !== undefined,
|
|
1722
|
+
ledger: sessionUiAnswerLedger(session),
|
|
1691
1723
|
// An answer is a person's input by its entry point, whatever the connection role.
|
|
1692
1724
|
beforeAnswer: () => session.admitHumanAnswer(),
|
|
1693
1725
|
});
|
|
@@ -1719,7 +1751,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1719
1751
|
// A single-session connection hears its account's new limit windows as they are reported. A host
|
|
1720
1752
|
// session's handler (it has a routing `sessionId`) does not: the host broadcasts the record once.
|
|
1721
1753
|
const stopLimitEvents = options.sessionId === undefined
|
|
1722
|
-
? subscriptionLimits.subscribe((limits) =>
|
|
1754
|
+
? subscriptionLimits.subscribe((limits) => outputEvent({ type: "subscription_limits", ...limits }))
|
|
1723
1755
|
: () => { };
|
|
1724
1756
|
const dispose = async (disposeOptions = {}) => {
|
|
1725
1757
|
unbindMcpForms?.();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionUIContext, QuestionRequest, QuestionResponse } from "../../core/extensions/types.ts";
|
|
2
|
+
import type { UiRequestChange } from "./snapshot-types.ts";
|
|
2
3
|
import type { RpcExtensionUIProgress, RpcExtensionUIResponse, RpcQuestionUiRequest } from "./rpc-types.ts";
|
|
3
4
|
type Options = Parameters<NonNullable<ExtensionUIContext["question"]>>[1];
|
|
4
5
|
export declare const sessionQuestionBridges: WeakMap<object, ConnectionQuestionBridge>;
|
|
@@ -7,7 +8,7 @@ export declare class ConnectionQuestionBridge {
|
|
|
7
8
|
private readonly resolved;
|
|
8
9
|
private readonly output;
|
|
9
10
|
private readonly changed;
|
|
10
|
-
constructor(output: (record: object) => void, changed?: () => void);
|
|
11
|
+
constructor(output: (record: object) => void, changed?: (change?: UiRequestChange) => void);
|
|
11
12
|
pendingQuestions(): RpcQuestionUiRequest[];
|
|
12
13
|
ask(request: QuestionRequest, opts?: Options): Promise<QuestionResponse>;
|
|
13
14
|
respond(response: RpcExtensionUIResponse, beforeAnswer?: () => void): boolean | "question_incomplete" | "question_already_resolved";
|
|
@@ -43,7 +43,11 @@ export class ConnectionQuestionBridge {
|
|
|
43
43
|
clearTimeout(timer);
|
|
44
44
|
opts?.signal?.removeEventListener("abort", cancel);
|
|
45
45
|
this.pending.delete(id);
|
|
46
|
-
this.changed?.(
|
|
46
|
+
this.changed?.({
|
|
47
|
+
type: "ui_request_settled",
|
|
48
|
+
uiRequestId: id,
|
|
49
|
+
disposition: status === "answered" || status === "comment-submitted" ? "answered" : "cancelled",
|
|
50
|
+
});
|
|
47
51
|
this.resolved.add(id);
|
|
48
52
|
const result = {
|
|
49
53
|
status,
|
|
@@ -121,7 +125,7 @@ export class ConnectionQuestionBridge {
|
|
|
121
125
|
},
|
|
122
126
|
});
|
|
123
127
|
arm();
|
|
124
|
-
this.changed?.();
|
|
128
|
+
this.changed?.({ type: "ui_request_pending", uiRequestId: id, method: "question", request: frame });
|
|
125
129
|
this.output(frame);
|
|
126
130
|
opts?.signal?.addEventListener("abort", cancel, { once: true });
|
|
127
131
|
if (opts?.signal?.aborted)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { QuestionRequest, QuestionResponse } from "../../core/extensions/types.ts";
|
|
2
2
|
import type { RpcExtensionUIResponse } from "./rpc-types.ts";
|
|
3
|
+
import type { UiAnswerLedger } from "./ui-answer-ledger.ts";
|
|
3
4
|
/**
|
|
4
5
|
* An `extension_ui_response` carries two ids: `id`, the frame's own correlation id that its reply
|
|
5
6
|
* echoes, and `uiRequestId`, the extension UI request it answers. The short form every older client
|
|
@@ -23,6 +24,8 @@ export type ExtensionUiResponseReply = {
|
|
|
23
24
|
readonly id: string;
|
|
24
25
|
readonly type: "response";
|
|
25
26
|
readonly command: "extension_ui_response";
|
|
27
|
+
/** The answer's outcome for its request; absent when a malformed answer was refused before settling anything. */
|
|
28
|
+
readonly disposition?: "applied" | "replay" | "already_answered" | "unknown";
|
|
26
29
|
} & ({
|
|
27
30
|
readonly success: true;
|
|
28
31
|
} | {
|
|
@@ -37,13 +40,15 @@ type DialogAnswers = {
|
|
|
37
40
|
};
|
|
38
41
|
/**
|
|
39
42
|
* Settles one response on a host connection and returns its reply, keyed by the frame `id`.
|
|
40
|
-
* `undefined` only on an unrouted (single-session stdio) connection for
|
|
41
|
-
* matches: that connection has always ignored such a response, and still does.
|
|
43
|
+
* `undefined` only on an unrouted (single-session stdio) connection for an unnamed response no
|
|
44
|
+
* request here matches: that connection has always ignored such a response, and still does. A named
|
|
45
|
+
* response (`answerId`) asks for a receipt, so every connection answers it.
|
|
42
46
|
*/
|
|
43
47
|
export declare function settleExtensionUiResponse(response: RpcExtensionUIResponse, targets: {
|
|
44
48
|
readonly questions: QuestionAnswers;
|
|
45
49
|
readonly dialogs: DialogAnswers;
|
|
46
50
|
readonly routed: boolean;
|
|
51
|
+
readonly ledger?: UiAnswerLedger;
|
|
47
52
|
readonly beforeAnswer?: () => void;
|
|
48
53
|
}): ExtensionUiResponseReply | undefined;
|
|
49
54
|
export {};
|
|
@@ -28,25 +28,45 @@ export function unansweredQuestionIds(questions, answers) {
|
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Settles one response on a host connection and returns its reply, keyed by the frame `id`.
|
|
31
|
-
* `undefined` only on an unrouted (single-session stdio) connection for
|
|
32
|
-
* matches: that connection has always ignored such a response, and still does.
|
|
31
|
+
* `undefined` only on an unrouted (single-session stdio) connection for an unnamed response no
|
|
32
|
+
* request here matches: that connection has always ignored such a response, and still does. A named
|
|
33
|
+
* response (`answerId`) asks for a receipt, so every connection answers it.
|
|
33
34
|
*/
|
|
34
35
|
export function settleExtensionUiResponse(response, targets) {
|
|
36
|
+
if (response.answerId !== undefined &&
|
|
37
|
+
(typeof response.answerId !== "string" || response.answerId.length < 1 || response.answerId.length > 256))
|
|
38
|
+
return refused(response.id, "invalid_answer_id");
|
|
35
39
|
const requestId = answeredUiRequestId(response) ?? response.id;
|
|
40
|
+
const receipt = response.answerId === undefined ? undefined : targets.ledger?.lookup(requestId, response.answerId);
|
|
41
|
+
if (receipt === "replay")
|
|
42
|
+
return { ...reply(response.id), success: true, disposition: "replay" };
|
|
43
|
+
if (receipt === "already_answered")
|
|
44
|
+
return { ...reply(response.id), success: false, error: "already_answered", disposition: receipt };
|
|
36
45
|
const answer = requestId === response.id ? response : { ...response, id: requestId };
|
|
37
46
|
const question = targets.questions.respond(answer, targets.beforeAnswer);
|
|
38
47
|
if (typeof question === "string")
|
|
39
|
-
return refused(response.id, question
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
return refused(response.id, response.answerId !== undefined && question === "question_already_resolved"
|
|
49
|
+
? "unknown_extension_ui_request"
|
|
50
|
+
: question);
|
|
51
|
+
if (question || targets.dialogs.resolve(answer, targets.beforeAnswer)) {
|
|
52
|
+
targets.ledger?.record(requestId, response.answerId);
|
|
53
|
+
return { ...reply(response.id), success: true, disposition: "applied" };
|
|
54
|
+
}
|
|
42
55
|
// A routed binding owns exactly one session's request map: an unmatched response is a protocol
|
|
43
56
|
// error for its sender, never a cross-session match.
|
|
44
|
-
return targets.routed
|
|
57
|
+
return targets.routed || response.answerId !== undefined
|
|
58
|
+
? refused(response.id, "unknown_extension_ui_request")
|
|
59
|
+
: undefined;
|
|
45
60
|
}
|
|
46
61
|
function reply(id) {
|
|
47
62
|
return { id, type: "response", command: "extension_ui_response" };
|
|
48
63
|
}
|
|
49
64
|
function refused(id, error) {
|
|
50
|
-
|
|
65
|
+
const disposition = error === "question_already_resolved"
|
|
66
|
+
? "already_answered"
|
|
67
|
+
: error === "unknown_extension_ui_request"
|
|
68
|
+
? "unknown"
|
|
69
|
+
: undefined;
|
|
70
|
+
return { ...reply(id), success: false, error, ...(disposition === undefined ? {} : { disposition }) };
|
|
51
71
|
}
|
|
52
72
|
//# sourceMappingURL=extension-ui-response.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AgentAbortSource } from "../../core/agent-abort-provenance.ts";
|
|
2
|
+
import type { AgentSession } from "../../core/agent-session.ts";
|
|
3
|
+
import type { RpcSnapshot } from "./snapshot-types.ts";
|
|
4
|
+
type SnapshotRequest = {
|
|
5
|
+
readonly tail?: unknown;
|
|
6
|
+
readonly maxBytes?: unknown;
|
|
7
|
+
};
|
|
8
|
+
/** Capture and mark on the source loop, before any response-writing await. */
|
|
9
|
+
export declare function getSnapshot(session: AgentSession, request: SnapshotRequest, cursor: {
|
|
10
|
+
readonly enqueue: (markerId: string) => void;
|
|
11
|
+
readonly lastAbortSource?: AgentAbortSource;
|
|
12
|
+
}): {
|
|
13
|
+
readonly data: RpcSnapshot;
|
|
14
|
+
} | {
|
|
15
|
+
readonly error: string;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=get-snapshot.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { describeUsageLimitEntry } from "../../core/retry-fallback/usage-limit.js";
|
|
3
|
+
import { currentResponseByteBudget, DEFAULT_ENTRY_PAGE_BYTES } from "./get-entries-page.js";
|
|
4
|
+
import { buildRpcSessionState } from "./rpc-session-state.js";
|
|
5
|
+
/** Capture and mark on the source loop, before any response-writing await. */
|
|
6
|
+
export function getSnapshot(session, request, cursor) {
|
|
7
|
+
const tail = request.tail === undefined ? 100 : request.tail;
|
|
8
|
+
const maxBytes = request.maxBytes === undefined ? DEFAULT_ENTRY_PAGE_BYTES : request.maxBytes;
|
|
9
|
+
if (typeof tail !== "number" || !Number.isSafeInteger(tail) || tail < 0)
|
|
10
|
+
return { error: "tail must be a non-negative integer" };
|
|
11
|
+
if (typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
12
|
+
return { error: "maxBytes must be a positive integer" };
|
|
13
|
+
const state = buildRpcSessionState(session, cursor.lastAbortSource);
|
|
14
|
+
const history = session.sessionManager.getEntries();
|
|
15
|
+
const entries = [];
|
|
16
|
+
let bytes = 2;
|
|
17
|
+
for (let i = history.length - 1; i >= 0 && entries.length < tail; i--) {
|
|
18
|
+
const entry = history[i];
|
|
19
|
+
if (entry === undefined)
|
|
20
|
+
break;
|
|
21
|
+
const projected = describeUsageLimitEntry(entry);
|
|
22
|
+
const size = Buffer.byteLength(JSON.stringify(projected)) + (entries.length > 0 ? 1 : 0);
|
|
23
|
+
if (bytes + size > Math.min(maxBytes, currentResponseByteBudget()))
|
|
24
|
+
break;
|
|
25
|
+
entries.push(projected);
|
|
26
|
+
bytes += size;
|
|
27
|
+
}
|
|
28
|
+
entries.reverse();
|
|
29
|
+
const history_truncated = entries.length < history.length;
|
|
30
|
+
const markerId = randomUUID();
|
|
31
|
+
// Some state fields and entry payloads are live references: detach them before yielding, too.
|
|
32
|
+
const data = structuredClone({
|
|
33
|
+
state,
|
|
34
|
+
entries,
|
|
35
|
+
history_truncated,
|
|
36
|
+
next_before: history_truncated ? (entries[0]?.id ?? null) : null,
|
|
37
|
+
markerId,
|
|
38
|
+
});
|
|
39
|
+
session.releaseFullHistoryView();
|
|
40
|
+
cursor.enqueue(markerId);
|
|
41
|
+
return { data };
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=get-snapshot.js.map
|
|
@@ -20,6 +20,9 @@ export function multiSessionHostCapabilities(options) {
|
|
|
20
20
|
return [
|
|
21
21
|
...new Set([
|
|
22
22
|
"multi_session",
|
|
23
|
+
"snapshot_cursor",
|
|
24
|
+
"ui_answer_ledger",
|
|
25
|
+
"dialog_observer",
|
|
23
26
|
...(options.mcpPresenters ? ["mcp_form_presenter_v1"] : []),
|
|
24
27
|
"session_lifecycle",
|
|
25
28
|
"expected_host_instance",
|
|
@@ -63,6 +63,7 @@ export async function startHost(paths, socket, options, generation = 0) {
|
|
|
63
63
|
socket,
|
|
64
64
|
...(options.owner ? ["--owner-fd", String(OWNER_WATCH_FD)] : []),
|
|
65
65
|
...(options.presenterBootstrap ? ["--mcp-presenter-fd", "4"] : []),
|
|
66
|
+
...(options.mcpPresenterEnforcement ? ["--mcp-presenter-enforcement"] : []),
|
|
66
67
|
...(options.hostArgs ?? []),
|
|
67
68
|
];
|
|
68
69
|
const launch = testOptions?.spawn ?? testOptions?.launch?.(supervisorArgs) ?? defaultHostLaunch(supervisorArgs);
|