@code-yeongyu/senpi 2026.10.25-3 → 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 +40 -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-FFQOEC76.js → chunk-3A3XCLNH.js} +3 -3
- package/dist/bundle/chunks/{chunk-EHQXLHRB.js → chunk-3LPWILUZ.js} +2 -2
- package/dist/bundle/chunks/{chunk-3BTN337S.js → chunk-3WP5RFQR.js} +1 -1
- package/dist/bundle/chunks/{chunk-WLHNEUGD.js → chunk-5FQMNHXV.js} +1 -1
- package/dist/bundle/chunks/{chunk-HVJI5T75.js → chunk-5KZH6EYQ.js} +2 -2
- package/dist/bundle/chunks/{chunk-4P7WARQZ.js → chunk-5LWUUXMH.js} +1 -1
- package/dist/bundle/chunks/{chunk-ODTGC6YU.js → chunk-6U3TBFBA.js} +1 -1
- package/dist/bundle/chunks/chunk-6VWIMHMH.js +2 -0
- 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-YRF4I43T.js → chunk-EZP6AYW2.js} +37 -31
- package/dist/bundle/chunks/chunk-GQJYNFMO.js +2 -0
- package/dist/bundle/chunks/{chunk-O3Q2ET6W.js → chunk-GWGNVYPB.js} +1 -1
- package/dist/bundle/chunks/{chunk-Z3UYNSVR.js → chunk-H4WJ4LY3.js} +1 -1
- package/dist/bundle/chunks/{chunk-UQNRW3VU.js → chunk-HWTD4QF3.js} +1 -1
- package/dist/bundle/chunks/{chunk-WD6WBYC5.js → chunk-I7QWTWVZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-SWJXRC2I.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-JA2R2ZSE.js → chunk-L2O6K3SJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-4AOMNOGY.js → chunk-LO5MYLGF.js} +1 -1
- package/dist/bundle/chunks/{chunk-P3M2PF6K.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-MJ5NNWX4.js → chunk-QYFOHFPS.js} +1 -1
- package/dist/bundle/chunks/chunk-SOXQ42VS.js +2 -0
- 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-XMJKZK5H.js +2 -0
- package/dist/bundle/chunks/{chunk-C5UFJ7B3.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-CNL6QSUB.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-ONQIMWMX.js → host-lifecycle-HX3EGRRW.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +2 -2
- package/dist/bundle/chunks/{host-session-command-J65ENSV5.js → host-session-command-OV5XDOJK.js} +1 -1
- package/dist/bundle/chunks/{host-socket-request-3CWE4265.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-YI2WZOET.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-GHZPJMRF.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-ZMQB4GPO.js +2 -0
- package/dist/bundle/chunks/rpc-mode-UJXOIZCW.js +2 -0
- package/dist/bundle/chunks/{rpc-monitor-source-FFEQS744.js → rpc-monitor-source-DRZXJHTL.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-YWG6D6ZF.js → schedule-command-KA7LFFNK.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-MFWXUR7Z.js +3 -0
- package/dist/bundle/chunks/session-picker-BP33NRSL.js +2 -0
- package/dist/bundle/chunks/session-worker.js +212 -203
- 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/elicitation.js +2 -4
- 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/core/session-control-actions.js +0 -2
- package/dist/modes/interactive/session-control-attach.d.ts +20 -0
- package/dist/modes/interactive/session-control-attach.js +13 -0
- package/dist/modes/interactive/session-control-commands.js +37 -10
- package/dist/modes/interactive/session-control-registry.d.ts +2 -1
- package/dist/modes/interactive/session-control-registry.js +3 -2
- package/dist/modes/interactive/session-control-rpc-commands.d.ts +4 -2
- package/dist/modes/interactive/session-control-rpc-commands.js +133 -4
- package/dist/modes/interactive/session-control-server.js +9 -3
- 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 +45 -47
- 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/custom-capability.d.ts +6 -0
- package/dist/modes/rpc/custom-capability.js +6 -0
- 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-entries-command.d.ts +13 -0
- package/dist/modes/rpc/get-entries-command.js +61 -0
- package/dist/modes/rpc/get-entries-page.d.ts +22 -0
- package/dist/modes/rpc/get-entries-page.js +41 -2
- 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 +7 -1
- 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.d.ts +2 -1
- package/dist/modes/rpc/host-observe-request.js +5 -1
- 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 +9 -0
- package/dist/modes/rpc/rpc-session-command-types.d.ts +14 -1
- 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 +87 -11
- package/package.json +7 -6
- package/dist/bundle/chunks/azure-openai-responses-7I45DI72.js +0 -2
- package/dist/bundle/chunks/chunk-3LNSLJ7N.js +0 -2
- package/dist/bundle/chunks/chunk-6O4SEW2W.js +0 -5
- package/dist/bundle/chunks/chunk-7W7OJDIY.js +0 -410
- package/dist/bundle/chunks/chunk-DMI55M2N.js +0 -2
- package/dist/bundle/chunks/chunk-H3HNMY2W.js +0 -13
- package/dist/bundle/chunks/chunk-MSQCTH6V.js +0 -2
- package/dist/bundle/chunks/cli-main-HEKFFB5S.js +0 -3
- package/dist/bundle/chunks/dispatch-NYAULNVW.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-Z3WPHJOW.js +0 -2
- package/dist/bundle/chunks/interactive-mode-DEPQC6TX.js +0 -2
- package/dist/bundle/chunks/openai-responses-TNHQ6SJS.js +0 -3
- package/dist/bundle/chunks/rotation-stream-ZPVCFOVD.js +0 -2
- package/dist/bundle/chunks/rpc-mode-ORHBGBBV.js +0 -2
- package/dist/bundle/chunks/session-control-endpoint-36HAQSQO.js +0 -3
- package/dist/bundle/chunks/session-picker-25H5KNYP.js +0 -2
|
@@ -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({
|
|
@@ -10,8 +10,6 @@ export function createSessionControlActions(deps) {
|
|
|
10
10
|
let retired = false;
|
|
11
11
|
return {
|
|
12
12
|
registerControlEndpoint: async (options) => {
|
|
13
|
-
if (process.platform === "win32")
|
|
14
|
-
return { status: "unsupported", reason: "unsupported_platform" };
|
|
15
13
|
const host = deps.host();
|
|
16
14
|
if (host === undefined)
|
|
17
15
|
return { status: "unsupported", reason: "unsupported_mode" };
|
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The terminal itself is one attachment; each attached client adds one. A client that only subscribed
|
|
9
9
|
* (mirror included) is not an attachment.
|
|
10
|
+
*
|
|
11
|
+
* It also keeps what a host keeps per connection and per session for the drive set (senpi#4267): the roles each
|
|
12
|
+
* connection declared with `set_client_info`, and the admission receipt of every input sent with a `clientMessageId`,
|
|
13
|
+
* so a retry is answered from its receipt instead of being delivered twice.
|
|
10
14
|
*/
|
|
11
15
|
import type { AgentSession } from "../../core/agent-session.ts";
|
|
16
|
+
import { ConnectionRoles } from "../rpc/connection-roles.ts";
|
|
12
17
|
import { SessionEventWriter } from "../rpc/session-event-writer.ts";
|
|
13
18
|
import type { ControlConnection } from "./session-control-server.ts";
|
|
14
19
|
export declare class ControlAttachments {
|
|
15
20
|
readonly writer: SessionEventWriter;
|
|
21
|
+
readonly roles: ConnectionRoles;
|
|
22
|
+
readonly receipts: Map<string, TerminalInputReceipt>;
|
|
16
23
|
private readonly attached;
|
|
17
24
|
private readonly stopRecords;
|
|
18
25
|
private readonly session;
|
|
@@ -21,6 +28,8 @@ export declare class ControlAttachments {
|
|
|
21
28
|
get handle(): string;
|
|
22
29
|
opened(connection: ControlConnection): void;
|
|
23
30
|
closed(connection: ControlConnection): void;
|
|
31
|
+
/** Drops this connection's attachment (`release_attachment`); false when it was not attached. */
|
|
32
|
+
detach(connection: ControlConnection): boolean;
|
|
24
33
|
/** Attaches the connection: it is replayed the in-flight snapshot, then receives the session's records. */
|
|
25
34
|
attach(connection: ControlConnection): boolean;
|
|
26
35
|
isAttached(connection: ControlConnection): boolean;
|
|
@@ -30,6 +39,17 @@ export declare class ControlAttachments {
|
|
|
30
39
|
dispose(): void;
|
|
31
40
|
private entryView;
|
|
32
41
|
}
|
|
42
|
+
/** The receipt a terminal answers an input sent with a `clientMessageId` with, and answers again on a retry. */
|
|
43
|
+
export interface TerminalInputReceipt {
|
|
44
|
+
/** What was sent: a retry with the same id and different content is a conflict, as on a host. */
|
|
45
|
+
readonly fingerprint: string;
|
|
46
|
+
readonly disposition: "started" | "queued";
|
|
47
|
+
readonly admission: {
|
|
48
|
+
readonly clientMessageId: string;
|
|
49
|
+
readonly durableSessionId: string;
|
|
50
|
+
readonly disposition: "started" | "queued";
|
|
51
|
+
};
|
|
52
|
+
}
|
|
33
53
|
/** The writer's id for one control connection. */
|
|
34
54
|
export declare function keyOf(connection: ControlConnection): string;
|
|
35
55
|
//# sourceMappingURL=session-control-attach.d.ts.map
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { ConnectionRoles } from "../rpc/connection-roles.js";
|
|
1
2
|
import { SessionEventWriter } from "../rpc/session-event-writer.js";
|
|
2
3
|
import { subscribeSessionRecords } from "../rpc/session-record-stream.js";
|
|
3
4
|
import { sessionRuntimeView } from "../rpc/session-status-source.js";
|
|
4
5
|
export class ControlAttachments {
|
|
5
6
|
constructor(session, pendingQuestions) {
|
|
6
7
|
this.writer = new SessionEventWriter(() => { });
|
|
8
|
+
this.roles = new ConnectionRoles();
|
|
9
|
+
this.receipts = new Map();
|
|
7
10
|
this.attached = new Set();
|
|
8
11
|
this.session = session;
|
|
9
12
|
this.writer.status.setSource({
|
|
@@ -29,8 +32,18 @@ export class ControlAttachments {
|
|
|
29
32
|
const key = keyOf(connection);
|
|
30
33
|
if (this.attached.delete(key))
|
|
31
34
|
this.writer.status.publish(this.handle, "detach", "client");
|
|
35
|
+
this.roles.release(key);
|
|
32
36
|
this.writer.unregisterConnection(key);
|
|
33
37
|
}
|
|
38
|
+
/** Drops this connection's attachment (`release_attachment`); false when it was not attached. */
|
|
39
|
+
detach(connection) {
|
|
40
|
+
const key = keyOf(connection);
|
|
41
|
+
if (!this.attached.delete(key))
|
|
42
|
+
return false;
|
|
43
|
+
this.writer.detachConnectionFromSession(key, this.handle);
|
|
44
|
+
this.writer.status.publish(this.handle, "detach", "client");
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
34
47
|
/** Attaches the connection: it is replayed the in-flight snapshot, then receives the session's records. */
|
|
35
48
|
attach(connection) {
|
|
36
49
|
const key = keyOf(connection);
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
* The terminal control endpoint's command surface. It answers who it is, which one session it holds
|
|
3
3
|
* and that session's state and messages, renames it, streams its feed, wakes its inbox drain, relays
|
|
4
4
|
* an answer to a question the session itself asked, and applies the pane's own session controls (a
|
|
5
|
-
* model, a thinking level, an interrupt - `session-control-session-commands.ts`)
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* model, a thinking level, an interrupt - `session-control-session-commands.ts`), and serves the host's
|
|
6
|
+
* observe-and-drive set over its one session (`session-control-rpc-commands.ts`, senpi#3229, senpi#4267). Input
|
|
7
|
+
* goes through the session's admission and only there. Every other command is answered `unsupported` as data;
|
|
8
|
+
* `get_protocol_info` lists the accepted ones in `commands`.
|
|
9
9
|
*/
|
|
10
10
|
import { VERSION } from "../../config.js";
|
|
11
11
|
import { engineBuildIdentity } from "../../core/engine-build-identity.js";
|
|
12
|
-
import { SESSION_STATUS_CAPABILITY } from "../rpc/custom-capability.js";
|
|
12
|
+
import { ENTRIES_TAIL_CAPABILITY, OBSERVE_READS_CAPABILITY, SESSION_STATUS_CAPABILITY, TUI_OBSERVER_CAPABILITY, } from "../rpc/custom-capability.js";
|
|
13
|
+
import { answerGetEntries } from "../rpc/get-entries-command.js";
|
|
13
14
|
import { answeredUiRequestId, settledQuestionStatus, unansweredQuestionIds } from "../rpc/extension-ui-response.js";
|
|
14
15
|
import { buildRpcSessionState } from "../rpc/rpc-session-state.js";
|
|
16
|
+
import { sessionUiAnswerLedger } from "../rpc/ui-answer-ledger.js";
|
|
15
17
|
import { runTuiRpcCommand, TUI_RPC_COMMANDS } from "./session-control-rpc-commands.js";
|
|
16
18
|
import { failure, success } from "./session-control-server.js";
|
|
17
19
|
import { runSessionControlCommand, SESSION_CONTROL_COMMANDS, } from "./session-control-session-commands.js";
|
|
@@ -21,6 +23,7 @@ const TUI_CONTROL_COMMANDS = [
|
|
|
21
23
|
"list_sessions",
|
|
22
24
|
"get_state",
|
|
23
25
|
"get_messages",
|
|
26
|
+
"get_entries",
|
|
24
27
|
"set_session_name",
|
|
25
28
|
"subscribe",
|
|
26
29
|
"wake",
|
|
@@ -37,7 +40,15 @@ export async function runControlCommand(context, connection, command) {
|
|
|
37
40
|
return success(id, type, {
|
|
38
41
|
protocolVersion: 1,
|
|
39
42
|
serverVersion: VERSION,
|
|
40
|
-
capabilities: [
|
|
43
|
+
capabilities: [
|
|
44
|
+
TUI_CONTROL_CAPABILITY,
|
|
45
|
+
SESSION_STATUS_CAPABILITY,
|
|
46
|
+
TUI_OBSERVER_CAPABILITY,
|
|
47
|
+
OBSERVE_READS_CAPABILITY,
|
|
48
|
+
ENTRIES_TAIL_CAPABILITY,
|
|
49
|
+
"snapshot_cursor",
|
|
50
|
+
"ui_answer_ledger",
|
|
51
|
+
],
|
|
41
52
|
mode: "tui",
|
|
42
53
|
endpoint_kind: "tui",
|
|
43
54
|
instanceId: context.instanceId,
|
|
@@ -60,6 +71,8 @@ export async function runControlCommand(context, connection, command) {
|
|
|
60
71
|
});
|
|
61
72
|
case "get_messages":
|
|
62
73
|
return success(id, type, { messages: session.messages });
|
|
74
|
+
case "get_entries":
|
|
75
|
+
return answerGetEntries(session, command);
|
|
63
76
|
case "set_session_name": {
|
|
64
77
|
const name = typeof command.name === "string" ? command.name.trim() : "";
|
|
65
78
|
if (name === "")
|
|
@@ -79,7 +92,7 @@ export async function runControlCommand(context, connection, command) {
|
|
|
79
92
|
return success(id, type, { admitted: result.admitted ?? [] });
|
|
80
93
|
}
|
|
81
94
|
case "extension_ui_response":
|
|
82
|
-
return answerQuestion(context
|
|
95
|
+
return answerQuestion(context, command);
|
|
83
96
|
default: {
|
|
84
97
|
const rpc = await runTuiRpcCommand(context, connection, command);
|
|
85
98
|
if (rpc !== undefined)
|
|
@@ -111,12 +124,25 @@ function sessionRow(session) {
|
|
|
111
124
|
* settles by the host's rule (`settledQuestionStatus`), so the same frame reaches the model as the same
|
|
112
125
|
* message on either surface.
|
|
113
126
|
*/
|
|
114
|
-
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");
|
|
115
133
|
const frameId = command.id;
|
|
116
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 };
|
|
117
140
|
const request = requestId === undefined ? undefined : surface.pendingQuestion(requestId);
|
|
118
141
|
if (requestId === undefined || request === undefined) {
|
|
119
|
-
return
|
|
142
|
+
return {
|
|
143
|
+
...failure(frameId, "extension_ui_response", answerId === undefined ? "unknown_request" : "unknown_extension_ui_request"),
|
|
144
|
+
disposition: "unknown",
|
|
145
|
+
};
|
|
120
146
|
}
|
|
121
147
|
const response = questionResponse(command, request.questions);
|
|
122
148
|
if (response === "question_incomplete")
|
|
@@ -124,7 +150,8 @@ function answerQuestion(surface, command) {
|
|
|
124
150
|
if (response === undefined || !surface.answerQuestion(requestId, response)) {
|
|
125
151
|
return failure(frameId, "extension_ui_response", "invalid_response");
|
|
126
152
|
}
|
|
127
|
-
|
|
153
|
+
ledger.record(requestId, answerId);
|
|
154
|
+
return { ...success(frameId, "extension_ui_response"), disposition: "applied" };
|
|
128
155
|
}
|
|
129
156
|
function questionResponse(command, questions) {
|
|
130
157
|
// Another client's cancel (the desktop's Remove) is the user's; the builtin settles it from its own draft.
|
|
@@ -3,7 +3,8 @@ import { type HostDaemonPaths } from "../rpc/host-daemon-paths.ts";
|
|
|
3
3
|
* `<agentDir>/rpc/tui/t-<16hex>.sock`, or - when the agent directory is too deep for `sun_path` -
|
|
4
4
|
* the same name under `/tmp/senpi-rpc-<sha256(agentDir)[:8]>/tui/`. The fallback root is
|
|
5
5
|
* predictable, so it is used only when it is a real directory owned by this user with no group or
|
|
6
|
-
* other access; anything else fails the registration instead of binding into it.
|
|
6
|
+
* other access; anything else fails the registration instead of binding into it. On win32 the path is only the
|
|
7
|
+
* logical address (the listener binds a named pipe hashed from it), so it never moves (senpi#4267).
|
|
7
8
|
*/
|
|
8
9
|
export declare function resolveTuiSocket(agentDir: string, instanceId: string): Promise<string>;
|
|
9
10
|
export interface TuiRegistryEntry {
|
|
@@ -27,12 +27,13 @@ const REGISTRY_LOCK_WAIT_MS = 5_000;
|
|
|
27
27
|
* `<agentDir>/rpc/tui/t-<16hex>.sock`, or - when the agent directory is too deep for `sun_path` -
|
|
28
28
|
* the same name under `/tmp/senpi-rpc-<sha256(agentDir)[:8]>/tui/`. The fallback root is
|
|
29
29
|
* predictable, so it is used only when it is a real directory owned by this user with no group or
|
|
30
|
-
* other access; anything else fails the registration instead of binding into it.
|
|
30
|
+
* other access; anything else fails the registration instead of binding into it. On win32 the path is only the
|
|
31
|
+
* logical address (the listener binds a named pipe hashed from it), so it never moves (senpi#4267).
|
|
31
32
|
*/
|
|
32
33
|
export async function resolveTuiSocket(agentDir, instanceId) {
|
|
33
34
|
const name = tuiSocketName(instanceId);
|
|
34
35
|
const primary = join(agentDir, "rpc", "tui", name);
|
|
35
|
-
if (Buffer.byteLength(primary) <= MAX_SOCKET_PATH_BYTES) {
|
|
36
|
+
if (process.platform === "win32" || Buffer.byteLength(primary) <= MAX_SOCKET_PATH_BYTES) {
|
|
36
37
|
await makePrivateDirectory(dirname(primary));
|
|
37
38
|
return primary;
|
|
38
39
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { AgentSession } from "../../core/agent-session.ts";
|
|
2
2
|
import { type ControlAttachments } from "./session-control-attach.ts";
|
|
3
|
-
import type
|
|
4
|
-
export declare const TUI_RPC_COMMANDS: readonly ["open_session", "subscribe_sessions", "unsubscribe_sessions", "steer", "follow_up", "prompt", "get_effect_status"];
|
|
3
|
+
import { type ControlCommand, type ControlConnection } from "./session-control-server.ts";
|
|
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
|
+
/** This endpoint's instance id: the `expectedHostInstance` an `attach_session` may name. */
|
|
7
|
+
readonly instanceId: string;
|
|
6
8
|
readonly session: AgentSession;
|
|
7
9
|
readonly attachments: ControlAttachments;
|
|
8
10
|
}
|
|
@@ -3,28 +3,60 @@
|
|
|
3
3
|
* attaches by path or id, `subscribe_sessions`/`unsubscribe_sessions` run the host's status stream over
|
|
4
4
|
* this one session, `steer`/`follow_up`/`prompt` go through the session's external admission (the
|
|
5
5
|
* terminal's one delivery authority, so the user's draft still holds them) with the host's `whenIdle`
|
|
6
|
-
* meaning, and `get_effect_status` reads the same effect ledger.
|
|
6
|
+
* meaning, and `get_effect_status` reads the same effect ledger. The rest of the host's drive set answers with the
|
|
7
|
+
* host's contract (senpi#4267): `set_client_info` declares the connection's roles, `attach_session` and
|
|
8
|
+
* `release_attachment` take and drop a live attachment on the session's identity, `abort` runs the host's effect
|
|
9
|
+
* path and its role rule (a Stop needs `interactive_user`), and input sent with a `clientMessageId` is answered
|
|
10
|
+
* with an admission receipt that a retry gets again, marked `replayed`. A frame this file does not own answers
|
|
7
11
|
* `undefined` and falls through to the endpoint's other commands.
|
|
8
12
|
*/
|
|
9
13
|
import { randomUUID } from "node:crypto";
|
|
10
14
|
import { realpathSync } from "node:fs";
|
|
11
15
|
import { resolve } from "node:path";
|
|
12
|
-
import {
|
|
16
|
+
import { CONNECTION_ROLE_FIXED } from "../rpc/connection-roles.js";
|
|
17
|
+
import { INTERACTIVE_USER_CAPABILITY } from "../rpc/custom-capability.js";
|
|
18
|
+
import { runAbort, runGetEffectStatus } from "../rpc/effect-commands.js";
|
|
13
19
|
import { buildRpcSessionState } from "../rpc/rpc-session-state.js";
|
|
20
|
+
import { getSnapshot } from "../rpc/get-snapshot.js";
|
|
14
21
|
import { answerSubscribeSessions, answerUnsubscribeSessions, refusal } from "../rpc/session-status-commands.js";
|
|
15
22
|
import { keyOf } from "./session-control-attach.js";
|
|
23
|
+
import { failure, success } from "./session-control-server.js";
|
|
16
24
|
export const TUI_RPC_COMMANDS = [
|
|
17
25
|
"open_session",
|
|
26
|
+
"get_snapshot",
|
|
18
27
|
"subscribe_sessions",
|
|
19
28
|
"unsubscribe_sessions",
|
|
20
29
|
"steer",
|
|
21
30
|
"follow_up",
|
|
22
31
|
"prompt",
|
|
23
32
|
"get_effect_status",
|
|
33
|
+
"set_client_info",
|
|
34
|
+
"attach_session",
|
|
35
|
+
"release_attachment",
|
|
36
|
+
"abort",
|
|
24
37
|
];
|
|
38
|
+
/** The host's answer to a Stop from a connection that never declared the user's role (senpi#3811). */
|
|
39
|
+
const STOP_REQUIRES_INTERACTIVE_CLIENT = "stop_requires_interactive_client: declare interactive_user with set_client_info first";
|
|
25
40
|
/** `null` = answered on the writer already; `undefined` = not a command this file owns. */
|
|
26
41
|
export async function runTuiRpcCommand(context, connection, command) {
|
|
27
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
|
+
}
|
|
28
60
|
case "open_session":
|
|
29
61
|
return openSession(context, connection, command);
|
|
30
62
|
case "subscribe_sessions":
|
|
@@ -36,6 +68,14 @@ export async function runTuiRpcCommand(context, connection, command) {
|
|
|
36
68
|
return admitInput(context, command, command.type === "steer" ? "steer" : "followUp", command.whenIdle);
|
|
37
69
|
case "prompt":
|
|
38
70
|
return admitPrompt(context, command);
|
|
71
|
+
case "set_client_info":
|
|
72
|
+
return setClientInfo(context, connection, command);
|
|
73
|
+
case "attach_session":
|
|
74
|
+
return attachSession(context, connection, command);
|
|
75
|
+
case "release_attachment":
|
|
76
|
+
return releaseAttachment(context, connection, command);
|
|
77
|
+
case "abort":
|
|
78
|
+
return abort(context, connection, frame(command));
|
|
39
79
|
case "get_effect_status":
|
|
40
80
|
return runGetEffectStatus({
|
|
41
81
|
session: context.session,
|
|
@@ -74,6 +114,68 @@ function openSession(context, connection, command) {
|
|
|
74
114
|
});
|
|
75
115
|
return null;
|
|
76
116
|
}
|
|
117
|
+
/** Declares this connection's roles, fixed once declared, exactly as on a host; the other fields change nothing here. */
|
|
118
|
+
function setClientInfo(context, connection, command) {
|
|
119
|
+
const capabilities = command.capabilities;
|
|
120
|
+
if (capabilities === undefined)
|
|
121
|
+
return success(command.id, "set_client_info");
|
|
122
|
+
if (!Array.isArray(capabilities) || !capabilities.every((entry) => typeof entry === "string"))
|
|
123
|
+
return failure(command.id, "set_client_info", "capabilities must be an array of strings");
|
|
124
|
+
if (!context.attachments.roles.declare(keyOf(connection), capabilities))
|
|
125
|
+
return failure(command.id, "set_client_info", CONNECTION_ROLE_FIXED);
|
|
126
|
+
return success(command.id, "set_client_info");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A live attachment on the session's identity, as on a host: `sessionId` is this terminal's routing handle,
|
|
130
|
+
* `durableSessionId` its session id, and `expectedHostInstance`, when sent, this endpoint's instance id. The answer
|
|
131
|
+
* follows the replay of the in-flight snapshot, then the session's records reach the connection.
|
|
132
|
+
*/
|
|
133
|
+
function attachSession(context, connection, command) {
|
|
134
|
+
const { session, attachments } = context;
|
|
135
|
+
const id = idOf(command);
|
|
136
|
+
if (command.sessionId !== attachments.handle)
|
|
137
|
+
return refusal(id, "attach_session", "unknown_session", {
|
|
138
|
+
reason: "this terminal hosts one session and the request names another",
|
|
139
|
+
action: "attach with this terminal's sessionId from list_sessions",
|
|
140
|
+
});
|
|
141
|
+
const instanceMatches = command.expectedHostInstance === undefined || command.expectedHostInstance === context.instanceId;
|
|
142
|
+
if (command.durableSessionId !== session.sessionId || !instanceMatches)
|
|
143
|
+
return refusal(id, "attach_session", "session_identity_mismatch", {
|
|
144
|
+
reason: "durableSessionId or expectedHostInstance does not name this terminal's session",
|
|
145
|
+
action: "read list_sessions and get_protocol_info again, then attach with their ids",
|
|
146
|
+
});
|
|
147
|
+
attachments.run(connection, () => {
|
|
148
|
+
attachments.attach(connection);
|
|
149
|
+
attachments.writer.enqueue(attachments.handle, {
|
|
150
|
+
id: command.id,
|
|
151
|
+
type: "response",
|
|
152
|
+
command: "attach_session",
|
|
153
|
+
success: true,
|
|
154
|
+
data: { sessionId: attachments.handle, attached: true },
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function releaseAttachment(context, connection, command) {
|
|
160
|
+
if (command.sessionId !== context.attachments.handle || !context.attachments.detach(connection))
|
|
161
|
+
return refusal(idOf(command), "release_attachment", "session_not_attached", {
|
|
162
|
+
reason: "this connection is not attached to that session",
|
|
163
|
+
action: "attach first with attach_session or open_session",
|
|
164
|
+
});
|
|
165
|
+
// Nothing outlives the terminal's own attachment, so a release is never deferred.
|
|
166
|
+
return success(command.id, "release_attachment", { deferred: false });
|
|
167
|
+
}
|
|
168
|
+
/** The host's `abort`: the same effect receipts, and a Stop only from a connection that declared `interactive_user`. */
|
|
169
|
+
async function abort(context, connection, command) {
|
|
170
|
+
const key = keyOf(connection);
|
|
171
|
+
if (command.intent === "stop" && !context.attachments.roles.has(key, INTERACTIVE_USER_CAPABILITY))
|
|
172
|
+
return failure(command.id, "abort", STOP_REQUIRES_INTERACTIVE_CLIENT);
|
|
173
|
+
const { session } = context;
|
|
174
|
+
return runAbort({ session, by: { connectionId: key }, sessionId: context.attachments.handle }, command, () => (command.intent === "stop" ? session.stop() : session.abort()).catch((cause) => {
|
|
175
|
+
// Reported out of band, as on a host; the answer never waits on it.
|
|
176
|
+
connection.send({ type: "rpc_error", error: String(cause) });
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
77
179
|
function subscribe(context, connection, command) {
|
|
78
180
|
const { writer } = context.attachments;
|
|
79
181
|
const key = keyOf(connection);
|
|
@@ -145,6 +247,22 @@ function admitInput(context, command, deliverAs, whenIdle) {
|
|
|
145
247
|
});
|
|
146
248
|
const admission = context.session.externalAdmission;
|
|
147
249
|
const clientMessageId = typeof command.clientMessageId === "string" ? command.clientMessageId : undefined;
|
|
250
|
+
const fingerprint = JSON.stringify([command.type, message, deliverAs, whenIdle ?? null]);
|
|
251
|
+
const prior = clientMessageId === undefined ? undefined : context.attachments.receipts.get(clientMessageId);
|
|
252
|
+
if (prior !== undefined) {
|
|
253
|
+
if (prior.fingerprint !== fingerprint)
|
|
254
|
+
return refusal(id, command.type, "client_message_id_conflict", {
|
|
255
|
+
reason: `clientMessageId ${clientMessageId} was already used for different input`,
|
|
256
|
+
action: "send new input with a new clientMessageId",
|
|
257
|
+
});
|
|
258
|
+
return answered(id, command.type, {
|
|
259
|
+
disposition: prior.disposition,
|
|
260
|
+
clientMessageId,
|
|
261
|
+
replayed: true,
|
|
262
|
+
duplicate: true,
|
|
263
|
+
admission: prior.admission,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
148
266
|
const expected = typeof command.expected_turn_id === "number" ? command.expected_turn_id : admission.turnEpoch;
|
|
149
267
|
const result = admission.admit({
|
|
150
268
|
delivery_id: clientMessageId ?? `tui-rpc-${randomUUID()}`,
|
|
@@ -155,12 +273,23 @@ function admitInput(context, command, deliverAs, whenIdle) {
|
|
|
155
273
|
whenIdle: whenIdle === "start" ? "start" : "queue",
|
|
156
274
|
});
|
|
157
275
|
const identity = clientMessageId === undefined ? {} : { clientMessageId };
|
|
276
|
+
const admitted = (disposition) => {
|
|
277
|
+
if (clientMessageId === undefined)
|
|
278
|
+
return answered(id, command.type, { disposition });
|
|
279
|
+
const receipt = {
|
|
280
|
+
fingerprint,
|
|
281
|
+
disposition,
|
|
282
|
+
admission: { clientMessageId, durableSessionId: context.session.sessionId, disposition },
|
|
283
|
+
};
|
|
284
|
+
context.attachments.receipts.set(clientMessageId, receipt);
|
|
285
|
+
return answered(id, command.type, { disposition, clientMessageId, admission: receipt.admission });
|
|
286
|
+
};
|
|
158
287
|
switch (result.kind) {
|
|
159
288
|
case "started":
|
|
160
|
-
return
|
|
289
|
+
return admitted("started");
|
|
161
290
|
case "steered":
|
|
162
291
|
case "queued":
|
|
163
|
-
return
|
|
292
|
+
return admitted("queued");
|
|
164
293
|
case "already_admitted":
|
|
165
294
|
return answered(id, command.type, { disposition: "queued", duplicate: true, ...identity });
|
|
166
295
|
case "held_draft":
|
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
* (`authenticateSocket`, the handshake every RPC transport shares); a wrong secret is disconnected
|
|
4
4
|
* before a single JSONL line is read. After that it is the RPC framing: one LF-terminated JSON
|
|
5
5
|
* object per request, one response per request carrying its `id`, plus pushed feed events.
|
|
6
|
+
*
|
|
7
|
+
* The socket path is the endpoint's logical address on every platform (its secret lives beside it); on win32 the
|
|
8
|
+
* listener binds the named pipe the host transport derives from that path and the secret (senpi#4267), which is
|
|
9
|
+
* the address every client already connects to.
|
|
6
10
|
*/
|
|
7
11
|
import { chmod } from "node:fs/promises";
|
|
8
12
|
import { createServer } from "node:net";
|
|
9
13
|
import { attachJsonlLineReader, MAX_RPC_LINE_CHARACTERS, serializeJsonLine } from "../rpc/jsonl.js";
|
|
10
14
|
import { rpcCommandShapeError } from "../rpc/rpc-input-validation.js";
|
|
11
15
|
import { socketSink } from "../rpc/socket-sink.js";
|
|
12
|
-
import { authenticateSocket } from "../rpc/socket-transport.js";
|
|
16
|
+
import { authenticateSocket, resolveSocketTransportAddress } from "../rpc/socket-transport.js";
|
|
13
17
|
export async function listenControlSocket(socketPath, secret, handlers) {
|
|
14
18
|
const clients = new Set();
|
|
15
19
|
let nextConnection = 0;
|
|
@@ -21,13 +25,15 @@ export async function listenControlSocket(socketPath, secret, handlers) {
|
|
|
21
25
|
});
|
|
22
26
|
await new Promise((resolve, reject) => {
|
|
23
27
|
server.once("error", reject);
|
|
24
|
-
server.listen(socketPath, () => {
|
|
28
|
+
server.listen(resolveSocketTransportAddress(socketPath, process.platform, secret), () => {
|
|
25
29
|
server.off("error", reject);
|
|
26
30
|
resolve();
|
|
27
31
|
});
|
|
28
32
|
});
|
|
29
33
|
server.on("error", (error) => handlers.listenerError(error));
|
|
30
|
-
|
|
34
|
+
// A named pipe has no file to restrict: the secret, 0600 beside the logical path, is its gate.
|
|
35
|
+
if (process.platform !== "win32")
|
|
36
|
+
await chmod(socketPath, 0o600);
|
|
31
37
|
return { close: () => closeServer(server, clients) };
|
|
32
38
|
}
|
|
33
39
|
function acceptConnection(socket, id, handlers) {
|
|
@@ -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
|