@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
|
@@ -7,6 +7,8 @@ export type PermissionRequest = Pick<Request, "permission" | "patterns" | "alway
|
|
|
7
7
|
};
|
|
8
8
|
/** Parser function that extracts permission requests from tool input */
|
|
9
9
|
export type ToolPermissionParser = (toolName: string, input: Record<string, unknown>, cwd: string) => PermissionRequest[];
|
|
10
|
+
/** Marks a bash request target that no command-specific rule can match: only a `*` rule decides it. */
|
|
11
|
+
export declare const UNPARSED_BASH_PREFIX = "(unparsed) ";
|
|
10
12
|
/** The `eval` class a cell carries on its own, used when its tool-owned parser names anything narrower. */
|
|
11
13
|
export declare function fallbackEvalPermissionRequest(): PermissionRequest;
|
|
12
14
|
/**
|
|
@@ -2,6 +2,7 @@ import { dirname, resolve } from "node:path";
|
|
|
2
2
|
import { realpathWithoutOpen } from "../../../../utils/paths.js";
|
|
3
3
|
import { extractPatchedPaths } from "../gpt-apply-patch/index.js";
|
|
4
4
|
import { BashArity } from "../permission-system/arity.js";
|
|
5
|
+
import { analyzeBashCommand, bestEffortCommandLines } from "../permission-system/bash-command-segments.js";
|
|
5
6
|
import { extractExternalPaths, isExternalPath, toParentDirectoryPattern, } from "../permission-system/external-dir.js";
|
|
6
7
|
import { setApprovedMonitorParent } from "../terminal/monitor-permission.js";
|
|
7
8
|
import { urlOriginAlias, widerAlwaysScope } from "./always-scope.js";
|
|
@@ -73,6 +74,48 @@ function withExternalDirectoryRequests(requests, paths, cwd, scope) {
|
|
|
73
74
|
},
|
|
74
75
|
];
|
|
75
76
|
}
|
|
77
|
+
const uniq = (values) => [...new Set(values)];
|
|
78
|
+
/**
|
|
79
|
+
* The targets an "Allow always" answer may store. Stored rules are matched as globs, so a target holding
|
|
80
|
+
* its own `*` or `?` would be remembered as a wildcard that matches other commands; such a target is
|
|
81
|
+
* allowed once and never stored. Request patterns stay raw, so existing rules match exactly as before.
|
|
82
|
+
*/
|
|
83
|
+
const rememberable = (targets) => targets.filter((target) => !/[*?]/.test(target));
|
|
84
|
+
/** Marks a bash request target that no command-specific rule can match: only a `*` rule decides it. */
|
|
85
|
+
export const UNPARSED_BASH_PREFIX = "(unparsed) ";
|
|
86
|
+
/**
|
|
87
|
+
* The `bash` requests for one command. Every simple command it runs is decided, so the call is as
|
|
88
|
+
* strict as its strictest command: one pattern per command (its arity prefix), all of which must be
|
|
89
|
+
* allowed, and "Allow always" stores exactly those prefixes. A command the lexer cannot parse is one
|
|
90
|
+
* target only a `*` rule (a preset, a blanket setting) can decide, so no grant for one command can
|
|
91
|
+
* allow it; its plain words are still checked against deny rules.
|
|
92
|
+
*/
|
|
93
|
+
function bashCommandRequests(command) {
|
|
94
|
+
const analysis = analyzeBashCommand(command);
|
|
95
|
+
if (analysis.kind === "unparsed") {
|
|
96
|
+
const target = `${UNPARSED_BASH_PREFIX}${command}`;
|
|
97
|
+
const candidates = uniq(bestEffortCommandLines(command)
|
|
98
|
+
.map((words) => BashArity.prefix(words).join(" "))
|
|
99
|
+
.filter(Boolean));
|
|
100
|
+
// The deny-only check runs first, so a denied call is refused before anyone is asked.
|
|
101
|
+
return [
|
|
102
|
+
...(candidates.length === 0
|
|
103
|
+
? []
|
|
104
|
+
: [{ permission: "bash", patterns: candidates, always: [], autoApproveAsk: true }]),
|
|
105
|
+
{ permission: "bash", patterns: [target], always: rememberable([target]) },
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
const prefixes = uniq(analysis.segments.map((words) => BashArity.prefix([...words]).join(" ")).filter(Boolean));
|
|
109
|
+
if (prefixes.length === 0)
|
|
110
|
+
return [{ permission: "bash", patterns: [command], always: rememberable([command]) }];
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
permission: "bash",
|
|
114
|
+
patterns: prefixes,
|
|
115
|
+
always: rememberable(prefixes).flatMap((prefix) => [prefix, `${prefix} *`]),
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
76
119
|
/** The `eval` class a cell carries on its own, used when its tool-owned parser names anything narrower. */
|
|
77
120
|
export function fallbackEvalPermissionRequest() {
|
|
78
121
|
return { permission: "eval", patterns: ["*"], always: ["*"] };
|
|
@@ -135,16 +178,7 @@ export function createBuiltinParserRegistry() {
|
|
|
135
178
|
if (!command) {
|
|
136
179
|
return [fallbackPermissionRequest("bash")];
|
|
137
180
|
}
|
|
138
|
-
const
|
|
139
|
-
const prefix = BashArity.prefix(tokens).join(" ");
|
|
140
|
-
const always = prefix ? [prefix, `${prefix} *`] : ["*"];
|
|
141
|
-
const requests = [
|
|
142
|
-
{
|
|
143
|
-
permission: "bash",
|
|
144
|
-
patterns: [prefix || command],
|
|
145
|
-
always,
|
|
146
|
-
},
|
|
147
|
-
];
|
|
181
|
+
const requests = bashCommandRequests(command);
|
|
148
182
|
const externalPaths = extractExternalPaths(command, cwd);
|
|
149
183
|
if (externalPaths.length > 0) {
|
|
150
184
|
requests.push({
|
|
@@ -13,6 +13,7 @@ import { ENTRIES_TAIL_CAPABILITY, OBSERVE_READS_CAPABILITY, SESSION_STATUS_CAPAB
|
|
|
13
13
|
import { answerGetEntries } from "../rpc/get-entries-command.js";
|
|
14
14
|
import { answeredUiRequestId, settledQuestionStatus, unansweredQuestionIds } from "../rpc/extension-ui-response.js";
|
|
15
15
|
import { buildRpcSessionState } from "../rpc/rpc-session-state.js";
|
|
16
|
+
import { sessionUiAnswerLedger } from "../rpc/ui-answer-ledger.js";
|
|
16
17
|
import { runTuiRpcCommand, TUI_RPC_COMMANDS } from "./session-control-rpc-commands.js";
|
|
17
18
|
import { failure, success } from "./session-control-server.js";
|
|
18
19
|
import { runSessionControlCommand, SESSION_CONTROL_COMMANDS, } from "./session-control-session-commands.js";
|
|
@@ -45,6 +46,8 @@ export async function runControlCommand(context, connection, command) {
|
|
|
45
46
|
TUI_OBSERVER_CAPABILITY,
|
|
46
47
|
OBSERVE_READS_CAPABILITY,
|
|
47
48
|
ENTRIES_TAIL_CAPABILITY,
|
|
49
|
+
"snapshot_cursor",
|
|
50
|
+
"ui_answer_ledger",
|
|
48
51
|
],
|
|
49
52
|
mode: "tui",
|
|
50
53
|
endpoint_kind: "tui",
|
|
@@ -89,7 +92,7 @@ export async function runControlCommand(context, connection, command) {
|
|
|
89
92
|
return success(id, type, { admitted: result.admitted ?? [] });
|
|
90
93
|
}
|
|
91
94
|
case "extension_ui_response":
|
|
92
|
-
return answerQuestion(context
|
|
95
|
+
return answerQuestion(context, command);
|
|
93
96
|
default: {
|
|
94
97
|
const rpc = await runTuiRpcCommand(context, connection, command);
|
|
95
98
|
if (rpc !== undefined)
|
|
@@ -121,12 +124,25 @@ function sessionRow(session) {
|
|
|
121
124
|
* settles by the host's rule (`settledQuestionStatus`), so the same frame reaches the model as the same
|
|
122
125
|
* message on either surface.
|
|
123
126
|
*/
|
|
124
|
-
function answerQuestion(
|
|
127
|
+
function answerQuestion(context, command) {
|
|
128
|
+
const { surface, session } = context;
|
|
129
|
+
const ledger = sessionUiAnswerLedger(session);
|
|
130
|
+
const answerId = command.answerId;
|
|
131
|
+
if (answerId !== undefined && (typeof answerId !== "string" || answerId.length < 1 || answerId.length > 256))
|
|
132
|
+
return failure(command.id, "extension_ui_response", "invalid_answer_id");
|
|
125
133
|
const frameId = command.id;
|
|
126
134
|
const requestId = answeredUiRequestId(command);
|
|
135
|
+
const receipt = requestId === undefined || answerId === undefined ? undefined : ledger.lookup(requestId, answerId);
|
|
136
|
+
if (receipt === "replay")
|
|
137
|
+
return { ...success(frameId, "extension_ui_response"), disposition: receipt };
|
|
138
|
+
if (receipt === "already_answered")
|
|
139
|
+
return { ...failure(frameId, "extension_ui_response", "already_answered"), disposition: receipt };
|
|
127
140
|
const request = requestId === undefined ? undefined : surface.pendingQuestion(requestId);
|
|
128
141
|
if (requestId === undefined || request === undefined) {
|
|
129
|
-
return
|
|
142
|
+
return {
|
|
143
|
+
...failure(frameId, "extension_ui_response", answerId === undefined ? "unknown_request" : "unknown_extension_ui_request"),
|
|
144
|
+
disposition: "unknown",
|
|
145
|
+
};
|
|
130
146
|
}
|
|
131
147
|
const response = questionResponse(command, request.questions);
|
|
132
148
|
if (response === "question_incomplete")
|
|
@@ -134,7 +150,8 @@ function answerQuestion(surface, command) {
|
|
|
134
150
|
if (response === undefined || !surface.answerQuestion(requestId, response)) {
|
|
135
151
|
return failure(frameId, "extension_ui_response", "invalid_response");
|
|
136
152
|
}
|
|
137
|
-
|
|
153
|
+
ledger.record(requestId, answerId);
|
|
154
|
+
return { ...success(frameId, "extension_ui_response"), disposition: "applied" };
|
|
138
155
|
}
|
|
139
156
|
function questionResponse(command, questions) {
|
|
140
157
|
// Another client's cancel (the desktop's Remove) is the user's; the builtin settles it from its own draft.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentSession } from "../../core/agent-session.ts";
|
|
2
2
|
import { type ControlAttachments } from "./session-control-attach.ts";
|
|
3
3
|
import { type ControlCommand, type ControlConnection } from "./session-control-server.ts";
|
|
4
|
-
export declare const TUI_RPC_COMMANDS: readonly ["open_session", "subscribe_sessions", "unsubscribe_sessions", "steer", "follow_up", "prompt", "get_effect_status", "set_client_info", "attach_session", "release_attachment", "abort"];
|
|
4
|
+
export declare const TUI_RPC_COMMANDS: readonly ["open_session", "get_snapshot", "subscribe_sessions", "unsubscribe_sessions", "steer", "follow_up", "prompt", "get_effect_status", "set_client_info", "attach_session", "release_attachment", "abort"];
|
|
5
5
|
export interface RpcCommandContext {
|
|
6
6
|
/** This endpoint's instance id: the `expectedHostInstance` an `attach_session` may name. */
|
|
7
7
|
readonly instanceId: string;
|
|
@@ -17,11 +17,13 @@ import { CONNECTION_ROLE_FIXED } from "../rpc/connection-roles.js";
|
|
|
17
17
|
import { INTERACTIVE_USER_CAPABILITY } from "../rpc/custom-capability.js";
|
|
18
18
|
import { runAbort, runGetEffectStatus } from "../rpc/effect-commands.js";
|
|
19
19
|
import { buildRpcSessionState } from "../rpc/rpc-session-state.js";
|
|
20
|
+
import { getSnapshot } from "../rpc/get-snapshot.js";
|
|
20
21
|
import { answerSubscribeSessions, answerUnsubscribeSessions, refusal } from "../rpc/session-status-commands.js";
|
|
21
22
|
import { keyOf } from "./session-control-attach.js";
|
|
22
23
|
import { failure, success } from "./session-control-server.js";
|
|
23
24
|
export const TUI_RPC_COMMANDS = [
|
|
24
25
|
"open_session",
|
|
26
|
+
"get_snapshot",
|
|
25
27
|
"subscribe_sessions",
|
|
26
28
|
"unsubscribe_sessions",
|
|
27
29
|
"steer",
|
|
@@ -38,6 +40,23 @@ const STOP_REQUIRES_INTERACTIVE_CLIENT = "stop_requires_interactive_client: decl
|
|
|
38
40
|
/** `null` = answered on the writer already; `undefined` = not a command this file owns. */
|
|
39
41
|
export async function runTuiRpcCommand(context, connection, command) {
|
|
40
42
|
switch (command.type) {
|
|
43
|
+
case "get_snapshot": {
|
|
44
|
+
const { attachments, session } = context;
|
|
45
|
+
if (command.sessionId !== undefined && command.sessionId !== attachments.handle)
|
|
46
|
+
return {
|
|
47
|
+
id: command.id,
|
|
48
|
+
type: "response",
|
|
49
|
+
command: command.type,
|
|
50
|
+
success: false,
|
|
51
|
+
error: "unknown_session",
|
|
52
|
+
};
|
|
53
|
+
const snapshot = getSnapshot(session, { tail: command.tail, maxBytes: command.maxBytes }, {
|
|
54
|
+
enqueue: (markerId) => attachments.run(connection, () => attachments.writer.enqueue(attachments.handle, { type: "snapshot_marker", markerId })),
|
|
55
|
+
});
|
|
56
|
+
return "error" in snapshot
|
|
57
|
+
? { id: command.id, type: "response", command: command.type, success: false, error: snapshot.error }
|
|
58
|
+
: answered(idOf(command), command.type, snapshot.data);
|
|
59
|
+
}
|
|
41
60
|
case "open_session":
|
|
42
61
|
return openSession(context, connection, command);
|
|
43
62
|
case "subscribe_sessions":
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type CompactStorageFailure, type CompactStorageIo } from "./compact-storage-io.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Owned bounded leaves for compact current state (senpi#4272, R3.C6.4 / R3.C2.4).
|
|
4
|
+
*
|
|
5
|
+
* Acceptance never waits here: a leaf is created synchronously in RAM, and a spill only relocates
|
|
6
|
+
* that immutable value. Its RAM copy is released only after the whole file was written and closed;
|
|
7
|
+
* a failed write keeps the bytes resident, halts further spilling and is reported through
|
|
8
|
+
* `status()`, never dropped. All storage work runs as one serial asynchronous job chain.
|
|
9
|
+
*/
|
|
10
|
+
/** 32,768 UTF-16 code units (64 KiB as UTF-16LE), so a split surrogate survives a leaf boundary exactly. */
|
|
11
|
+
export declare const COMPACT_LEAF_CODE_UNITS = 32768;
|
|
12
|
+
export type LeafId = number;
|
|
13
|
+
export interface CompactStorageStatus {
|
|
14
|
+
readonly residentPayloadBytes: number;
|
|
15
|
+
readonly pendingIoCopyBytes: number;
|
|
16
|
+
readonly pinnedOnlyBytes: number;
|
|
17
|
+
readonly spilledLiveBytes: number;
|
|
18
|
+
readonly spillBytesWritten: number;
|
|
19
|
+
readonly reclaimDebtBytes: number;
|
|
20
|
+
/** Resident bytes above the spill target that a halted spool keeps in RAM (ruling 08 condition 1). */
|
|
21
|
+
readonly spillFailureRetainedBytes: number;
|
|
22
|
+
readonly spillHalted: boolean;
|
|
23
|
+
readonly failures: readonly CompactStorageFailure[];
|
|
24
|
+
}
|
|
25
|
+
export interface CompactContentStoreOptions {
|
|
26
|
+
/** Private epoch spool directory the store creates itself (0700, never an existing one; files 0600). Not a transcript. */
|
|
27
|
+
readonly dir: string;
|
|
28
|
+
readonly io?: CompactStorageIo;
|
|
29
|
+
/** Spilling starts above this many resident leaf bytes and stops at the target. */
|
|
30
|
+
readonly spillAboveBytes?: number;
|
|
31
|
+
readonly spillTargetBytes?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare class CompactContentStore {
|
|
34
|
+
readonly dir: string;
|
|
35
|
+
private readonly io;
|
|
36
|
+
private readonly spillAboveBytes;
|
|
37
|
+
private readonly spillTargetBytes;
|
|
38
|
+
private readonly leaves;
|
|
39
|
+
/** Resident leaves in creation order, so each spill takes the oldest without rescanning committed ones. */
|
|
40
|
+
private readonly ramLeaves;
|
|
41
|
+
private readonly failures;
|
|
42
|
+
private nextLeafId;
|
|
43
|
+
private nextFile;
|
|
44
|
+
private dirReady;
|
|
45
|
+
private spoolToken;
|
|
46
|
+
private chain;
|
|
47
|
+
private queued;
|
|
48
|
+
private spillQueued;
|
|
49
|
+
private halted;
|
|
50
|
+
private retired;
|
|
51
|
+
private residentPayloadBytes;
|
|
52
|
+
private pendingIoCopyBytes;
|
|
53
|
+
private pinnedOnlyBytes;
|
|
54
|
+
private spilledLiveBytes;
|
|
55
|
+
private spillBytesWritten;
|
|
56
|
+
private reclaimDebtBytes;
|
|
57
|
+
constructor(options: CompactContentStoreOptions);
|
|
58
|
+
/** Takes ownership of an immutable value with one live reference. */
|
|
59
|
+
createLeaf(text: string): LeafId;
|
|
60
|
+
/** Moves live and cut-pin references; the last reference drops RAM or reclaims the file. */
|
|
61
|
+
adjust(id: LeafId, live: number, pins: number): void;
|
|
62
|
+
/** Reads a leaf the caller still references, from RAM or its committed file. */
|
|
63
|
+
readLeaf(id: LeafId): Promise<string>;
|
|
64
|
+
/** Resolves once every queued storage job, including the ones they queued, has completed. */
|
|
65
|
+
idle(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Invalidates spilling, joins owned I/O, then removes the epoch directory only when this store
|
|
68
|
+
* created it and it still passes the ownership checks; otherwise rejects with `CompactSpoolRefusedError`.
|
|
69
|
+
*/
|
|
70
|
+
retire(): Promise<void>;
|
|
71
|
+
status(): CompactStorageStatus;
|
|
72
|
+
/** Reads a committed file and rejects a missing, unreadable or short one. */
|
|
73
|
+
private readFile;
|
|
74
|
+
/** Writes one new spool file; undefined means the write failed, was reported, and spilling halted. */
|
|
75
|
+
private writeFile;
|
|
76
|
+
/**
|
|
77
|
+
* An unreferenced (or partially written) spool file: reclamation debt until its unlink completes or
|
|
78
|
+
* finds it gone. Files dropped after retirement started belong to the directory retirement removes.
|
|
79
|
+
*/
|
|
80
|
+
private discard;
|
|
81
|
+
/** A failed unlink keeps its debt and is retried with bounded backoff outside the job chain. */
|
|
82
|
+
private reclaim;
|
|
83
|
+
private get spillAllowed();
|
|
84
|
+
/** True while a storage failure or unreclaimed debt above the work allowance holds spilling back. */
|
|
85
|
+
private get spillStopped();
|
|
86
|
+
private enqueue;
|
|
87
|
+
private scheduleSpill;
|
|
88
|
+
/** Spills one oldest resident leaf per job, so other storage work interleaves fairly. */
|
|
89
|
+
private spillNext;
|
|
90
|
+
/** Creates the private epoch directory once; a refusal or failure halts spilling and is reported. */
|
|
91
|
+
private ensureDir;
|
|
92
|
+
private readFailure;
|
|
93
|
+
private fail;
|
|
94
|
+
private record;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=compact-content-store.d.ts.map
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { createSpoolDir, removeSpoolDir } from "./compact-spool-dir.js";
|
|
3
|
+
import { CompactStorageReadError, describeCause, nodeCompactStorageIo, } from "./compact-storage-io.js";
|
|
4
|
+
/**
|
|
5
|
+
* Owned bounded leaves for compact current state (senpi#4272, R3.C6.4 / R3.C2.4).
|
|
6
|
+
*
|
|
7
|
+
* Acceptance never waits here: a leaf is created synchronously in RAM, and a spill only relocates
|
|
8
|
+
* that immutable value. Its RAM copy is released only after the whole file was written and closed;
|
|
9
|
+
* a failed write keeps the bytes resident, halts further spilling and is reported through
|
|
10
|
+
* `status()`, never dropped. All storage work runs as one serial asynchronous job chain.
|
|
11
|
+
*/
|
|
12
|
+
/** 32,768 UTF-16 code units (64 KiB as UTF-16LE), so a split surrogate survives a leaf boundary exactly. */
|
|
13
|
+
export const COMPACT_LEAF_CODE_UNITS = 32_768;
|
|
14
|
+
const RECLAIM_WORK_ALLOWANCE_BYTES = 1024 * 1024;
|
|
15
|
+
const RECLAIM_RETRY_DELAYS_MS = [100, 400, 1_600, 6_400, 25_600];
|
|
16
|
+
const FAILURE_HISTORY = 32;
|
|
17
|
+
export class CompactContentStore {
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.leaves = new Map();
|
|
20
|
+
/** Resident leaves in creation order, so each spill takes the oldest without rescanning committed ones. */
|
|
21
|
+
this.ramLeaves = new Set();
|
|
22
|
+
this.failures = [];
|
|
23
|
+
this.nextLeafId = 1;
|
|
24
|
+
this.nextFile = 1;
|
|
25
|
+
this.chain = Promise.resolve();
|
|
26
|
+
this.queued = 0;
|
|
27
|
+
this.spillQueued = false;
|
|
28
|
+
this.halted = false;
|
|
29
|
+
this.retired = false;
|
|
30
|
+
this.residentPayloadBytes = 0;
|
|
31
|
+
this.pendingIoCopyBytes = 0;
|
|
32
|
+
this.pinnedOnlyBytes = 0;
|
|
33
|
+
this.spilledLiveBytes = 0;
|
|
34
|
+
this.spillBytesWritten = 0;
|
|
35
|
+
this.reclaimDebtBytes = 0;
|
|
36
|
+
this.dir = options.dir;
|
|
37
|
+
this.io = options.io ?? nodeCompactStorageIo;
|
|
38
|
+
this.spillAboveBytes = options.spillAboveBytes ?? 4 * 1024 * 1024;
|
|
39
|
+
this.spillTargetBytes = options.spillTargetBytes ?? 2 * 1024 * 1024;
|
|
40
|
+
}
|
|
41
|
+
/** Takes ownership of an immutable value with one live reference. */
|
|
42
|
+
createLeaf(text) {
|
|
43
|
+
const id = this.nextLeafId++;
|
|
44
|
+
this.leaves.set(id, { text, units: text.length, live: 1, pins: 0, state: "ram" });
|
|
45
|
+
this.ramLeaves.add(id);
|
|
46
|
+
this.residentPayloadBytes += text.length * 2;
|
|
47
|
+
this.scheduleSpill();
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
/** Moves live and cut-pin references; the last reference drops RAM or reclaims the file. */
|
|
51
|
+
adjust(id, live, pins) {
|
|
52
|
+
const leaf = this.leaves.get(id);
|
|
53
|
+
if (!leaf)
|
|
54
|
+
throw new Error(`compact leaf ${id} is not owned`);
|
|
55
|
+
const pinnedOnly = leaf.live === 0 && leaf.pins > 0;
|
|
56
|
+
leaf.live += live;
|
|
57
|
+
leaf.pins += pins;
|
|
58
|
+
if (pinnedOnly !== (leaf.live === 0 && leaf.pins > 0))
|
|
59
|
+
this.pinnedOnlyBytes += (pinnedOnly ? -2 : 2) * leaf.units;
|
|
60
|
+
if (leaf.live > 0 || leaf.pins > 0)
|
|
61
|
+
return;
|
|
62
|
+
this.leaves.delete(id);
|
|
63
|
+
this.ramLeaves.delete(id);
|
|
64
|
+
if (leaf.state === "committed" && leaf.path) {
|
|
65
|
+
this.spilledLiveBytes -= leaf.units * 2;
|
|
66
|
+
this.discard(leaf.path, leaf.units * 2);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.residentPayloadBytes -= leaf.units * 2;
|
|
70
|
+
}
|
|
71
|
+
/** Reads a leaf the caller still references, from RAM or its committed file. */
|
|
72
|
+
async readLeaf(id) {
|
|
73
|
+
const leaf = this.leaves.get(id);
|
|
74
|
+
if (!leaf)
|
|
75
|
+
throw new Error(`compact leaf ${id} is not owned`);
|
|
76
|
+
if (leaf.text !== undefined)
|
|
77
|
+
return leaf.text;
|
|
78
|
+
return (await this.readFile(leaf.path ?? "", leaf.units * 2, "leaf_read")).toString("utf16le");
|
|
79
|
+
}
|
|
80
|
+
/** Resolves once every queued storage job, including the ones they queued, has completed. */
|
|
81
|
+
async idle() {
|
|
82
|
+
while (this.queued > 0)
|
|
83
|
+
await this.chain;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Invalidates spilling, joins owned I/O, then removes the epoch directory only when this store
|
|
87
|
+
* created it and it still passes the ownership checks; otherwise rejects with `CompactSpoolRefusedError`.
|
|
88
|
+
*/
|
|
89
|
+
async retire() {
|
|
90
|
+
this.retired = true;
|
|
91
|
+
await this.idle();
|
|
92
|
+
const token = this.spoolToken;
|
|
93
|
+
this.spoolToken = undefined;
|
|
94
|
+
if (token !== undefined)
|
|
95
|
+
await removeSpoolDir(this.dir, token);
|
|
96
|
+
}
|
|
97
|
+
status() {
|
|
98
|
+
return {
|
|
99
|
+
residentPayloadBytes: this.residentPayloadBytes,
|
|
100
|
+
pendingIoCopyBytes: this.pendingIoCopyBytes,
|
|
101
|
+
pinnedOnlyBytes: this.pinnedOnlyBytes,
|
|
102
|
+
spilledLiveBytes: this.spilledLiveBytes,
|
|
103
|
+
spillBytesWritten: this.spillBytesWritten,
|
|
104
|
+
reclaimDebtBytes: this.reclaimDebtBytes,
|
|
105
|
+
spillFailureRetainedBytes: this.spillStopped
|
|
106
|
+
? Math.max(0, this.residentPayloadBytes - this.spillTargetBytes)
|
|
107
|
+
: 0,
|
|
108
|
+
spillHalted: this.spillStopped,
|
|
109
|
+
failures: [...this.failures],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Reads a committed file and rejects a missing, unreadable or short one. */
|
|
113
|
+
async readFile(path, bytes, operation) {
|
|
114
|
+
let data;
|
|
115
|
+
try {
|
|
116
|
+
data = await this.io.readFile(path);
|
|
117
|
+
}
|
|
118
|
+
catch (cause) {
|
|
119
|
+
throw this.readFailure(operation, path, cause);
|
|
120
|
+
}
|
|
121
|
+
if (data.byteLength !== bytes) {
|
|
122
|
+
throw this.readFailure(operation, path, new Error(`expected ${bytes} bytes, found ${data.byteLength}`));
|
|
123
|
+
}
|
|
124
|
+
return data;
|
|
125
|
+
}
|
|
126
|
+
/** Writes one new spool file; undefined means the write failed, was reported, and spilling halted. */
|
|
127
|
+
async writeFile(data, operation, retainedInMemoryBytes) {
|
|
128
|
+
const path = await this.ensureDir(retainedInMemoryBytes);
|
|
129
|
+
if (path === undefined)
|
|
130
|
+
return undefined;
|
|
131
|
+
const file = join(path, `leaf-${this.nextFile++}.bin`);
|
|
132
|
+
this.pendingIoCopyBytes += data.byteLength;
|
|
133
|
+
try {
|
|
134
|
+
await this.io.writeFile(file, data);
|
|
135
|
+
this.spillBytesWritten += data.byteLength;
|
|
136
|
+
return file;
|
|
137
|
+
}
|
|
138
|
+
catch (cause) {
|
|
139
|
+
this.fail(operation, file, cause, retainedInMemoryBytes);
|
|
140
|
+
this.discard(file, data.byteLength);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
this.pendingIoCopyBytes -= data.byteLength;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* An unreferenced (or partially written) spool file: reclamation debt until its unlink completes or
|
|
149
|
+
* finds it gone. Files dropped after retirement started belong to the directory retirement removes.
|
|
150
|
+
*/
|
|
151
|
+
discard(path, bytes) {
|
|
152
|
+
if (this.retired)
|
|
153
|
+
return;
|
|
154
|
+
this.reclaimDebtBytes += bytes;
|
|
155
|
+
this.enqueue("reclaim", () => this.reclaim(path, bytes, 0));
|
|
156
|
+
}
|
|
157
|
+
/** A failed unlink keeps its debt and is retried with bounded backoff outside the job chain. */
|
|
158
|
+
async reclaim(path, bytes, attempt) {
|
|
159
|
+
try {
|
|
160
|
+
await this.io.unlink(path);
|
|
161
|
+
}
|
|
162
|
+
catch (cause) {
|
|
163
|
+
if (cause.code !== "ENOENT") {
|
|
164
|
+
this.fail("reclaim", path, cause, 0);
|
|
165
|
+
const delay = RECLAIM_RETRY_DELAYS_MS[attempt];
|
|
166
|
+
if (delay !== undefined) {
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
if (this.retired)
|
|
169
|
+
this.reclaimDebtBytes -= bytes;
|
|
170
|
+
else
|
|
171
|
+
this.enqueue("reclaim", () => this.reclaim(path, bytes, attempt + 1));
|
|
172
|
+
}, delay).unref?.();
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
this.reclaimDebtBytes -= bytes;
|
|
178
|
+
this.scheduleSpill();
|
|
179
|
+
}
|
|
180
|
+
get spillAllowed() {
|
|
181
|
+
return !this.halted && !this.retired;
|
|
182
|
+
}
|
|
183
|
+
/** True while a storage failure or unreclaimed debt above the work allowance holds spilling back. */
|
|
184
|
+
get spillStopped() {
|
|
185
|
+
return this.halted || this.reclaimDebtBytes > RECLAIM_WORK_ALLOWANCE_BYTES;
|
|
186
|
+
}
|
|
187
|
+
enqueue(operation, job) {
|
|
188
|
+
this.queued++;
|
|
189
|
+
this.chain = this.chain
|
|
190
|
+
.then(job)
|
|
191
|
+
.catch((cause) => {
|
|
192
|
+
if (operation === "spill_write")
|
|
193
|
+
this.spillQueued = false;
|
|
194
|
+
this.fail(operation, this.dir, cause, 0);
|
|
195
|
+
})
|
|
196
|
+
.finally(() => {
|
|
197
|
+
this.queued--;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
scheduleSpill() {
|
|
201
|
+
if (this.spillQueued || !this.spillAllowed || this.residentPayloadBytes <= this.spillAboveBytes)
|
|
202
|
+
return;
|
|
203
|
+
if (this.reclaimDebtBytes > RECLAIM_WORK_ALLOWANCE_BYTES)
|
|
204
|
+
return;
|
|
205
|
+
this.spillQueued = true;
|
|
206
|
+
this.enqueue("spill_write", () => this.spillNext());
|
|
207
|
+
}
|
|
208
|
+
/** Spills one oldest resident leaf per job, so other storage work interleaves fairly. */
|
|
209
|
+
async spillNext() {
|
|
210
|
+
const [id] = this.ramLeaves;
|
|
211
|
+
const leaf = id === undefined ? undefined : this.leaves.get(id);
|
|
212
|
+
if (id === undefined || !leaf || leaf.text === undefined || !this.spillAllowed) {
|
|
213
|
+
this.spillQueued = false;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const bytes = leaf.units * 2;
|
|
217
|
+
leaf.state = "pending";
|
|
218
|
+
this.ramLeaves.delete(id);
|
|
219
|
+
const path = await this.writeFile(Buffer.from(leaf.text, "utf16le"), "spill_write", bytes);
|
|
220
|
+
const owned = this.leaves.get(id) === leaf;
|
|
221
|
+
if (path === undefined) {
|
|
222
|
+
leaf.state = "ram";
|
|
223
|
+
if (owned)
|
|
224
|
+
this.ramLeaves.add(id);
|
|
225
|
+
}
|
|
226
|
+
else if (!owned || this.retired) {
|
|
227
|
+
this.discard(path, bytes);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
leaf.state = "committed";
|
|
231
|
+
leaf.path = path;
|
|
232
|
+
leaf.text = undefined;
|
|
233
|
+
this.residentPayloadBytes -= bytes;
|
|
234
|
+
this.spilledLiveBytes += bytes;
|
|
235
|
+
}
|
|
236
|
+
this.spillQueued = false;
|
|
237
|
+
if (this.residentPayloadBytes > this.spillTargetBytes) {
|
|
238
|
+
this.spillQueued = this.spillAllowed;
|
|
239
|
+
if (this.spillQueued)
|
|
240
|
+
this.enqueue("spill_write", () => this.spillNext());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Creates the private epoch directory once; a refusal or failure halts spilling and is reported. */
|
|
244
|
+
ensureDir(retained) {
|
|
245
|
+
this.dirReady ??= createSpoolDir(this.dir).then((token) => {
|
|
246
|
+
this.spoolToken = token;
|
|
247
|
+
return this.dir;
|
|
248
|
+
}, (cause) => {
|
|
249
|
+
this.fail("spool_create", this.dir, cause, retained);
|
|
250
|
+
return undefined;
|
|
251
|
+
});
|
|
252
|
+
return this.dirReady;
|
|
253
|
+
}
|
|
254
|
+
readFailure(operation, path, cause) {
|
|
255
|
+
this.fail(operation, path, cause, 0);
|
|
256
|
+
return new CompactStorageReadError(operation, path, cause);
|
|
257
|
+
}
|
|
258
|
+
fail(operation, path, cause, retained) {
|
|
259
|
+
if (operation === "spill_write" || operation === "spool_create")
|
|
260
|
+
this.halted = true;
|
|
261
|
+
this.record({ operation, path, cause: describeCause(cause), retainedInMemoryBytes: retained });
|
|
262
|
+
}
|
|
263
|
+
record(failure) {
|
|
264
|
+
this.failures.push(failure);
|
|
265
|
+
if (this.failures.length > FAILURE_HISTORY)
|
|
266
|
+
this.failures.shift();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=compact-content-store.js.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type CompactContentStoreOptions, type CompactStorageStatus, type LeafId } from "./compact-content-store.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The in-memory current-state owner over compact storage (senpi#4272, R3.C6.4 first half).
|
|
4
|
+
*
|
|
5
|
+
* `accept` is synchronous: it validates a typed transition completely, then publishes the next
|
|
6
|
+
* sequence, or throws and leaves the prior root untouched. Block text lives in owned bounded leaves
|
|
7
|
+
* that spill asynchronously; settled message rows live in a resident map whose persistence status
|
|
8
|
+
* changes only through `message_status`, never through storage success or failure. The map is
|
|
9
|
+
* exact for every transition: a status change replaces the whole row, a removal deletes it, and a
|
|
10
|
+
* transition addressing a removed or unknown message is refused rather than masked. `freeze` pins
|
|
11
|
+
* an immutable cut synchronously, and `read` materializes it asynchronously from RAM or disk.
|
|
12
|
+
*/
|
|
13
|
+
type CompactBlockKind = "text" | "thinking" | "tool_arguments";
|
|
14
|
+
type CompactPersistence = "pending" | "appended" | "failed";
|
|
15
|
+
interface BlockAddress {
|
|
16
|
+
readonly messageId: string;
|
|
17
|
+
readonly contentIndex: number;
|
|
18
|
+
}
|
|
19
|
+
export type CompactTransition = (BlockAddress & {
|
|
20
|
+
readonly type: "block_start";
|
|
21
|
+
readonly kind: CompactBlockKind;
|
|
22
|
+
readonly seed: string;
|
|
23
|
+
/**
|
|
24
|
+
* The whole seed when later deltas replay it from offset 0, or 0 when they continue after it. Partial
|
|
25
|
+
* coverage is refused: the uncovered seed tail would make every later delta overlap or leave a gap.
|
|
26
|
+
*/
|
|
27
|
+
readonly coveredCodeUnits: number;
|
|
28
|
+
}) | (BlockAddress & {
|
|
29
|
+
readonly type: "block_delta";
|
|
30
|
+
readonly offset: number;
|
|
31
|
+
readonly text: string;
|
|
32
|
+
}) | (BlockAddress & {
|
|
33
|
+
readonly type: "block_replace";
|
|
34
|
+
readonly text: string;
|
|
35
|
+
}) | {
|
|
36
|
+
readonly type: "message_status";
|
|
37
|
+
readonly messageId: string;
|
|
38
|
+
readonly persistence: CompactPersistence;
|
|
39
|
+
} | {
|
|
40
|
+
readonly type: "message_remove";
|
|
41
|
+
readonly messageId: string;
|
|
42
|
+
};
|
|
43
|
+
interface FrozenBlock {
|
|
44
|
+
readonly contentIndex: number;
|
|
45
|
+
readonly kind: CompactBlockKind;
|
|
46
|
+
readonly leaves: readonly LeafId[];
|
|
47
|
+
/** A cut shares the immutable active tail string (under one leaf) instead of minting a leaf per freeze. */
|
|
48
|
+
readonly tail: string;
|
|
49
|
+
}
|
|
50
|
+
interface SettledRow {
|
|
51
|
+
readonly persistence: CompactPersistence;
|
|
52
|
+
readonly blocks: readonly FrozenBlock[];
|
|
53
|
+
}
|
|
54
|
+
interface FrozenCompactState {
|
|
55
|
+
readonly sequence: number;
|
|
56
|
+
readonly active: ReadonlyMap<string, readonly FrozenBlock[]>;
|
|
57
|
+
readonly settled: ReadonlyMap<string, SettledRow>;
|
|
58
|
+
}
|
|
59
|
+
interface CompactMessageView {
|
|
60
|
+
readonly messageId: string;
|
|
61
|
+
readonly persistence: CompactPersistence | undefined;
|
|
62
|
+
readonly blocks: readonly {
|
|
63
|
+
readonly contentIndex: number;
|
|
64
|
+
readonly kind: CompactBlockKind;
|
|
65
|
+
readonly text: string;
|
|
66
|
+
}[];
|
|
67
|
+
}
|
|
68
|
+
export declare class CompactSessionState {
|
|
69
|
+
private readonly store;
|
|
70
|
+
private readonly settled;
|
|
71
|
+
private readonly active;
|
|
72
|
+
private sequence;
|
|
73
|
+
private frozenTailBytes;
|
|
74
|
+
constructor(options: CompactContentStoreOptions);
|
|
75
|
+
get acceptedSequence(): number;
|
|
76
|
+
accept(transition: CompactTransition): number;
|
|
77
|
+
freeze(): FrozenCompactState;
|
|
78
|
+
release(frozen: FrozenCompactState): void;
|
|
79
|
+
/** Rejects with the storage error when committed content is gone; never returns empty content for it. */
|
|
80
|
+
read(frozen: FrozenCompactState): Promise<{
|
|
81
|
+
sequence: number;
|
|
82
|
+
messages: CompactMessageView[];
|
|
83
|
+
}>;
|
|
84
|
+
storageStatus(): CompactStorageStatus & {
|
|
85
|
+
readonly residentTailBytes: number;
|
|
86
|
+
/** Active tails held by unreleased cuts; may share storage with `residentTailBytes`. */
|
|
87
|
+
readonly frozenTailBytes: number;
|
|
88
|
+
readonly hotWitnessBytes: number;
|
|
89
|
+
/** Settled rows kept resident: S03a-1 has no paged row index, so every row header stays in RAM. */
|
|
90
|
+
readonly residentSettledRows: number;
|
|
91
|
+
};
|
|
92
|
+
idle(): Promise<void>;
|
|
93
|
+
retire(): Promise<void>;
|
|
94
|
+
private start;
|
|
95
|
+
/**
|
|
96
|
+
* Appends only the new suffix. Overlap exists only while a fully covered seed is being replayed
|
|
97
|
+
* (`emitted < covered == units`), so it always lies inside the producer-declared coverage.
|
|
98
|
+
*/
|
|
99
|
+
private delta;
|
|
100
|
+
private replace;
|
|
101
|
+
private setPersistence;
|
|
102
|
+
private remove;
|
|
103
|
+
private block;
|
|
104
|
+
private append;
|
|
105
|
+
/** Settling mints the final tail into a spillable leaf; a freeze of an active block shares the tail. */
|
|
106
|
+
private seal;
|
|
107
|
+
private pin;
|
|
108
|
+
private texts;
|
|
109
|
+
}
|
|
110
|
+
export {};
|
|
111
|
+
//# sourceMappingURL=compact-session-state.d.ts.map
|