@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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
export const nodeCompactStorageIo = {
|
|
3
|
+
writeFile: (path, data) => writeFile(path, data, { flag: "wx", mode: 0o600 }),
|
|
4
|
+
readFile: (path) => readFile(path),
|
|
5
|
+
unlink: (path) => unlink(path),
|
|
6
|
+
};
|
|
7
|
+
export function describeCause(cause) {
|
|
8
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
9
|
+
}
|
|
10
|
+
/** Committed storage that can no longer be read: a real error, never empty content. */
|
|
11
|
+
export class CompactStorageReadError extends Error {
|
|
12
|
+
constructor(operation, path, cause) {
|
|
13
|
+
super(`compact storage ${operation} failed for ${path}: ${describeCause(cause)}`, { cause });
|
|
14
|
+
this.name = "CompactStorageReadError";
|
|
15
|
+
this.operation = operation;
|
|
16
|
+
this.path = path;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=compact-storage-io.js.map
|
|
@@ -48,6 +48,8 @@ export interface RpcConnectionOptions {
|
|
|
48
48
|
*/
|
|
49
49
|
export interface RpcConnectionSink {
|
|
50
50
|
writeRaw(chunk: string): void;
|
|
51
|
+
/** Shared bindings accept correlated records synchronously, before JSONL buffering. */
|
|
52
|
+
writeRecord?(record: object): void;
|
|
51
53
|
waitForBackpressure(): Promise<void>;
|
|
52
54
|
/** Tears the transport down once its event queue can no longer deliver (overflow, write failure). */
|
|
53
55
|
close?(): void;
|
|
@@ -42,13 +42,13 @@ import { ClientAdmissions } from "./client-admissions.js";
|
|
|
42
42
|
import { handleClientInput } from "./client-input-handler.js";
|
|
43
43
|
import { ConnectionQuestionBridge, degradeQuestion, sessionQuestionBridges } from "./connection-question-bridge.js";
|
|
44
44
|
import { CONNECTION_ROLE_FIXED, ConnectionRoles, roleConnection } from "./connection-roles.js";
|
|
45
|
-
import { AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EVAL_CELLS_CAPABILITY, EXTENSION_EVENTS_CAPABILITY, INTERACTIVE_USER_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, QUESTION_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETRY_FALLBACK_COMMAND_CAPABILITY, SESSION_HELD_CAPABILITY, TOOL_BARRIER_CAPABILITY, TASK_PARENT_CAPABILITY, } from "./custom-capability.js";
|
|
45
|
+
import { AUTH_KEY_VERIFY_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EVAL_CELLS_CAPABILITY, EXTENSION_EVENTS_CAPABILITY, ENTRIES_TAIL_CAPABILITY, INTERACTIVE_USER_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, QUESTION_CAPABILITY, QUEUED_MESSAGE_REVISION_CAPABILITY, RESUME_SESSION_CAPABILITY, RETRY_FALLBACK_COMMAND_CAPABILITY, SESSION_HELD_CAPABILITY, TOOL_BARRIER_CAPABILITY, TASK_PARENT_CAPABILITY, } from "./custom-capability.js";
|
|
46
46
|
import { runAbort, runGetEffectStatus, runQueuedInputChange, runSetModel, runSetThinkingLevel, } from "./effect-commands.js";
|
|
47
47
|
import { EffectLedger } from "./effect-ledger.js";
|
|
48
48
|
import { EFFECT_LEDGER_CAPABILITY } from "./effect-record.js";
|
|
49
49
|
import { createRpcEventOutputBuffer } from "./event-output-buffer.js";
|
|
50
50
|
import { settleExtensionUiResponse } from "./extension-ui-response.js";
|
|
51
|
-
import {
|
|
51
|
+
import { answerGetEntries } from "./get-entries-command.js";
|
|
52
52
|
import { runHoldInput } from "./held-input-commands.js";
|
|
53
53
|
import { HostSessionControl } from "./host-session-control.js";
|
|
54
54
|
import { createRpcLoginPromptCallbacks } from "./login-prompts.js";
|
|
@@ -57,7 +57,9 @@ import { protocolIdentity } from "./protocol-identity.js";
|
|
|
57
57
|
import { buildRpcCommandsForSession, createCommandsChangedEvent, rpcCommandListDigest } from "./rpc-command-surface.js";
|
|
58
58
|
import { rpcCommandPayloadError, rpcCommandShapeError, rpcMessageLengthError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
|
|
59
59
|
import { buildRpcSessionState } from "./rpc-session-state.js";
|
|
60
|
-
import {
|
|
60
|
+
import { getSnapshot } from "./get-snapshot.js";
|
|
61
|
+
import { sessionUiAnswerLedger } from "./ui-answer-ledger.js";
|
|
62
|
+
import { RPC_ERROR_EVAL_CELL_NOT_FOUND, RPC_ERROR_EVAL_CELLS_UNAVAILABLE, RPC_ERROR_INVALID_ARGUMENT, RPC_ERROR_INVALID_EVAL_CELL_REQUEST, RPC_ERROR_MEDIA_NOT_FOUND, RPC_ERROR_MODEL_NOT_FOUND, STEER_WHEN_IDLE_CAPABILITY, USER_ACTIVITY_CAPABILITY, } from "./rpc-types.js";
|
|
61
63
|
import { runPauseForRun, runResumeForRun } from "./run-hold-commands.js";
|
|
62
64
|
import { availableModelsData, interruptRunningTurn } from "./session-control-actions-data.js";
|
|
63
65
|
import { SessionExtensionUiRequests, sessionDialogRequests } from "./session-extension-ui-requests.js";
|
|
@@ -197,7 +199,9 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
197
199
|
let loadedSurfacesDigest;
|
|
198
200
|
let rpcCommandsDigest;
|
|
199
201
|
let suppressLoadedSurfaceEvents = false;
|
|
200
|
-
const eventOutput =
|
|
202
|
+
const eventOutput = sink.writeRecord
|
|
203
|
+
? undefined
|
|
204
|
+
: createRpcEventOutputBuffer(sink.writeRaw, options.eventFlushScheduler);
|
|
201
205
|
const effectContext = () => ({
|
|
202
206
|
session,
|
|
203
207
|
by: { connectionId: options.clientInfo?.connectionId() ?? "local" },
|
|
@@ -205,10 +209,18 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
205
209
|
});
|
|
206
210
|
const tagSessionRecord = (value) => routingSessionId === undefined ? value : { ...value, sessionId: routingSessionId };
|
|
207
211
|
const output = (obj) => {
|
|
208
|
-
|
|
212
|
+
const record = tagSessionRecord(obj);
|
|
213
|
+
if (sink.writeRecord)
|
|
214
|
+
sink.writeRecord(record);
|
|
215
|
+
else
|
|
216
|
+
eventOutput?.writeImmediate(record);
|
|
209
217
|
};
|
|
210
218
|
const outputEvent = (event) => {
|
|
211
|
-
|
|
219
|
+
const record = tagSessionRecord(event);
|
|
220
|
+
if (sink.writeRecord)
|
|
221
|
+
sink.writeRecord(record);
|
|
222
|
+
else
|
|
223
|
+
eventOutput?.enqueueEvent(record);
|
|
212
224
|
};
|
|
213
225
|
const currentLoadedSurfaces = () => {
|
|
214
226
|
const extensions = loadedExtensions(session);
|
|
@@ -268,7 +280,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
268
280
|
});
|
|
269
281
|
});
|
|
270
282
|
const waitForRpcBackpressure = async () => {
|
|
271
|
-
eventOutput
|
|
283
|
+
eventOutput?.flushEvents();
|
|
272
284
|
await sink.waitForBackpressure();
|
|
273
285
|
};
|
|
274
286
|
const success = (id, command, data) => {
|
|
@@ -292,10 +304,15 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
292
304
|
};
|
|
293
305
|
};
|
|
294
306
|
// Pending extension UI requests waiting for response
|
|
295
|
-
const
|
|
307
|
+
const uiRequestChanged = (change) => {
|
|
308
|
+
session.refreshLifecycle();
|
|
309
|
+
if (change && routingSessionId !== undefined)
|
|
310
|
+
output(change);
|
|
311
|
+
};
|
|
312
|
+
const pendingExtensionRequests = new SessionExtensionUiRequests(uiRequestChanged);
|
|
296
313
|
const grantRequests = new Map();
|
|
297
314
|
sessionDialogRequests.set(session, pendingExtensionRequests);
|
|
298
|
-
const questions = new ConnectionQuestionBridge(output,
|
|
315
|
+
const questions = new ConnectionQuestionBridge(output, uiRequestChanged);
|
|
299
316
|
let shutdownRequested = false;
|
|
300
317
|
// In-flight OAuth logins, keyed by provider. login_start registers one; the
|
|
301
318
|
// login promise clears it on completion; login_cancel aborts it. The flow is
|
|
@@ -858,6 +875,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
858
875
|
capabilities: [
|
|
859
876
|
...new Set([
|
|
860
877
|
"multi_session",
|
|
878
|
+
"snapshot_cursor",
|
|
879
|
+
"ui_answer_ledger",
|
|
861
880
|
"session_lifecycle",
|
|
862
881
|
AUTO_TITLE_SESSIONS_CAPABILITY,
|
|
863
882
|
MEDIA_PLACEHOLDERS_CAPABILITY,
|
|
@@ -871,6 +890,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
871
890
|
TOOL_BARRIER_CAPABILITY,
|
|
872
891
|
USER_ACTIVITY_CAPABILITY,
|
|
873
892
|
RESUME_SESSION_CAPABILITY,
|
|
893
|
+
ENTRIES_TAIL_CAPABILITY,
|
|
874
894
|
...(options.retryFallbackCommand ? [RETRY_FALLBACK_COMMAND_CAPABILITY] : []),
|
|
875
895
|
...(options.toolRuntime ? toolRuntimeCapabilities(options.toolRuntime) : []),
|
|
876
896
|
// Client flags cannot advertise a host guard this classic binding does not run, nor a tool runtime.
|
|
@@ -1079,6 +1099,18 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1079
1099
|
refreshGrantPresentation();
|
|
1080
1100
|
return success(id, "set_client_info");
|
|
1081
1101
|
}
|
|
1102
|
+
case "get_snapshot": {
|
|
1103
|
+
const snapshot = getSnapshot(session, command, {
|
|
1104
|
+
lastAbortSource,
|
|
1105
|
+
enqueue: (markerId) => {
|
|
1106
|
+
if (routingSessionId !== undefined)
|
|
1107
|
+
output({ type: "snapshot_marker", markerId });
|
|
1108
|
+
},
|
|
1109
|
+
});
|
|
1110
|
+
return "error" in snapshot
|
|
1111
|
+
? error(id, "get_snapshot", snapshot.error, RPC_ERROR_INVALID_ARGUMENT)
|
|
1112
|
+
: success(id, "get_snapshot", snapshot.data);
|
|
1113
|
+
}
|
|
1082
1114
|
case "get_state":
|
|
1083
1115
|
return success(id, "get_state", buildRpcSessionState(session, lastAbortSource));
|
|
1084
1116
|
case "request_self_settle":
|
|
@@ -1457,43 +1489,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1457
1489
|
const messages = session.getUserMessagesForForking();
|
|
1458
1490
|
return success(id, "get_fork_messages", { messages });
|
|
1459
1491
|
}
|
|
1460
|
-
case "get_entries":
|
|
1461
|
-
|
|
1462
|
-
let entries = sessionManager.getEntries();
|
|
1463
|
-
if (command.since !== undefined) {
|
|
1464
|
-
const sinceIndex = entries.findIndex((e) => e.id === command.since);
|
|
1465
|
-
if (sinceIndex === -1) {
|
|
1466
|
-
return error(id, "get_entries", `Entry not found: ${command.since}`, RPC_ERROR_ENTRY_NOT_FOUND);
|
|
1467
|
-
}
|
|
1468
|
-
entries = entries.slice(sinceIndex + 1);
|
|
1469
|
-
}
|
|
1470
|
-
const bounds = entryPageBounds(command);
|
|
1471
|
-
if (!bounds.ok)
|
|
1472
|
-
return error(id, "get_entries", bounds.message, RPC_ERROR_INVALID_ARGUMENT);
|
|
1473
|
-
const page = pageEntries(entries, describeUsageLimitEntry, bounds);
|
|
1474
|
-
// A client walking pages asks again at once; holding the view until the last page spares each page a
|
|
1475
|
-
// full re-read of the file. The hold expires on its own, so a walk the client abandons on an idle
|
|
1476
|
-
// session does not pin the whole history; any other answer lets the view go now.
|
|
1477
|
-
if (bounds.paged && page.hasMore)
|
|
1478
|
-
holdEntryWalk(session, () => session.releaseFullHistoryView());
|
|
1479
|
-
else {
|
|
1480
|
-
endEntryWalk(session);
|
|
1481
|
-
session.releaseFullHistoryView();
|
|
1482
|
-
}
|
|
1483
|
-
const budget = currentResponseByteBudget();
|
|
1484
|
-
if (page.firstEntryBytes > budget) {
|
|
1485
|
-
return error(id, "get_entries", `Entry ${entries[0]?.id ?? ""} alone is larger than one answer can carry (${budget} bytes), so it cannot be sent over RPC.`, RPC_ERROR_RESPONSE_TOO_LARGE);
|
|
1486
|
-
}
|
|
1487
|
-
if (!bounds.paged && page.hasMore) {
|
|
1488
|
-
return error(id, "get_entries", `The session history is larger than one answer can carry (${budget} bytes). Read it in pages: pass limit or maxBytes, then since: nextSince until hasMore is false.`, RPC_ERROR_RESPONSE_TOO_LARGE);
|
|
1489
|
-
}
|
|
1490
|
-
return success(id, "get_entries", {
|
|
1491
|
-
entries: page.entries,
|
|
1492
|
-
leafId: sessionManager.getLeafId(),
|
|
1493
|
-
...(bounds.paged ? { hasMore: page.hasMore } : {}),
|
|
1494
|
-
...(page.nextSince === undefined ? {} : { nextSince: page.nextSince }),
|
|
1495
|
-
});
|
|
1496
|
-
}
|
|
1492
|
+
case "get_entries":
|
|
1493
|
+
return answerGetEntries(session, command);
|
|
1497
1494
|
case "get_tree": {
|
|
1498
1495
|
const sessionManager = session.sessionManager;
|
|
1499
1496
|
const tree = sessionManager.getTree();
|
|
@@ -1722,6 +1719,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1722
1719
|
questions,
|
|
1723
1720
|
dialogs: pendingExtensionRequests,
|
|
1724
1721
|
routed: routingSessionId !== undefined,
|
|
1722
|
+
ledger: sessionUiAnswerLedger(session),
|
|
1725
1723
|
// An answer is a person's input by its entry point, whatever the connection role.
|
|
1726
1724
|
beforeAnswer: () => session.admitHumanAnswer(),
|
|
1727
1725
|
});
|
|
@@ -1753,7 +1751,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1753
1751
|
// A single-session connection hears its account's new limit windows as they are reported. A host
|
|
1754
1752
|
// session's handler (it has a routing `sessionId`) does not: the host broadcasts the record once.
|
|
1755
1753
|
const stopLimitEvents = options.sessionId === undefined
|
|
1756
|
-
? subscriptionLimits.subscribe((limits) =>
|
|
1754
|
+
? subscriptionLimits.subscribe((limits) => outputEvent({ type: "subscription_limits", ...limits }))
|
|
1757
1755
|
: () => { };
|
|
1758
1756
|
const dispose = async (disposeOptions = {}) => {
|
|
1759
1757
|
unbindMcpForms?.();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionUIContext, QuestionRequest, QuestionResponse } from "../../core/extensions/types.ts";
|
|
2
|
+
import type { UiRequestChange } from "./snapshot-types.ts";
|
|
2
3
|
import type { RpcExtensionUIProgress, RpcExtensionUIResponse, RpcQuestionUiRequest } from "./rpc-types.ts";
|
|
3
4
|
type Options = Parameters<NonNullable<ExtensionUIContext["question"]>>[1];
|
|
4
5
|
export declare const sessionQuestionBridges: WeakMap<object, ConnectionQuestionBridge>;
|
|
@@ -7,7 +8,7 @@ export declare class ConnectionQuestionBridge {
|
|
|
7
8
|
private readonly resolved;
|
|
8
9
|
private readonly output;
|
|
9
10
|
private readonly changed;
|
|
10
|
-
constructor(output: (record: object) => void, changed?: () => void);
|
|
11
|
+
constructor(output: (record: object) => void, changed?: (change?: UiRequestChange) => void);
|
|
11
12
|
pendingQuestions(): RpcQuestionUiRequest[];
|
|
12
13
|
ask(request: QuestionRequest, opts?: Options): Promise<QuestionResponse>;
|
|
13
14
|
respond(response: RpcExtensionUIResponse, beforeAnswer?: () => void): boolean | "question_incomplete" | "question_already_resolved";
|
|
@@ -43,7 +43,11 @@ export class ConnectionQuestionBridge {
|
|
|
43
43
|
clearTimeout(timer);
|
|
44
44
|
opts?.signal?.removeEventListener("abort", cancel);
|
|
45
45
|
this.pending.delete(id);
|
|
46
|
-
this.changed?.(
|
|
46
|
+
this.changed?.({
|
|
47
|
+
type: "ui_request_settled",
|
|
48
|
+
uiRequestId: id,
|
|
49
|
+
disposition: status === "answered" || status === "comment-submitted" ? "answered" : "cancelled",
|
|
50
|
+
});
|
|
47
51
|
this.resolved.add(id);
|
|
48
52
|
const result = {
|
|
49
53
|
status,
|
|
@@ -121,7 +125,7 @@ export class ConnectionQuestionBridge {
|
|
|
121
125
|
},
|
|
122
126
|
});
|
|
123
127
|
arm();
|
|
124
|
-
this.changed?.();
|
|
128
|
+
this.changed?.({ type: "ui_request_pending", uiRequestId: id, method: "question", request: frame });
|
|
125
129
|
this.output(frame);
|
|
126
130
|
opts?.signal?.addEventListener("abort", cancel, { once: true });
|
|
127
131
|
if (opts?.signal?.aborted)
|
|
@@ -60,6 +60,12 @@ export declare const DETACH_CONNECTION_CAPABILITY = "detach_connection";
|
|
|
60
60
|
* client's own `set_client_info` and fixed at that first declaration (see `connection-roles.ts`).
|
|
61
61
|
*/
|
|
62
62
|
export declare const INTERACTIVE_USER_CAPABILITY = "interactive_user";
|
|
63
|
+
/** `get_state` and `get_entries` accept `observe: true` and then never count as an attachment (senpi#4267). */
|
|
64
|
+
export declare const OBSERVE_READS_CAPABILITY = "observe_reads";
|
|
65
|
+
/** `get_entries` accepts `before` and `tail` and answers `next_before` (senpi#4267). */
|
|
66
|
+
export declare const ENTRIES_TAIL_CAPABILITY = "entries_tail";
|
|
67
|
+
/** A terminal control endpoint serves the host's observe-and-drive command set (senpi#4267). */
|
|
68
|
+
export declare const TUI_OBSERVER_CAPABILITY = "tui_observer";
|
|
63
69
|
/** The connection owns this session as a task child; only it may pause or resume the session for its run (senpi#3811). */
|
|
64
70
|
export declare const TASK_PARENT_CAPABILITY = "task_parent";
|
|
65
71
|
/**
|
|
@@ -60,6 +60,12 @@ export const DETACH_CONNECTION_CAPABILITY = "detach_connection";
|
|
|
60
60
|
* client's own `set_client_info` and fixed at that first declaration (see `connection-roles.ts`).
|
|
61
61
|
*/
|
|
62
62
|
export const INTERACTIVE_USER_CAPABILITY = "interactive_user";
|
|
63
|
+
/** `get_state` and `get_entries` accept `observe: true` and then never count as an attachment (senpi#4267). */
|
|
64
|
+
export const OBSERVE_READS_CAPABILITY = "observe_reads";
|
|
65
|
+
/** `get_entries` accepts `before` and `tail` and answers `next_before` (senpi#4267). */
|
|
66
|
+
export const ENTRIES_TAIL_CAPABILITY = "entries_tail";
|
|
67
|
+
/** A terminal control endpoint serves the host's observe-and-drive command set (senpi#4267). */
|
|
68
|
+
export const TUI_OBSERVER_CAPABILITY = "tui_observer";
|
|
63
69
|
/** The connection owns this session as a task child; only it may pause or resume the session for its run (senpi#3811). */
|
|
64
70
|
export const TASK_PARENT_CAPABILITY = "task_parent";
|
|
65
71
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { QuestionRequest, QuestionResponse } from "../../core/extensions/types.ts";
|
|
2
2
|
import type { RpcExtensionUIResponse } from "./rpc-types.ts";
|
|
3
|
+
import type { UiAnswerLedger } from "./ui-answer-ledger.ts";
|
|
3
4
|
/**
|
|
4
5
|
* An `extension_ui_response` carries two ids: `id`, the frame's own correlation id that its reply
|
|
5
6
|
* echoes, and `uiRequestId`, the extension UI request it answers. The short form every older client
|
|
@@ -23,6 +24,8 @@ export type ExtensionUiResponseReply = {
|
|
|
23
24
|
readonly id: string;
|
|
24
25
|
readonly type: "response";
|
|
25
26
|
readonly command: "extension_ui_response";
|
|
27
|
+
/** The answer's outcome for its request; absent when a malformed answer was refused before settling anything. */
|
|
28
|
+
readonly disposition?: "applied" | "replay" | "already_answered" | "unknown";
|
|
26
29
|
} & ({
|
|
27
30
|
readonly success: true;
|
|
28
31
|
} | {
|
|
@@ -37,13 +40,15 @@ type DialogAnswers = {
|
|
|
37
40
|
};
|
|
38
41
|
/**
|
|
39
42
|
* Settles one response on a host connection and returns its reply, keyed by the frame `id`.
|
|
40
|
-
* `undefined` only on an unrouted (single-session stdio) connection for
|
|
41
|
-
* matches: that connection has always ignored such a response, and still does.
|
|
43
|
+
* `undefined` only on an unrouted (single-session stdio) connection for an unnamed response no
|
|
44
|
+
* request here matches: that connection has always ignored such a response, and still does. A named
|
|
45
|
+
* response (`answerId`) asks for a receipt, so every connection answers it.
|
|
42
46
|
*/
|
|
43
47
|
export declare function settleExtensionUiResponse(response: RpcExtensionUIResponse, targets: {
|
|
44
48
|
readonly questions: QuestionAnswers;
|
|
45
49
|
readonly dialogs: DialogAnswers;
|
|
46
50
|
readonly routed: boolean;
|
|
51
|
+
readonly ledger?: UiAnswerLedger;
|
|
47
52
|
readonly beforeAnswer?: () => void;
|
|
48
53
|
}): ExtensionUiResponseReply | undefined;
|
|
49
54
|
export {};
|
|
@@ -28,25 +28,45 @@ export function unansweredQuestionIds(questions, answers) {
|
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Settles one response on a host connection and returns its reply, keyed by the frame `id`.
|
|
31
|
-
* `undefined` only on an unrouted (single-session stdio) connection for
|
|
32
|
-
* matches: that connection has always ignored such a response, and still does.
|
|
31
|
+
* `undefined` only on an unrouted (single-session stdio) connection for an unnamed response no
|
|
32
|
+
* request here matches: that connection has always ignored such a response, and still does. A named
|
|
33
|
+
* response (`answerId`) asks for a receipt, so every connection answers it.
|
|
33
34
|
*/
|
|
34
35
|
export function settleExtensionUiResponse(response, targets) {
|
|
36
|
+
if (response.answerId !== undefined &&
|
|
37
|
+
(typeof response.answerId !== "string" || response.answerId.length < 1 || response.answerId.length > 256))
|
|
38
|
+
return refused(response.id, "invalid_answer_id");
|
|
35
39
|
const requestId = answeredUiRequestId(response) ?? response.id;
|
|
40
|
+
const receipt = response.answerId === undefined ? undefined : targets.ledger?.lookup(requestId, response.answerId);
|
|
41
|
+
if (receipt === "replay")
|
|
42
|
+
return { ...reply(response.id), success: true, disposition: "replay" };
|
|
43
|
+
if (receipt === "already_answered")
|
|
44
|
+
return { ...reply(response.id), success: false, error: "already_answered", disposition: receipt };
|
|
36
45
|
const answer = requestId === response.id ? response : { ...response, id: requestId };
|
|
37
46
|
const question = targets.questions.respond(answer, targets.beforeAnswer);
|
|
38
47
|
if (typeof question === "string")
|
|
39
|
-
return refused(response.id, question
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
return refused(response.id, response.answerId !== undefined && question === "question_already_resolved"
|
|
49
|
+
? "unknown_extension_ui_request"
|
|
50
|
+
: question);
|
|
51
|
+
if (question || targets.dialogs.resolve(answer, targets.beforeAnswer)) {
|
|
52
|
+
targets.ledger?.record(requestId, response.answerId);
|
|
53
|
+
return { ...reply(response.id), success: true, disposition: "applied" };
|
|
54
|
+
}
|
|
42
55
|
// A routed binding owns exactly one session's request map: an unmatched response is a protocol
|
|
43
56
|
// error for its sender, never a cross-session match.
|
|
44
|
-
return targets.routed
|
|
57
|
+
return targets.routed || response.answerId !== undefined
|
|
58
|
+
? refused(response.id, "unknown_extension_ui_request")
|
|
59
|
+
: undefined;
|
|
45
60
|
}
|
|
46
61
|
function reply(id) {
|
|
47
62
|
return { id, type: "response", command: "extension_ui_response" };
|
|
48
63
|
}
|
|
49
64
|
function refused(id, error) {
|
|
50
|
-
|
|
65
|
+
const disposition = error === "question_already_resolved"
|
|
66
|
+
? "already_answered"
|
|
67
|
+
: error === "unknown_extension_ui_request"
|
|
68
|
+
? "unknown"
|
|
69
|
+
: undefined;
|
|
70
|
+
return { ...reply(id), success: false, error, ...(disposition === undefined ? {} : { disposition }) };
|
|
51
71
|
}
|
|
52
72
|
//# sourceMappingURL=extension-ui-response.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one `get_entries` answer: a host connection and a terminal control endpoint both read history through it, so
|
|
3
|
+
* the range rules (`since`, `before`), the forward (`limit`) and backward (`tail`, senpi#4267) pages and the byte
|
|
4
|
+
* budget (senpi#3534) are the same on every surface.
|
|
5
|
+
*/
|
|
6
|
+
import type { AgentSession } from "../../core/agent-session.ts";
|
|
7
|
+
import { type RpcCommand, type RpcResponse } from "./rpc-types.ts";
|
|
8
|
+
type GetEntries = Extract<RpcCommand, {
|
|
9
|
+
type: "get_entries";
|
|
10
|
+
}>;
|
|
11
|
+
export declare function answerGetEntries(session: AgentSession, command: GetEntries): RpcResponse;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=get-entries-command.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describeUsageLimitEntry } from "../../core/retry-fallback/usage-limit.js";
|
|
2
|
+
import { currentResponseByteBudget, endEntryWalk, entryPageBounds, holdEntryWalk, pageEntries, pageEntriesFromEnd, } from "./get-entries-page.js";
|
|
3
|
+
import { RPC_ERROR_ENTRY_NOT_FOUND, RPC_ERROR_INVALID_ARGUMENT, RPC_ERROR_RESPONSE_TOO_LARGE, } from "./rpc-types.js";
|
|
4
|
+
function refuse(id, message, errorCode) {
|
|
5
|
+
return { id, type: "response", command: "get_entries", success: false, error: message, errorCode };
|
|
6
|
+
}
|
|
7
|
+
export function answerGetEntries(session, command) {
|
|
8
|
+
const { id } = command;
|
|
9
|
+
const sessionManager = session.sessionManager;
|
|
10
|
+
let entries = sessionManager.getEntries();
|
|
11
|
+
if (command.before !== undefined) {
|
|
12
|
+
const beforeIndex = entries.findIndex((e) => e.id === command.before);
|
|
13
|
+
if (beforeIndex === -1)
|
|
14
|
+
return refuse(id, `Entry not found: ${command.before}`, RPC_ERROR_ENTRY_NOT_FOUND);
|
|
15
|
+
entries = entries.slice(0, beforeIndex);
|
|
16
|
+
}
|
|
17
|
+
if (command.since !== undefined) {
|
|
18
|
+
const sinceIndex = sessionManager.getEntries().findIndex((e) => e.id === command.since);
|
|
19
|
+
if (sinceIndex === -1)
|
|
20
|
+
return refuse(id, `Entry not found: ${command.since}`, RPC_ERROR_ENTRY_NOT_FOUND);
|
|
21
|
+
entries = entries.slice(sinceIndex + 1);
|
|
22
|
+
}
|
|
23
|
+
const bounds = entryPageBounds(command);
|
|
24
|
+
if (!bounds.ok)
|
|
25
|
+
return refuse(id, bounds.message, RPC_ERROR_INVALID_ARGUMENT);
|
|
26
|
+
const backward = bounds.tail !== undefined;
|
|
27
|
+
const page = backward
|
|
28
|
+
? pageEntriesFromEnd(entries, describeUsageLimitEntry, bounds)
|
|
29
|
+
: pageEntries(entries, describeUsageLimitEntry, bounds);
|
|
30
|
+
// A client walking pages asks again at once; holding the view until the last page spares each page a
|
|
31
|
+
// full re-read of the file. The hold expires on its own, so a walk the client abandons on an idle
|
|
32
|
+
// session does not pin the whole history; any other answer lets the view go now.
|
|
33
|
+
if (bounds.paged && page.hasMore)
|
|
34
|
+
holdEntryWalk(session, () => session.releaseFullHistoryView());
|
|
35
|
+
else {
|
|
36
|
+
endEntryWalk(session);
|
|
37
|
+
session.releaseFullHistoryView();
|
|
38
|
+
}
|
|
39
|
+
const budget = currentResponseByteBudget();
|
|
40
|
+
if (page.firstEntryBytes > budget) {
|
|
41
|
+
const entry = backward ? entries.at(-1) : entries[0];
|
|
42
|
+
return refuse(id, `Entry ${entry?.id ?? ""} alone is larger than one answer can carry (${budget} bytes), so it cannot be sent over RPC.`, RPC_ERROR_RESPONSE_TOO_LARGE);
|
|
43
|
+
}
|
|
44
|
+
if (!bounds.paged && page.hasMore) {
|
|
45
|
+
return refuse(id, `The session history is larger than one answer can carry (${budget} bytes). Read it in pages: pass limit or maxBytes, then since: nextSince until hasMore is false.`, RPC_ERROR_RESPONSE_TOO_LARGE);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
id,
|
|
49
|
+
type: "response",
|
|
50
|
+
command: "get_entries",
|
|
51
|
+
success: true,
|
|
52
|
+
data: {
|
|
53
|
+
entries: page.entries,
|
|
54
|
+
leafId: sessionManager.getLeafId(),
|
|
55
|
+
...(bounds.paged ? { hasMore: page.hasMore } : {}),
|
|
56
|
+
...("nextSince" in page && page.nextSince !== undefined ? { nextSince: page.nextSince } : {}),
|
|
57
|
+
...("nextBefore" in page && page.nextBefore !== undefined ? { next_before: page.nextBefore } : {}),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=get-entries-command.js.map
|
|
@@ -14,6 +14,8 @@ export declare function currentResponseByteBudget(): number;
|
|
|
14
14
|
export declare function setResponseByteBudgetForTesting(bytes: number): () => void;
|
|
15
15
|
export interface EntryPageRequest {
|
|
16
16
|
readonly limit?: number;
|
|
17
|
+
/** The newest `tail` entries of the range, read backwards (senpi#4267); never combined with `limit`. */
|
|
18
|
+
readonly tail?: number;
|
|
17
19
|
readonly maxBytes?: number;
|
|
18
20
|
}
|
|
19
21
|
export interface EntryPage<T> {
|
|
@@ -27,6 +29,7 @@ export type EntryPageBounds = {
|
|
|
27
29
|
readonly ok: true;
|
|
28
30
|
readonly paged: boolean;
|
|
29
31
|
readonly limit?: number;
|
|
32
|
+
readonly tail?: number;
|
|
30
33
|
readonly maxBytes: number;
|
|
31
34
|
} | {
|
|
32
35
|
readonly ok: false;
|
|
@@ -44,6 +47,25 @@ export declare function pageEntries<S extends {
|
|
|
44
47
|
readonly limit?: number;
|
|
45
48
|
readonly maxBytes: number;
|
|
46
49
|
}): EntryPage<T>;
|
|
50
|
+
export interface TailEntryPage<T> {
|
|
51
|
+
/** In append order: the oldest of the page first. */
|
|
52
|
+
readonly entries: T[];
|
|
53
|
+
readonly hasMore: boolean;
|
|
54
|
+
/** The oldest entry id the page returned, while older entries remain: pass it as the next `before`. */
|
|
55
|
+
readonly nextBefore?: string;
|
|
56
|
+
/** Serialized size of the page's newest entry (0 for an empty page). */
|
|
57
|
+
readonly firstEntryBytes: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The newest entries first, until the count or the byte size would pass the bounds, answered in append order. The
|
|
61
|
+
* newest entry is always taken, so a backward walk always moves even when one entry alone is larger than `maxBytes`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function pageEntriesFromEnd<S extends {
|
|
64
|
+
readonly id: string;
|
|
65
|
+
}, T>(entries: readonly S[], project: (entry: S) => T, bounds: {
|
|
66
|
+
readonly tail?: number;
|
|
67
|
+
readonly maxBytes: number;
|
|
68
|
+
}): TailEntryPage<T>;
|
|
47
69
|
/**
|
|
48
70
|
* Keeps a page walk's full-history view for the next page, and lets it go after `ENTRY_WALK_HOLD_MS` without
|
|
49
71
|
* one: a client that stops mid-walk must not pin the whole history of a session that is already idle.
|
|
@@ -22,17 +22,22 @@ export function setResponseByteBudgetForTesting(bytes) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
export function entryPageBounds(request) {
|
|
25
|
-
const { limit, maxBytes } = request;
|
|
25
|
+
const { limit, tail, maxBytes } = request;
|
|
26
26
|
if (limit !== undefined && !(Number.isInteger(limit) && limit > 0))
|
|
27
27
|
return { ok: false, message: "limit must be a positive integer" };
|
|
28
|
+
if (tail !== undefined && !(Number.isInteger(tail) && tail > 0))
|
|
29
|
+
return { ok: false, message: "tail must be a positive integer" };
|
|
30
|
+
if (limit !== undefined && tail !== undefined)
|
|
31
|
+
return { ok: false, message: "limit and tail cannot be combined: limit pages forward, tail backward" };
|
|
28
32
|
if (maxBytes !== undefined && !(Number.isInteger(maxBytes) && maxBytes > 0))
|
|
29
33
|
return { ok: false, message: "maxBytes must be a positive integer" };
|
|
30
|
-
if (limit === undefined && maxBytes === undefined)
|
|
34
|
+
if (limit === undefined && tail === undefined && maxBytes === undefined)
|
|
31
35
|
return { ok: true, paged: false, maxBytes: responseByteBudget };
|
|
32
36
|
return {
|
|
33
37
|
ok: true,
|
|
34
38
|
paged: true,
|
|
35
39
|
...(limit === undefined ? {} : { limit }),
|
|
40
|
+
...(tail === undefined ? {} : { tail }),
|
|
36
41
|
maxBytes: Math.min(maxBytes ?? DEFAULT_ENTRY_PAGE_BYTES, responseByteBudget),
|
|
37
42
|
};
|
|
38
43
|
}
|
|
@@ -68,6 +73,40 @@ export function pageEntries(entries, project, bounds) {
|
|
|
68
73
|
firstEntryBytes,
|
|
69
74
|
};
|
|
70
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* The newest entries first, until the count or the byte size would pass the bounds, answered in append order. The
|
|
78
|
+
* newest entry is always taken, so a backward walk always moves even when one entry alone is larger than `maxBytes`.
|
|
79
|
+
*/
|
|
80
|
+
export function pageEntriesFromEnd(entries, project, bounds) {
|
|
81
|
+
const page = [];
|
|
82
|
+
let bytes = 0;
|
|
83
|
+
let firstEntryBytes = 0;
|
|
84
|
+
let index = entries.length - 1;
|
|
85
|
+
for (; index >= 0; index--) {
|
|
86
|
+
if (bounds.tail !== undefined && page.length >= bounds.tail)
|
|
87
|
+
break;
|
|
88
|
+
const entry = entries[index];
|
|
89
|
+
if (entry === undefined)
|
|
90
|
+
break;
|
|
91
|
+
const projected = project(entry);
|
|
92
|
+
const size = Buffer.byteLength(JSON.stringify(projected)) + 1;
|
|
93
|
+
if (page.length > 0 && bytes + size > bounds.maxBytes)
|
|
94
|
+
break;
|
|
95
|
+
if (page.length === 0)
|
|
96
|
+
firstEntryBytes = size;
|
|
97
|
+
page.push(projected);
|
|
98
|
+
bytes += size;
|
|
99
|
+
}
|
|
100
|
+
page.reverse();
|
|
101
|
+
const oldest = entries[index + 1];
|
|
102
|
+
const hasMore = index >= 0;
|
|
103
|
+
return {
|
|
104
|
+
entries: page,
|
|
105
|
+
hasMore,
|
|
106
|
+
...(hasMore && oldest !== undefined ? { nextBefore: oldest.id } : {}),
|
|
107
|
+
firstEntryBytes,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
71
110
|
const heldWalks = new WeakMap();
|
|
72
111
|
/**
|
|
73
112
|
* Keeps a page walk's full-history view for the next page, and lets it go after `ENTRY_WALK_HOLD_MS` without
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AgentAbortSource } from "../../core/agent-abort-provenance.ts";
|
|
2
|
+
import type { AgentSession } from "../../core/agent-session.ts";
|
|
3
|
+
import type { RpcSnapshot } from "./snapshot-types.ts";
|
|
4
|
+
type SnapshotRequest = {
|
|
5
|
+
readonly tail?: unknown;
|
|
6
|
+
readonly maxBytes?: unknown;
|
|
7
|
+
};
|
|
8
|
+
/** Capture and mark on the source loop, before any response-writing await. */
|
|
9
|
+
export declare function getSnapshot(session: AgentSession, request: SnapshotRequest, cursor: {
|
|
10
|
+
readonly enqueue: (markerId: string) => void;
|
|
11
|
+
readonly lastAbortSource?: AgentAbortSource;
|
|
12
|
+
}): {
|
|
13
|
+
readonly data: RpcSnapshot;
|
|
14
|
+
} | {
|
|
15
|
+
readonly error: string;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=get-snapshot.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { describeUsageLimitEntry } from "../../core/retry-fallback/usage-limit.js";
|
|
3
|
+
import { currentResponseByteBudget, DEFAULT_ENTRY_PAGE_BYTES } from "./get-entries-page.js";
|
|
4
|
+
import { buildRpcSessionState } from "./rpc-session-state.js";
|
|
5
|
+
/** Capture and mark on the source loop, before any response-writing await. */
|
|
6
|
+
export function getSnapshot(session, request, cursor) {
|
|
7
|
+
const tail = request.tail === undefined ? 100 : request.tail;
|
|
8
|
+
const maxBytes = request.maxBytes === undefined ? DEFAULT_ENTRY_PAGE_BYTES : request.maxBytes;
|
|
9
|
+
if (typeof tail !== "number" || !Number.isSafeInteger(tail) || tail < 0)
|
|
10
|
+
return { error: "tail must be a non-negative integer" };
|
|
11
|
+
if (typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
12
|
+
return { error: "maxBytes must be a positive integer" };
|
|
13
|
+
const state = buildRpcSessionState(session, cursor.lastAbortSource);
|
|
14
|
+
const history = session.sessionManager.getEntries();
|
|
15
|
+
const entries = [];
|
|
16
|
+
let bytes = 2;
|
|
17
|
+
for (let i = history.length - 1; i >= 0 && entries.length < tail; i--) {
|
|
18
|
+
const entry = history[i];
|
|
19
|
+
if (entry === undefined)
|
|
20
|
+
break;
|
|
21
|
+
const projected = describeUsageLimitEntry(entry);
|
|
22
|
+
const size = Buffer.byteLength(JSON.stringify(projected)) + (entries.length > 0 ? 1 : 0);
|
|
23
|
+
if (bytes + size > Math.min(maxBytes, currentResponseByteBudget()))
|
|
24
|
+
break;
|
|
25
|
+
entries.push(projected);
|
|
26
|
+
bytes += size;
|
|
27
|
+
}
|
|
28
|
+
entries.reverse();
|
|
29
|
+
const history_truncated = entries.length < history.length;
|
|
30
|
+
const markerId = randomUUID();
|
|
31
|
+
// Some state fields and entry payloads are live references: detach them before yielding, too.
|
|
32
|
+
const data = structuredClone({
|
|
33
|
+
state,
|
|
34
|
+
entries,
|
|
35
|
+
history_truncated,
|
|
36
|
+
next_before: history_truncated ? (entries[0]?.id ?? null) : null,
|
|
37
|
+
markerId,
|
|
38
|
+
});
|
|
39
|
+
session.releaseFullHistoryView();
|
|
40
|
+
cursor.enqueue(markerId);
|
|
41
|
+
return { data };
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=get-snapshot.js.map
|