@code-yeongyu/senpi 2026.11.6-2 → 2026.11.9
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 +52 -0
- package/dist/bundle/chunks/{bun-extension-importer-RCGPQTI2.js → bun-extension-importer-GQ6IKEXD.js} +2 -2
- package/dist/bundle/chunks/{chunk-GSKCWXXG.js → chunk-3CRPNPTR.js} +1 -1
- package/dist/bundle/chunks/{chunk-RQ6CHXQ2.js → chunk-3TZQXZKH.js} +1 -1
- package/dist/bundle/chunks/{chunk-QN2BHP3M.js → chunk-44BJPS5I.js} +1 -1
- package/dist/bundle/chunks/{chunk-WGRKM3N6.js → chunk-45MVD7BU.js} +1 -1
- package/dist/bundle/chunks/chunk-4JIBEZZE.js +106 -0
- package/dist/bundle/chunks/{chunk-CF5OGBC4.js → chunk-4NYNMAYF.js} +1 -1
- package/dist/bundle/chunks/{chunk-X5ZVYNOL.js → chunk-4UR6UFBH.js} +1 -1
- package/dist/bundle/chunks/{chunk-LNPAT22L.js → chunk-527Q2UYM.js} +9 -8
- package/dist/bundle/chunks/{chunk-HLM46XEB.js → chunk-622VYTRI.js} +1 -1
- package/dist/bundle/chunks/{chunk-GKAYPJ4E.js → chunk-7PROV6BB.js} +1 -1
- package/dist/bundle/chunks/{chunk-4E7GDG4H.js → chunk-7VMEE62L.js} +1 -1
- package/dist/bundle/chunks/{chunk-ECW7VBMB.js → chunk-HXNXISJD.js} +1 -1
- package/dist/bundle/chunks/{chunk-C7OKUC7L.js → chunk-K3UHGDUQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-47PPZKPO.js → chunk-LM6DOWQH.js} +4 -4
- package/dist/bundle/chunks/chunk-LPYBKMPT.js +2 -0
- package/dist/bundle/chunks/{chunk-2FDW2LQL.js → chunk-M7GF73FO.js} +2 -2
- package/dist/bundle/chunks/{chunk-D5MRJWUK.js → chunk-OCMGRBJ5.js} +221 -229
- package/dist/bundle/chunks/{chunk-TIPKAMSS.js → chunk-SGAFIMAU.js} +1 -1
- package/dist/bundle/chunks/{chunk-7PSBD5KS.js → chunk-STGOYIXC.js} +1 -1
- package/dist/bundle/chunks/{chunk-DSWDJ7HT.js → chunk-WQ4RBPMS.js} +1 -1
- package/dist/bundle/chunks/{chunk-7III44PV.js → chunk-WX2HB4DD.js} +1 -1
- package/dist/bundle/chunks/chunk-XZDCMDUH.js +2 -0
- package/dist/bundle/chunks/{chunk-GNSMNSL7.js → chunk-Y5L4UFMI.js} +2 -2
- package/dist/bundle/chunks/{cli-main-T74YJGIF.js → cli-main-E4NDWAUD.js} +1 -1
- package/dist/bundle/chunks/{dispatch-BAK2YZKD.js → dispatch-VDIYSXQN.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-AYRUGE2A.js → hand-entry-5X5Z5Y3D.js} +1 -1
- package/dist/bundle/chunks/{headless-host-ES74ZB6M.js → headless-host-YXUGONR3.js} +1 -1
- package/dist/bundle/chunks/{host-command-YRXZE4TV.js → host-command-VHX2ILLM.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-N7IN7ZDV.js → host-session-command-P3MRW63Z.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-XUSBLLWI.js +2 -0
- package/dist/bundle/chunks/main-3TWVKTJN.js +2 -0
- package/dist/bundle/chunks/{multi-session-host-XSUJDHFZ.js → multi-session-host-HRGM66PY.js} +20 -17
- package/dist/bundle/chunks/{package-manager-cli-GLUNPQJU.js → package-manager-cli-WSH7U4YE.js} +1 -1
- package/dist/bundle/chunks/{role-RRFFHZF5.js → role-4BUU46VW.js} +3 -3
- package/dist/bundle/chunks/{rotation-stream-BN2YK2MC.js → rotation-stream-TODBIHXJ.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-OAPIV46G.js +2 -0
- package/dist/bundle/chunks/{run-attached-tui-W236SEOR.js → run-attached-tui-MXCC5IZR.js} +1 -1
- package/dist/bundle/chunks/{runtime-session-OBXYK4PI.js → runtime-session-VO3X4OE3.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-B2EJTBOE.js → schedule-command-MX56D6UE.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-YIMLHGLG.js → session-control-endpoint-IL6ERD4U.js} +1 -1
- package/dist/bundle/chunks/{session-picker-E6ZONUJB.js → session-picker-6VRC25MJ.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +114 -110
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session-runtime.d.ts +2 -0
- package/dist/core/agent-session-runtime.js +9 -2
- package/dist/core/agent-session-services.d.ts +2 -0
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session.d.ts +32 -3
- package/dist/core/agent-session.js +142 -29
- package/dist/core/credential-pool/rotation-slots.d.ts +3 -3
- package/dist/core/credential-pool/state-store.d.ts +1 -1
- package/dist/core/egress/classify.d.ts +8 -3
- package/dist/core/egress/classify.js +177 -158
- package/dist/core/egress/console-redactor.d.ts +7 -0
- package/dist/core/egress/console-redactor.js +74 -0
- package/dist/core/egress/credentials.d.ts +73 -0
- package/dist/core/egress/credentials.js +20 -0
- package/dist/core/egress/device-custody.d.ts +1 -1
- package/dist/core/egress/device-custody.js +18 -15
- package/dist/core/egress/device-proxy-call.d.ts +59 -0
- package/dist/core/egress/device-proxy-call.js +148 -0
- package/dist/core/egress/device-proxy-lost-calls.d.ts +27 -0
- package/dist/core/egress/device-proxy-lost-calls.js +28 -0
- package/dist/core/egress/device-proxy-ports.d.ts +49 -0
- package/dist/core/egress/device-proxy-ports.js +2 -0
- package/dist/core/egress/device-proxy-request.d.ts +34 -0
- package/dist/core/egress/device-proxy-request.js +69 -0
- package/dist/core/egress/device-proxy-waits.d.ts +40 -0
- package/dist/core/egress/device-proxy-waits.js +67 -0
- package/dist/core/egress/device-proxy.d.ts +3 -0
- package/dist/core/egress/device-proxy.js +214 -0
- package/dist/core/egress/endpoint.js +2 -1
- package/dist/core/egress/entry-verify.d.ts +13 -0
- package/dist/core/egress/entry-verify.js +33 -0
- package/dist/core/egress/hpke.d.ts +63 -0
- package/dist/core/egress/hpke.js +192 -0
- package/dist/core/egress/key-policy-template.d.ts +7 -1
- package/dist/core/egress/ledger-schema.d.ts +80 -0
- package/dist/core/egress/ledger-schema.js +117 -0
- package/dist/core/egress/ledger.d.ts +55 -0
- package/dist/core/egress/ledger.js +159 -0
- package/dist/core/egress/pinned-infra.d.ts +23 -2
- package/dist/core/egress/pinned-infra.js +9 -2
- package/dist/core/egress/placement-claim.d.ts +69 -0
- package/dist/core/egress/placement-claim.js +115 -0
- package/dist/core/egress/pool-state.d.ts +18 -0
- package/dist/core/egress/pool-state.js +5 -0
- package/dist/core/egress/redact.d.ts +10 -0
- package/dist/core/egress/redact.js +134 -0
- package/dist/core/egress/sealed-format.d.ts +101 -0
- package/dist/core/egress/sealed-format.js +174 -0
- package/dist/core/egress/sealed-source.d.ts +35 -0
- package/dist/core/egress/sealed-source.js +140 -0
- package/dist/core/egress/verified-keys.js +1 -1
- package/dist/core/egress/wire.d.ts +228 -0
- package/dist/core/egress/wire.js +49 -0
- package/dist/core/extensions/builtin/ask-user/escape-residue.d.ts +50 -0
- package/dist/core/extensions/builtin/ask-user/escape-residue.js +113 -0
- package/dist/core/extensions/builtin/ask-user/extension.js +7 -1
- package/dist/core/extensions/builtin/ask-user/resume.js +2 -1
- package/dist/core/extensions/builtin/ask-user/tool.d.ts +3 -0
- package/dist/core/extensions/builtin/ask-user/tool.js +8 -0
- package/dist/core/extensions/builtin/goal/index.js +46 -0
- package/dist/core/extensions/builtin/goal/stop-pause.d.ts +1 -0
- package/dist/core/extensions/builtin/goal/stop-pause.js +4 -2
- package/dist/core/extensions/builtin/monitor-state-event.d.ts +26 -0
- package/dist/core/extensions/builtin/monitor-state-event.js +2 -0
- package/dist/core/extensions/builtin/permission-system/index.js +10 -4
- package/dist/core/extensions/builtin/permission-system/preset-registry.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/preset-registry.js +11 -0
- package/dist/core/extensions/builtin/permission-system/settings.d.ts +2 -0
- package/dist/core/extensions/builtin/permission-system/settings.js +10 -1
- package/dist/core/extensions/builtin/terminal/background-age.d.ts +25 -0
- package/dist/core/extensions/builtin/terminal/background-age.js +91 -0
- package/dist/core/extensions/builtin/terminal/extension-state.d.ts +1 -1
- package/dist/core/extensions/builtin/terminal/extension-state.js +21 -3
- package/dist/core/extensions/builtin/terminal/notify.d.ts +6 -0
- package/dist/core/extensions/builtin/terminal/notify.js +39 -5
- package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +9 -2
- package/dist/core/extensions/builtin/terminal/session-bundle.js +36 -1
- package/dist/core/extensions/builtin/terminal/settings.d.ts +2 -0
- package/dist/core/extensions/builtin/terminal/settings.js +8 -0
- package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.d.ts +15 -0
- package/dist/core/extensions/builtin/terminal/tools/background-auto-reap.js +87 -0
- package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +3 -1
- package/dist/core/extensions/builtin/terminal/tools/bash.js +44 -15
- package/dist/core/extensions/builtin/terminal/tools/child-placement.d.ts +8 -0
- package/dist/core/extensions/builtin/terminal/tools/child-placement.js +43 -6
- package/dist/core/extensions/builtin/terminal/tools/context.d.ts +14 -2
- package/dist/core/extensions/bun-extension-importer.js +4 -3
- package/dist/core/extensions/bun-extension-load-turns.d.ts +4 -0
- package/dist/core/extensions/bun-extension-load-turns.js +45 -0
- package/dist/core/extensions/bun-extension-registry.d.ts +1 -0
- package/dist/core/extensions/bun-extension-registry.js +19 -1
- package/dist/core/extensions/loader.js +5 -0
- package/dist/core/extensions/session-control-types.d.ts +15 -0
- package/dist/core/extensions/session-control-types.js +11 -0
- package/dist/core/extensions/session-dialog-api.d.ts +5 -0
- package/dist/core/extensions/session-dialog-api.js +13 -0
- package/dist/core/extensions/types.d.ts +14 -1
- package/dist/core/external-admission.d.ts +16 -2
- package/dist/core/external-admission.js +22 -4
- package/dist/core/permission-decision.d.ts +22 -0
- package/dist/core/permission-decision.js +53 -0
- package/dist/core/permission-mode.d.ts +56 -0
- package/dist/core/permission-mode.js +133 -0
- package/dist/core/retry-fallback/controller-types.d.ts +6 -0
- package/dist/core/retry-fallback/controller.d.ts +9 -0
- package/dist/core/retry-fallback/controller.js +34 -3
- package/dist/core/retry-fallback/unusable.d.ts +25 -1
- package/dist/core/retry-fallback/unusable.js +87 -2
- package/dist/core/retry-fallback/usage-limit.d.ts +6 -1
- package/dist/core/retry-fallback/usage-limit.js +12 -4
- package/dist/core/sdk.d.ts +6 -1
- package/dist/core/sdk.js +3 -0
- package/dist/core/session-log.js +1 -1
- package/dist/core/stall-warning.d.ts +41 -0
- package/dist/core/stall-warning.js +72 -0
- package/dist/core/terminal-settings.d.ts +1 -0
- package/dist/core/turn-origin.d.ts +31 -0
- package/dist/core/turn-origin.js +64 -0
- package/dist/modes/interactive/components/remote-delivery-message.d.ts +2 -1
- package/dist/modes/interactive/components/remote-delivery-message.js +13 -5
- package/dist/modes/rpc/admit-external-message-command.d.ts +37 -0
- package/dist/modes/rpc/admit-external-message-command.js +61 -0
- package/dist/modes/rpc/admit-external-message-wire.d.ts +42 -0
- package/dist/modes/rpc/admit-external-message-wire.js +55 -0
- package/dist/modes/rpc/client-input-handler.d.ts +2 -0
- package/dist/modes/rpc/client-input-handler.js +2 -0
- package/dist/modes/rpc/connection-handler.js +30 -5
- package/dist/modes/rpc/connection-roles.d.ts +6 -0
- package/dist/modes/rpc/connection-roles.js +13 -0
- package/dist/modes/rpc/delegated-drive-origin.d.ts +9 -0
- package/dist/modes/rpc/delegated-drive-origin.js +16 -0
- package/dist/modes/rpc/effect-commands.d.ts +7 -1
- package/dist/modes/rpc/host-bare-endpoint-registration.d.ts +7 -0
- package/dist/modes/rpc/host-bare-endpoint-registration.js +35 -0
- package/dist/modes/rpc/host-capabilities.d.ts +2 -0
- package/dist/modes/rpc/host-capabilities.js +3 -0
- package/dist/modes/rpc/host-handover-parked.d.ts +11 -0
- package/dist/modes/rpc/host-handover-parked.js +17 -0
- package/dist/modes/rpc/host-handover-reopen.js +18 -6
- package/dist/modes/rpc/host-handover-resident-writer.d.ts +16 -0
- package/dist/modes/rpc/host-handover-resident-writer.js +57 -0
- package/dist/modes/rpc/host-idle-handover.js +1 -0
- package/dist/modes/rpc/host-warm.d.ts +16 -1
- package/dist/modes/rpc/host-warm.js +33 -6
- package/dist/modes/rpc/method-classes-schema-delivery.d.ts +32 -0
- package/dist/modes/rpc/method-classes-schema-delivery.js +22 -0
- package/dist/modes/rpc/method-classes-schema-session.d.ts +36 -0
- package/dist/modes/rpc/method-classes-schema-session.js +3 -0
- package/dist/modes/rpc/method-classes-schema.d.ts +36 -0
- package/dist/modes/rpc/method-classes.d.ts +2 -0
- package/dist/modes/rpc/method-classes.js +4 -0
- package/dist/modes/rpc/multi-session-host.js +6 -1
- package/dist/modes/rpc/permission-preset-command.d.ts +6 -0
- package/dist/modes/rpc/permission-preset-command.js +27 -0
- package/dist/modes/rpc/rpc-input-validation.js +4 -3
- package/dist/modes/rpc/rpc-mutation-response-types.d.ts +7 -0
- package/dist/modes/rpc/rpc-response-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-command-types.d.ts +6 -1
- package/dist/modes/rpc/rpc-session-event-types.d.ts +5 -2
- package/dist/modes/rpc/rpc-session-state-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-state.js +2 -0
- package/dist/modes/rpc/session-attachment-commands.js +3 -3
- package/dist/modes/rpc/session-event-writer-connections.js +1 -0
- package/dist/modes/rpc/session-event-writer.d.ts +1 -1
- package/dist/modes/rpc/session-registry-attach.d.ts +0 -9
- package/dist/modes/rpc/session-registry-attach.js +16 -3
- package/dist/modes/rpc/session-registry-types.d.ts +2 -0
- package/dist/modes/rpc/session-registry.d.ts +2 -0
- package/dist/modes/rpc/session-registry.js +16 -3
- package/dist/modes/rpc/session-router-close.js +43 -6
- package/dist/modes/rpc/session-router-context.js +2 -0
- package/dist/modes/rpc/session-router-occupancy.js +2 -11
- package/dist/modes/rpc/session-router-open-profile.js +4 -0
- package/dist/modes/rpc/session-router-open.d.ts +1 -1
- package/dist/modes/rpc/session-router-open.js +6 -17
- package/dist/modes/rpc/session-router-residency.d.ts +24 -0
- package/dist/modes/rpc/session-router-residency.js +90 -0
- package/dist/modes/rpc/session-router-routing.js +7 -1
- package/dist/modes/rpc/session-status.js +4 -1
- package/dist/modes/rpc/session-worker.js +4 -0
- package/dist/modes/rpc/tool-child/terminal-jobs.d.ts +1 -0
- package/dist/modes/rpc/tool-child/terminal-jobs.js +17 -1
- package/dist/modes/rpc/worker-session-registry.js +5 -2
- package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
- package/docs/engine-socket/fixtures/rpc-inbound-samples.json +29 -2
- package/docs/extensions.md +12 -0
- package/docs/rpc.md +129 -12
- package/docs/terminal-tools.md +25 -1
- package/native/socket-secret/prebuilds/win32-arm64/senpi_socket_secret.win32-arm64.node +0 -0
- package/native/socket-secret/prebuilds/win32-x64/senpi_socket_secret.win32-x64.node +0 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-C7HEEBFH.js +0 -2
- package/dist/bundle/chunks/chunk-XCMII2VQ.js +0 -98
- package/dist/bundle/chunks/interactive-mode-B5SQDLH7.js +0 -2
- package/dist/bundle/chunks/main-UQFVFCNY.js +0 -2
- package/dist/bundle/chunks/rpc-mode-JYN3E7P7.js +0 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isHostOwnedToolRuntimeCapability, toolRuntimeCapabilities, } from "../../cli/tool-runtime.js";
|
|
2
2
|
import { messagePathProjectRoot, resolveMessagePath } from "../../core/message-path-resolution.js";
|
|
3
3
|
import { startOpenTimer } from "../../core/open-timing.js";
|
|
4
|
+
import { admitExternalMessage } from "./admit-external-message-command.js";
|
|
4
5
|
import { apiKeyLoginCredential } from "./api-key-login-credential.js";
|
|
5
6
|
import { clientFacingIdentity } from "./client-admission-record.js";
|
|
6
7
|
import { ExtensionOwnerLifetime } from "./extension-owner-lifetime.js";
|
|
@@ -51,12 +52,15 @@ import { ConnectionQuestionBridge, degradeQuestion, sessionQuestionBridges } fro
|
|
|
51
52
|
import { remoteOriginDeclaration, remoteOriginField } from "./remote-origin.js";
|
|
52
53
|
import { advertisedCompactCapabilities, sessionDeclaration } from "./compact-negotiation.js";
|
|
53
54
|
import { commandAuthority } from "./command-authority.js";
|
|
55
|
+
import { noteDelegatedRecord } from "./delegated-drive-origin.js";
|
|
54
56
|
import { ConnectionRoles, DELEGATION_INCARNATION_STALE, roleConnection } from "./connection-roles.js";
|
|
55
57
|
import { ACCOUNT_RESET_CAPABILITY, 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, PROVIDER_ACCOUNT_TEST_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";
|
|
56
58
|
import { runAbort, runGetEffectStatus, runQueuedInputChange, runSetThinkingLevel, } from "./effect-commands.js";
|
|
57
59
|
import { EffectLedger } from "./effect-ledger.js";
|
|
58
60
|
import { EFFECT_LEDGER_CAPABILITY } from "./effect-record.js";
|
|
59
61
|
import { handleModelCommand } from "./handlers-model.js";
|
|
62
|
+
import { SET_PERMISSION_PRESET_CAPABILITY } from "./host-capabilities.js";
|
|
63
|
+
import { runSetPermissionPreset } from "./permission-preset-command.js";
|
|
60
64
|
import { createRpcEventOutputBuffer } from "./event-output-buffer.js";
|
|
61
65
|
import { extensionUiProgressReply, refusedExtensionUiResponse } from "./extension-ui-response.js";
|
|
62
66
|
import { answerExtensionUiResponse } from "./handlers-answer.js";
|
|
@@ -86,11 +90,13 @@ import { answerHostResponse, isHostResponse, relocationChannel, } from "./sessio
|
|
|
86
90
|
export { buildRpcSessionState } from "./rpc-session-state.js";
|
|
87
91
|
/** The tool call a dialog is about, as wire fields (absent when the dialog is not a permission request). */
|
|
88
92
|
function dialogCall(opts) {
|
|
93
|
+
const audience = opts?.audience === "session" ? { audience: "session" } : {};
|
|
89
94
|
if (opts?.toolCallId === undefined)
|
|
90
|
-
return
|
|
95
|
+
return audience;
|
|
91
96
|
return {
|
|
92
97
|
toolCallId: opts.toolCallId,
|
|
93
98
|
...(opts.parentToolCallId === undefined ? {} : { parentToolCallId: opts.parentToolCallId }),
|
|
99
|
+
...audience,
|
|
94
100
|
};
|
|
95
101
|
}
|
|
96
102
|
function loadedExtensionName(path) {
|
|
@@ -201,6 +207,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
201
207
|
const connectionRoles = options.connectionRoles ?? new ConnectionRoles();
|
|
202
208
|
const currentConnectionId = () => roleConnection(options.clientInfo?.connectionId());
|
|
203
209
|
const hasRole = (role) => connectionRoles.has(currentConnectionId(), role);
|
|
210
|
+
const connectionOrigin = () => connectionRoles.isDelegated(currentConnectionId()) ? "delegated" : "local";
|
|
204
211
|
const routingSessionId = options.sessionId;
|
|
205
212
|
// True only while THIS connection's own command drives a replacement; the issuer
|
|
206
213
|
// already learns the new identity from its command response.
|
|
@@ -449,7 +456,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
449
456
|
? questions.ask(request, opts)
|
|
450
457
|
: degradeQuestion(createExtensionUIContext(), request, opts),
|
|
451
458
|
select: (title, options, opts) => createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
|
|
452
|
-
confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false),
|
|
459
|
+
confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false)),
|
|
453
460
|
input: (title, placeholder, opts) => opts?.secret
|
|
454
461
|
? secretInputs.input(title, placeholder, opts)
|
|
455
462
|
: createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout, ...dialogCall(opts) }, (r) => ("cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined)),
|
|
@@ -564,7 +571,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
564
571
|
// Host should track editor state locally if needed
|
|
565
572
|
return "";
|
|
566
573
|
},
|
|
567
|
-
async editor(title, prefill) {
|
|
574
|
+
async editor(title, prefill, opts) {
|
|
568
575
|
const id = crypto.randomUUID();
|
|
569
576
|
const frame = {
|
|
570
577
|
type: "extension_ui_request",
|
|
@@ -572,6 +579,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
572
579
|
method: "editor",
|
|
573
580
|
title,
|
|
574
581
|
prefill,
|
|
582
|
+
...dialogCall(opts),
|
|
575
583
|
...remoteOriginField(raiserOrigin()),
|
|
576
584
|
};
|
|
577
585
|
return new Promise((resolve, reject) => {
|
|
@@ -1000,6 +1008,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1000
1008
|
// Handle a single command
|
|
1001
1009
|
const handleCommand = async (command, requestContext) => {
|
|
1002
1010
|
const id = command.id;
|
|
1011
|
+
noteDelegatedRecord(command, connectionOrigin(), session.sessionManager);
|
|
1003
1012
|
switch (command.type) {
|
|
1004
1013
|
case "get_protocol_info":
|
|
1005
1014
|
return {
|
|
@@ -1033,6 +1042,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1033
1042
|
SESSION_MODEL_SCOPE_CAPABILITY,
|
|
1034
1043
|
RESUME_SESSION_CAPABILITY,
|
|
1035
1044
|
ENTRIES_TAIL_CAPABILITY,
|
|
1045
|
+
SET_PERMISSION_PRESET_CAPABILITY,
|
|
1036
1046
|
...(options.retryFallbackCommand ? [RETRY_FALLBACK_COMMAND_CAPABILITY] : []),
|
|
1037
1047
|
...(options.toolRuntime ? toolRuntimeCapabilities(options.toolRuntime) : []),
|
|
1038
1048
|
// Client flags cannot advertise a host guard this classic binding does not run, nor a tool runtime.
|
|
@@ -1055,7 +1065,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1055
1065
|
if (holdAutomaticInput(session, command, hasRole(INTERACTIVE_USER_CAPABILITY)))
|
|
1056
1066
|
return success(id, "prompt", { disposition: "handled", heldAsNotice: true });
|
|
1057
1067
|
turnRequested = true;
|
|
1058
|
-
const admission = handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY) });
|
|
1068
|
+
const admission = handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY), connectionOrigin: connectionOrigin() });
|
|
1059
1069
|
promptCalls.add(admission);
|
|
1060
1070
|
void admission.finally(() => promptCalls.delete(admission));
|
|
1061
1071
|
return undefined;
|
|
@@ -1125,7 +1135,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1125
1135
|
if (holdAutomaticInput(session, command, hasRole(INTERACTIVE_USER_CAPABILITY)))
|
|
1126
1136
|
return success(id, command.type, { disposition: "handled", heldAsNotice: true });
|
|
1127
1137
|
turnRequested = true;
|
|
1128
|
-
await handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY) });
|
|
1138
|
+
await handleClientInput(session, command, { output, promptCalls }, { interactiveUser: hasRole(INTERACTIVE_USER_CAPABILITY), connectionOrigin: connectionOrigin() });
|
|
1129
1139
|
return undefined;
|
|
1130
1140
|
}
|
|
1131
1141
|
case "abort":
|
|
@@ -1223,6 +1233,19 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1223
1233
|
const result = (await sessionControl?.wake(deliveryIds)) ?? {};
|
|
1224
1234
|
return success(id, "wake", { admitted: result.admitted ?? [] });
|
|
1225
1235
|
}
|
|
1236
|
+
case "admit_external_message": {
|
|
1237
|
+
const peer = {
|
|
1238
|
+
origin: connectionRoles.originOf(currentConnectionId()),
|
|
1239
|
+
connectionOrigin: connectionOrigin(),
|
|
1240
|
+
};
|
|
1241
|
+
const answer = admitExternalMessage(session, command, peer);
|
|
1242
|
+
if (!answer.ok) {
|
|
1243
|
+
const { refusal } = answer;
|
|
1244
|
+
return error(id, command.type, refusal.message, refusal.errorCode, "errorData" in refusal ? refusal.errorData : undefined);
|
|
1245
|
+
}
|
|
1246
|
+
turnRequested = true;
|
|
1247
|
+
return success(id, command.type, answer.data);
|
|
1248
|
+
}
|
|
1226
1249
|
case "new_session": {
|
|
1227
1250
|
const options = command.parentSession ? { parentSession: command.parentSession } : undefined;
|
|
1228
1251
|
const result = await runtimeHost.newSession(options);
|
|
@@ -1289,6 +1312,8 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1289
1312
|
// =================================================================
|
|
1290
1313
|
case "set_thinking_level":
|
|
1291
1314
|
return runSetThinkingLevel(effectContext(), command);
|
|
1315
|
+
case "set_permission_preset":
|
|
1316
|
+
return runSetPermissionPreset(runtimeHost, command);
|
|
1292
1317
|
case "cycle_thinking_level": {
|
|
1293
1318
|
const level = session.cycleThinkingLevel();
|
|
1294
1319
|
if (!level) {
|
|
@@ -34,6 +34,8 @@ export declare class ConnectionRoles {
|
|
|
34
34
|
private readonly origins;
|
|
35
35
|
/** A worker isolate's first-seen delegation per connection (`retain`), kept for the worker's life. */
|
|
36
36
|
private readonly retained;
|
|
37
|
+
/** A worker isolate's connections whose bind or command carried a delegation (a legacy one is never retained). */
|
|
38
|
+
private readonly carried;
|
|
37
39
|
private source;
|
|
38
40
|
/** The host reads each connection's live record from its writer's delegations. */
|
|
39
41
|
useDelegations(source: (connectionId: string) => DelegatedConnectionView | undefined): void;
|
|
@@ -59,11 +61,15 @@ export declare class ConnectionRoles {
|
|
|
59
61
|
declareOrigin(connectionId: string, origin: RpcRemoteOrigin): void;
|
|
60
62
|
/** The remote device this connection speaks for: a delegated connection's principal, else its declared origin. */
|
|
61
63
|
originOf(connectionId: string | undefined): RpcRemoteOrigin | undefined;
|
|
64
|
+
/** Whether the connection is delegated (a remote device's): on the host, retained by a worker, or carried to one. */
|
|
65
|
+
isDelegated(connectionId: string | undefined): boolean;
|
|
62
66
|
has(connectionId: string | undefined, role: string): boolean;
|
|
63
67
|
/** A logical detach: drops claims of earlier incarnations of a delegated socket, and everything of a local one. */
|
|
64
68
|
release(connectionId: string): void;
|
|
65
69
|
/** The socket is gone: nothing of it is kept. */
|
|
66
70
|
forget(connectionId: string): void;
|
|
71
|
+
/** Worker isolate: a bind or command for `connectionId` carried a delegation, in either mode. */
|
|
72
|
+
noteCarriedDelegation(connectionId: string): void;
|
|
67
73
|
/** Forgets every connection, for a worker isolate whose session ends. */
|
|
68
74
|
clear(): void;
|
|
69
75
|
}
|
|
@@ -35,6 +35,8 @@ export class ConnectionRoles {
|
|
|
35
35
|
this.origins = new Map();
|
|
36
36
|
/** A worker isolate's first-seen delegation per connection (`retain`), kept for the worker's life. */
|
|
37
37
|
this.retained = new Map();
|
|
38
|
+
/** A worker isolate's connections whose bind or command carried a delegation (a legacy one is never retained). */
|
|
39
|
+
this.carried = new Set();
|
|
38
40
|
}
|
|
39
41
|
/** The host reads each connection's live record from its writer's delegations. */
|
|
40
42
|
useDelegations(source) {
|
|
@@ -125,6 +127,11 @@ export class ConnectionRoles {
|
|
|
125
127
|
? { deviceId: principal.deviceId }
|
|
126
128
|
: { deviceId: principal.deviceId, deviceName: principal.deviceName };
|
|
127
129
|
}
|
|
130
|
+
/** Whether the connection is delegated (a remote device's): on the host, retained by a worker, or carried to one. */
|
|
131
|
+
isDelegated(connectionId) {
|
|
132
|
+
const key = roleConnection(connectionId);
|
|
133
|
+
return this.viewOf(key) !== undefined || this.carried.has(key);
|
|
134
|
+
}
|
|
128
135
|
has(connectionId, role) {
|
|
129
136
|
const key = roleConnection(connectionId);
|
|
130
137
|
const store = commandAuthority.getStore();
|
|
@@ -155,12 +162,18 @@ export class ConnectionRoles {
|
|
|
155
162
|
this.claims.delete(connectionId);
|
|
156
163
|
this.origins.delete(connectionId);
|
|
157
164
|
this.retained.delete(connectionId);
|
|
165
|
+
this.carried.delete(connectionId);
|
|
166
|
+
}
|
|
167
|
+
/** Worker isolate: a bind or command for `connectionId` carried a delegation, in either mode. */
|
|
168
|
+
noteCarriedDelegation(connectionId) {
|
|
169
|
+
this.carried.add(connectionId);
|
|
158
170
|
}
|
|
159
171
|
/** Forgets every connection, for a worker isolate whose session ends. */
|
|
160
172
|
clear() {
|
|
161
173
|
this.claims.clear();
|
|
162
174
|
this.origins.clear();
|
|
163
175
|
this.retained.clear();
|
|
176
|
+
this.carried.clear();
|
|
164
177
|
}
|
|
165
178
|
}
|
|
166
179
|
//# sourceMappingURL=connection-roles.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SessionManager } from "../../core/session-manager.ts";
|
|
2
|
+
import { type ConnectionOrigin } from "../../core/turn-origin.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Marks a session as remote-driven when a delegated connection sends it any record that can act on its work, decided
|
|
5
|
+
* by the engine's own method-class table (`drive`, and `device_admin`, which grants more), never by a hand-kept list of
|
|
6
|
+
* commands, so a new run starter (`continue_from_leaf`, `wake`, `resume_for_run`, ...) cannot slip past the rule.
|
|
7
|
+
*/
|
|
8
|
+
export declare function noteDelegatedRecord(command: object, origin: ConnectionOrigin, sessionManager: Pick<SessionManager, "getEntries" | "appendCustomEntry">): void;
|
|
9
|
+
//# sourceMappingURL=delegated-drive-origin.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { recordDelegatedOrigin } from "../../core/turn-origin.js";
|
|
2
|
+
import { classifyRpcMethod } from "./method-classes.js";
|
|
3
|
+
const ACTING_CLASSES = new Set(["drive", "device_admin"]);
|
|
4
|
+
/**
|
|
5
|
+
* Marks a session as remote-driven when a delegated connection sends it any record that can act on its work, decided
|
|
6
|
+
* by the engine's own method-class table (`drive`, and `device_admin`, which grants more), never by a hand-kept list of
|
|
7
|
+
* commands, so a new run starter (`continue_from_leaf`, `wake`, `resume_for_run`, ...) cannot slip past the rule.
|
|
8
|
+
*/
|
|
9
|
+
export function noteDelegatedRecord(command, origin, sessionManager) {
|
|
10
|
+
if (origin !== "delegated")
|
|
11
|
+
return;
|
|
12
|
+
const methodClass = classifyRpcMethod(command);
|
|
13
|
+
if (methodClass !== undefined && ACTING_CLASSES.has(methodClass))
|
|
14
|
+
recordDelegatedOrigin(sessionManager);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=delegated-drive-origin.js.map
|
|
@@ -37,7 +37,7 @@ export declare function runAbort(ctx: EffectCommandContext, command: Command<"ab
|
|
|
37
37
|
* or no longer exists.
|
|
38
38
|
*/
|
|
39
39
|
export declare function runQueuedInputChange(ctx: EffectCommandContext, command: Command<"dequeue_message"> | Command<"edit_queued_message">): Promise<RpcResponse>;
|
|
40
|
-
export declare function runSetThinkingLevel(ctx: EffectCommandContext, command: Command<"set_thinking_level">): Promise<import("./session-move-wire.ts").RpcMoveSessionResponse | import("./compact-admission-error.ts").UnknownAdmissionResponse | {
|
|
40
|
+
export declare function runSetThinkingLevel(ctx: EffectCommandContext, command: Command<"set_thinking_level">): Promise<import("./admit-external-message-wire.ts").RpcAdmitExternalMessageResponse | import("./session-move-wire.ts").RpcMoveSessionResponse | import("./compact-admission-error.ts").UnknownAdmissionResponse | {
|
|
41
41
|
readonly id?: string;
|
|
42
42
|
readonly type: "response";
|
|
43
43
|
readonly command: "attach_session";
|
|
@@ -430,6 +430,12 @@ export declare function runSetThinkingLevel(ctx: EffectCommandContext, command:
|
|
|
430
430
|
type: "response";
|
|
431
431
|
command: "set_auto_compaction";
|
|
432
432
|
success: true;
|
|
433
|
+
} | {
|
|
434
|
+
id?: string;
|
|
435
|
+
type: "response";
|
|
436
|
+
command: "set_permission_preset";
|
|
437
|
+
success: true;
|
|
438
|
+
data: import("../../core/permission-mode.ts").PermissionModeReply;
|
|
433
439
|
} | {
|
|
434
440
|
id?: string;
|
|
435
441
|
type: "response";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface BareEndpointRegistration {
|
|
2
|
+
readonly agentDir: string;
|
|
3
|
+
readonly socket: string;
|
|
4
|
+
readonly log: (message: string) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function bindRegisteredEndpoint<T>(registration: BareEndpointRegistration, bind: () => Promise<T>): Promise<T>;
|
|
7
|
+
//# sourceMappingURL=host-bare-endpoint-registration.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry record of a socket host that binds its own public socket (`--listen`, `--headless`). No
|
|
3
|
+
* `host ensure` runs for such a host, so nothing else writes its `endpoint.json`, and `host gc` removes the
|
|
4
|
+
* record with the rest of a dead endpoint's directory: after one outage the restarted host would serve
|
|
5
|
+
* while every registry reader (`host status --all`, a relay device service) finds nothing (senpi#6190).
|
|
6
|
+
*
|
|
7
|
+
* So the host writes the record itself before it serves, and binds inside the endpoint's ensure lock, the
|
|
8
|
+
* lock `host gc` judges and removes under: a gc that judged the socket dead has finished its removal
|
|
9
|
+
* before this write, and one that starts later finds the socket bound and keeps the endpoint. A supervised
|
|
10
|
+
* host never comes here - its ensure wrote the record and holds that lock while it starts the host.
|
|
11
|
+
*/
|
|
12
|
+
import { createDaemonDirectories, createHostDaemonPaths } from "./host-daemon-paths.js";
|
|
13
|
+
import { acquireHostEnsureLock } from "./host-ensure-lock.js";
|
|
14
|
+
/** Longer than a gc's hold of one endpoint (its 2 s lock wait, the evidence probes, the removal). */
|
|
15
|
+
const REGISTRATION_LOCK_WAIT_MS = 10_000;
|
|
16
|
+
export async function bindRegisteredEndpoint(registration, bind) {
|
|
17
|
+
// An abstract socket has no path, so it has no daemon directory either.
|
|
18
|
+
if (registration.socket.startsWith("\0"))
|
|
19
|
+
return bind();
|
|
20
|
+
const release = await acquireHostEnsureLock(registration.socket, REGISTRATION_LOCK_WAIT_MS).catch((cause) => {
|
|
21
|
+
registration.log(`endpoint ensure lock unavailable, registering without it: ${messageOf(cause)}`);
|
|
22
|
+
return undefined;
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
await createDaemonDirectories(createHostDaemonPaths({ socket: registration.socket, agentDir: registration.agentDir })).catch((cause) => registration.log(`endpoint registry record not written: ${messageOf(cause)}`));
|
|
26
|
+
return await bind();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await release?.();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function messageOf(cause) {
|
|
33
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=host-bare-endpoint-registration.js.map
|
|
@@ -5,6 +5,8 @@ export declare const UNLOAD_SESSION_CAPABILITY = "unload_session";
|
|
|
5
5
|
export declare const SUBSCRIPTION_LIMITS_CAPABILITY = "subscription_limits";
|
|
6
6
|
/** `resolve_path`: which file a message's path means, the engine's one rule (senpi#3415). */
|
|
7
7
|
export declare const RESOLVE_PATH_CAPABILITY = "resolve_path";
|
|
8
|
+
/** `set_permission_preset`: a live session's approval mode can be set and is echoed back (classic and multi-session). */
|
|
9
|
+
export declare const SET_PERMISSION_PRESET_CAPABILITY = "set_permission_preset_v1";
|
|
8
10
|
/**
|
|
9
11
|
* The capabilities a multi-session host advertises in `get_protocol_info`, in their wire order. `warm` is added only
|
|
10
12
|
* when the runtime is in-process, `tool_runtime:<kind>` names where heavy tool work runs, and `negotiated` carries
|
|
@@ -13,6 +13,8 @@ export const UNLOAD_SESSION_CAPABILITY = "unload_session";
|
|
|
13
13
|
export const SUBSCRIPTION_LIMITS_CAPABILITY = "subscription_limits";
|
|
14
14
|
/** `resolve_path`: which file a message's path means, the engine's one rule (senpi#3415). */
|
|
15
15
|
export const RESOLVE_PATH_CAPABILITY = "resolve_path";
|
|
16
|
+
/** `set_permission_preset`: a live session's approval mode can be set and is echoed back (classic and multi-session). */
|
|
17
|
+
export const SET_PERMISSION_PRESET_CAPABILITY = "set_permission_preset_v1";
|
|
16
18
|
/**
|
|
17
19
|
* The capabilities a multi-session host advertises in `get_protocol_info`, in their wire order. `warm` is added only
|
|
18
20
|
* when the runtime is in-process, `tool_runtime:<kind>` names where heavy tool work runs, and `negotiated` carries
|
|
@@ -56,6 +58,7 @@ export function multiSessionHostCapabilities(options) {
|
|
|
56
58
|
// `get_state`/`get_entries` marked `observe` never attach, and history reads page backward too (senpi#4267).
|
|
57
59
|
OBSERVE_READS_CAPABILITY,
|
|
58
60
|
ENTRIES_TAIL_CAPABILITY,
|
|
61
|
+
SET_PERMISSION_PRESET_CAPABILITY,
|
|
59
62
|
// Host capabilities, not client opt-ins: only a multi-session host owns the
|
|
60
63
|
// attachment refcount `open_session.retain_on_disconnect` detaches from, the
|
|
61
64
|
// per-session launch profile `context`/`auto_title` travel on, and the session
|
|
@@ -4,6 +4,7 @@ import type { PromptSurface } from "../../core/dynamic-prompt/types.ts";
|
|
|
4
4
|
import type { SessionContext, SessionKind } from "../../core/extensions/types.ts";
|
|
5
5
|
import { type SessionEnv } from "./session-caller-env.ts";
|
|
6
6
|
import { type SessionContextOverlay } from "./session-context-patch.ts";
|
|
7
|
+
import { type ResidentWriter } from "./host-handover-resident-writer.ts";
|
|
7
8
|
export interface HandoverParkedEntry {
|
|
8
9
|
readonly sessionPath: string;
|
|
9
10
|
readonly durableSessionId?: string;
|
|
@@ -41,9 +42,19 @@ export interface HandoverParkedEntry {
|
|
|
41
42
|
* Absent on entries an older generation wrote, which every reader treats as a predecessor's park.
|
|
42
43
|
*/
|
|
43
44
|
readonly parkedBy?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Present while the session is open on its host (senpi#6190); a park drops it. A successor reopens a stamped
|
|
47
|
+
* entry only once that writer process is gone, so it never opens a session its live predecessor still holds.
|
|
48
|
+
*/
|
|
49
|
+
readonly residentWriter?: ResidentWriter;
|
|
44
50
|
}
|
|
45
51
|
export declare function recordHandoverParkedSession(daemonDir: string, entry: HandoverParkedEntry): Promise<void>;
|
|
46
52
|
export declare function readHandoverParkedSessions(daemonDir: string): Promise<readonly HandoverParkedEntry[]>;
|
|
47
53
|
export declare function clearHandoverParkedSession(daemonDir: string, sessionPath: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Clears an inherited entry after its reopen, unless the reopen re-recorded it as resident on
|
|
56
|
+
* `holder` (a retained session is recorded by the host it is open on, so the next start reopens it).
|
|
57
|
+
*/
|
|
58
|
+
export declare function clearInheritedParkedSession(daemonDir: string, sessionPath: string, holder: string | undefined): Promise<void>;
|
|
48
59
|
export declare function dropHandoverParkedSessionIfFileGone(daemonDir: string, sessionPath: string): Promise<void>;
|
|
49
60
|
//# sourceMappingURL=host-handover-parked.d.ts.map
|
|
@@ -21,6 +21,7 @@ import { parsePermissionPresetName } from "../../core/extensions/builtin/permiss
|
|
|
21
21
|
import { sessionAutoTitleError, sessionBrowserEngineError, sessionContextError, sessionKindError, sessionPromptSurfaceError, sessionRetryFallbackError, } from "./rpc-input-validation.js";
|
|
22
22
|
import { sessionEnvShapeError } from "./session-caller-env.js";
|
|
23
23
|
import { applyContextPatch } from "./session-context-patch.js";
|
|
24
|
+
import { isResidentWriter } from "./host-handover-resident-writer.js";
|
|
24
25
|
const recordDir = (daemonDir) => join(daemonDir, "handover-parked");
|
|
25
26
|
const entryFile = (daemonDir, sessionPath) => join(recordDir(daemonDir), `${createHash("sha256").update(sessionPath, "utf8").digest("hex").slice(0, 16)}.json`);
|
|
26
27
|
let mutationChain = Promise.resolve();
|
|
@@ -38,6 +39,7 @@ function isEntry(value) {
|
|
|
38
39
|
(record.durableSessionId === undefined || typeof record.durableSessionId === "string") &&
|
|
39
40
|
(record.cwd === undefined || typeof record.cwd === "string") &&
|
|
40
41
|
(record.parkedBy === undefined || typeof record.parkedBy === "string") &&
|
|
42
|
+
(record.residentWriter === undefined || isResidentWriter(record.residentWriter)) &&
|
|
41
43
|
(record.thinkingLevel === undefined || typeof record.thinkingLevel === "string") &&
|
|
42
44
|
(record.permissionPreset === undefined ||
|
|
43
45
|
(typeof record.permissionPreset === "string" &&
|
|
@@ -100,6 +102,21 @@ export async function readHandoverParkedSessions(daemonDir) {
|
|
|
100
102
|
export function clearHandoverParkedSession(daemonDir, sessionPath) {
|
|
101
103
|
return enqueueMutation(() => rm(entryFile(daemonDir, sessionPath), { force: true }));
|
|
102
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Clears an inherited entry after its reopen, unless the reopen re-recorded it as resident on
|
|
107
|
+
* `holder` (a retained session is recorded by the host it is open on, so the next start reopens it).
|
|
108
|
+
*/
|
|
109
|
+
export function clearInheritedParkedSession(daemonDir, sessionPath, holder) {
|
|
110
|
+
return enqueueMutation(async () => {
|
|
111
|
+
const file = entryFile(daemonDir, sessionPath);
|
|
112
|
+
if (holder !== undefined) {
|
|
113
|
+
const recorded = await readFile(file, "utf8").then((text) => JSON.parse(text), () => undefined);
|
|
114
|
+
if (isEntry(recorded) && recorded.parkedBy === holder)
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await rm(file, { force: true });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
103
120
|
export function dropHandoverParkedSessionIfFileGone(daemonDir, sessionPath) {
|
|
104
121
|
if (existsSync(sessionPath))
|
|
105
122
|
return Promise.resolve();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { createHostDaemonPaths } from "./host-daemon-paths.js";
|
|
3
|
-
import { clearHandoverParkedSession, dropHandoverParkedSessionIfFileGone, readHandoverParkedSessions, } from "./host-handover-parked.js";
|
|
3
|
+
import { clearHandoverParkedSession, clearInheritedParkedSession, dropHandoverParkedSessionIfFileGone, readHandoverParkedSessions, } from "./host-handover-parked.js";
|
|
4
|
+
import { residentWriterProbe } from "./host-handover-resident-writer.js";
|
|
4
5
|
import { RpcSessionRegistryError } from "./session-registry.js";
|
|
5
6
|
// The watch budget must outlast a heavily loaded drain: a predecessor whose event loop is blocked
|
|
6
7
|
// (parallel hosts, a busy turn settling) can take tens of seconds to write its park after this
|
|
@@ -37,6 +38,7 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
|
|
|
37
38
|
const deadline = Date.now() + HANDOVER_PARKED_REOPEN_BUDGET_MS;
|
|
38
39
|
const reopenAttempts = new Map();
|
|
39
40
|
const heldPolls = new Map();
|
|
41
|
+
const writerAlive = residentWriterProbe();
|
|
40
42
|
let emptyStreak = 0;
|
|
41
43
|
let sawAnyPark = false;
|
|
42
44
|
for (;;) {
|
|
@@ -46,12 +48,12 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
|
|
|
46
48
|
// it by path with the same durable id.
|
|
47
49
|
if (holder?.shouldReopen !== undefined && !holder.shouldReopen())
|
|
48
50
|
return;
|
|
49
|
-
const parked = await inheritedParks(daemonDir, holder);
|
|
51
|
+
const parked = await inheritedParks(daemonDir, holder, writerAlive);
|
|
50
52
|
for (const entry of parked) {
|
|
51
53
|
sawAnyPark = true;
|
|
52
54
|
await reopenOne(daemonDir, open, entry, reopenAttempts, heldPolls, holder);
|
|
53
55
|
}
|
|
54
|
-
const remaining = (await inheritedParks(daemonDir, holder)).length;
|
|
56
|
+
const remaining = (await inheritedParks(daemonDir, holder, writerAlive)).length;
|
|
55
57
|
if (holder?.shouldReopen !== undefined && !holder.shouldReopen())
|
|
56
58
|
return;
|
|
57
59
|
emptyStreak = remaining === 0 ? emptyStreak + 1 : 0;
|
|
@@ -67,9 +69,19 @@ export async function reopenHandoverParkedSessions(open, agentDir, socket, holde
|
|
|
67
69
|
* parks retained sessions into the same record) is for its successor: reopening it here would put a
|
|
68
70
|
* second open of the path in flight against the client reopening it (senpi#3485).
|
|
69
71
|
*/
|
|
70
|
-
async function inheritedParks(daemonDir, holder) {
|
|
72
|
+
async function inheritedParks(daemonDir, holder, writerAlive) {
|
|
71
73
|
const parked = await readHandoverParkedSessions(daemonDir);
|
|
72
|
-
|
|
74
|
+
const inherited = [];
|
|
75
|
+
for (const entry of parked) {
|
|
76
|
+
if (entry.parkedBy !== undefined && entry.parkedBy === holder?.hostInstance)
|
|
77
|
+
continue;
|
|
78
|
+
// A session still resident in its live writer (a handoff's draining predecessor) is that process's to park,
|
|
79
|
+
// and its unstamped park is what this host reopens; only a writer that is gone leaves it here to restore.
|
|
80
|
+
if (entry.residentWriter !== undefined && (await writerAlive(entry.residentWriter)))
|
|
81
|
+
continue;
|
|
82
|
+
inherited.push(entry);
|
|
83
|
+
}
|
|
84
|
+
return inherited;
|
|
73
85
|
}
|
|
74
86
|
async function reopenOne(daemonDir, open, entry, attempts, heldPolls, holder) {
|
|
75
87
|
const taskStatus = entry.contextOverlay && Object.hasOwn(entry.contextOverlay, "task_status")
|
|
@@ -93,7 +105,7 @@ async function reopenOne(daemonDir, open, entry, attempts, heldPolls, holder) {
|
|
|
93
105
|
// reopens it; clearing it here would lose the session when the drain closes it (senpi#3282).
|
|
94
106
|
if (holder?.shouldReopen !== undefined && !holder.shouldReopen())
|
|
95
107
|
return;
|
|
96
|
-
await
|
|
108
|
+
await clearInheritedParkedSession(daemonDir, entry.sessionPath, holder?.hostInstance);
|
|
97
109
|
}
|
|
98
110
|
catch (cause) {
|
|
99
111
|
// A `session_path_in_use` refusal means the predecessor still holds the path's cross-generation
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type WriterProbes } from "../../core/session-writer-claims.ts";
|
|
2
|
+
export interface ResidentWriter {
|
|
3
|
+
readonly host: string;
|
|
4
|
+
readonly pid: number;
|
|
5
|
+
readonly bootAtMs: number;
|
|
6
|
+
readonly processStartedAtMs: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function currentResidentWriter(probes?: WriterProbes): ResidentWriter;
|
|
9
|
+
export declare function isResidentWriter(value: unknown): value is ResidentWriter;
|
|
10
|
+
/**
|
|
11
|
+
* A liveness probe for one reopen watch, which polls: a writer found gone stays gone, and a live pid's start time
|
|
12
|
+
* (a process spawn on some platforms) is read once. A writer on another host cannot be probed from here, so it is
|
|
13
|
+
* never displaced.
|
|
14
|
+
*/
|
|
15
|
+
export declare function residentWriterProbe(probes?: WriterProbes): (writer: ResidentWriter) => Promise<boolean>;
|
|
16
|
+
//# sourceMappingURL=host-handover-resident-writer.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process that holds a retained session resident (senpi#6190). A host records each retained session it has
|
|
3
|
+
* open, stamped with this writer, so the next host on the endpoint can restore it after a crash; while that
|
|
4
|
+
* writer is still alive (a generation handoff's draining predecessor) the session is not its successor's to
|
|
5
|
+
* reopen - the predecessor parks it, and the unstamped park is what the successor consumes. Liveness follows the
|
|
6
|
+
* session writer claims: same host, same boot, live pid whose start time matches.
|
|
7
|
+
*/
|
|
8
|
+
import { BOOT_INSTANT_TOLERANCE_MS, sameBoot } from "../../core/extensions/builtin/terminal/process-identity.js";
|
|
9
|
+
import { realWriterProbes } from "../../core/session-writer-claims.js";
|
|
10
|
+
export function currentResidentWriter(probes = realWriterProbes) {
|
|
11
|
+
const self = probes.self();
|
|
12
|
+
return {
|
|
13
|
+
host: probes.host(),
|
|
14
|
+
pid: self.pid,
|
|
15
|
+
bootAtMs: probes.bootAtMs(),
|
|
16
|
+
processStartedAtMs: self.processStartedAtMs,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function isResidentWriter(value) {
|
|
20
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
21
|
+
return false;
|
|
22
|
+
const record = value;
|
|
23
|
+
return (typeof record.host === "string" &&
|
|
24
|
+
Number.isInteger(record.pid) &&
|
|
25
|
+
typeof record.bootAtMs === "number" &&
|
|
26
|
+
typeof record.processStartedAtMs === "number");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A liveness probe for one reopen watch, which polls: a writer found gone stays gone, and a live pid's start time
|
|
30
|
+
* (a process spawn on some platforms) is read once. A writer on another host cannot be probed from here, so it is
|
|
31
|
+
* never displaced.
|
|
32
|
+
*/
|
|
33
|
+
export function residentWriterProbe(probes = realWriterProbes) {
|
|
34
|
+
const gone = new Set();
|
|
35
|
+
const verified = new Set();
|
|
36
|
+
return async (writer) => {
|
|
37
|
+
const key = `${writer.host}:${writer.pid}:${writer.processStartedAtMs}`;
|
|
38
|
+
if (gone.has(key))
|
|
39
|
+
return false;
|
|
40
|
+
if (writer.host !== probes.host())
|
|
41
|
+
return true;
|
|
42
|
+
if (!sameBoot(probes.bootAtMs(), writer.bootAtMs) || !probes.pidAlive(writer.pid)) {
|
|
43
|
+
gone.add(key);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (verified.has(key))
|
|
47
|
+
return true;
|
|
48
|
+
const startedAtMs = await probes.readProcessStartMs(writer.pid);
|
|
49
|
+
if (startedAtMs !== undefined && Math.abs(startedAtMs - writer.processStartedAtMs) > BOOT_INSTANT_TOLERANCE_MS) {
|
|
50
|
+
gone.add(key);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
verified.add(key);
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=host-handover-resident-writer.js.map
|
|
@@ -30,6 +30,11 @@ export declare class HostWarmer {
|
|
|
30
30
|
private readonly load;
|
|
31
31
|
constructor(load: (profile: HostWarmProfile) => Promise<void>);
|
|
32
32
|
run(profile: HostWarmProfile): Promise<HostWarmState>;
|
|
33
|
+
/**
|
|
34
|
+
* Settles once every load of `cwd` in flight NOW has settled; `undefined` when none is. A snapshot: a warm
|
|
35
|
+
* that starts later, or one of another cwd, is not part of it.
|
|
36
|
+
*/
|
|
37
|
+
inFlight(cwd: string): Promise<void> | undefined;
|
|
33
38
|
private loadOnce;
|
|
34
39
|
}
|
|
35
40
|
/**
|
|
@@ -40,7 +45,17 @@ export declare class HostWarmer {
|
|
|
40
45
|
export declare function createRegistryWarm(prepare: NonNullable<PreparableRuntimeFactory["prepare"]>, host: {
|
|
41
46
|
agentDir: string;
|
|
42
47
|
mcpRegistry?: HostMcpRegistry;
|
|
43
|
-
}):
|
|
48
|
+
}): {
|
|
49
|
+
readonly warm: HostWarm;
|
|
50
|
+
readonly inFlight: (cwd: string) => Promise<void> | undefined;
|
|
51
|
+
};
|
|
52
|
+
/** How an open's wait for the warms of its cwd ended. */
|
|
53
|
+
export type WarmWaitOutcome = "settled" | "warm_wait_deadline";
|
|
54
|
+
/**
|
|
55
|
+
* Waits for `warming` at most `boundMs`. A warm whose extension factory never settles (there is no factory
|
|
56
|
+
* timeout) must not stall an open, so past the bound the open proceeds and the reason is reported.
|
|
57
|
+
*/
|
|
58
|
+
export declare function waitForWarms(warming: Promise<void>, boundMs: number, cwd: string): Promise<WarmWaitOutcome>;
|
|
44
59
|
type WarmCommand = Extract<RpcCommand, {
|
|
45
60
|
type: "warm";
|
|
46
61
|
}>;
|
|
@@ -34,14 +34,24 @@ export class HostWarmer {
|
|
|
34
34
|
const key = profileKey(profile);
|
|
35
35
|
if (this.warm.has(key))
|
|
36
36
|
return "already_warm";
|
|
37
|
-
let
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
this.loading.set(key,
|
|
37
|
+
let load = this.loading.get(key);
|
|
38
|
+
if (load === undefined) {
|
|
39
|
+
load = { cwd: profile.cwd, pending: this.loadOnce(key, profile) };
|
|
40
|
+
this.loading.set(key, load);
|
|
41
41
|
}
|
|
42
|
-
await pending;
|
|
42
|
+
await load.pending;
|
|
43
43
|
return "warmed";
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Settles once every load of `cwd` in flight NOW has settled; `undefined` when none is. A snapshot: a warm
|
|
47
|
+
* that starts later, or one of another cwd, is not part of it.
|
|
48
|
+
*/
|
|
49
|
+
inFlight(cwd) {
|
|
50
|
+
const pending = [...this.loading.values()].filter((load) => load.cwd === cwd).map((load) => load.pending);
|
|
51
|
+
if (pending.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
return Promise.allSettled(pending).then(() => undefined);
|
|
54
|
+
}
|
|
45
55
|
async loadOnce(key, profile) {
|
|
46
56
|
try {
|
|
47
57
|
await this.load(profile);
|
|
@@ -85,7 +95,24 @@ export function createRegistryWarm(prepare, host) {
|
|
|
85
95
|
scope.close();
|
|
86
96
|
}
|
|
87
97
|
});
|
|
88
|
-
return (profile) => warmer.run(profile);
|
|
98
|
+
return { warm: (profile) => warmer.run(profile), inFlight: (cwd) => warmer.inFlight(cwd) };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Waits for `warming` at most `boundMs`. A warm whose extension factory never settles (there is no factory
|
|
102
|
+
* timeout) must not stall an open, so past the bound the open proceeds and the reason is reported.
|
|
103
|
+
*/
|
|
104
|
+
export async function waitForWarms(warming, boundMs, cwd) {
|
|
105
|
+
let deadline;
|
|
106
|
+
const outcome = await Promise.race([
|
|
107
|
+
warming.then(() => "settled"),
|
|
108
|
+
new Promise((resolve) => {
|
|
109
|
+
deadline = setTimeout(() => resolve("warm_wait_deadline"), boundMs);
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
clearTimeout(deadline);
|
|
113
|
+
if (outcome === "warm_wait_deadline")
|
|
114
|
+
process.stderr.write(`senpi rpc open: warm_wait_deadline: a warm of ${cwd} still loading after ${boundMs} ms; opening anyway\n`);
|
|
115
|
+
return outcome;
|
|
89
116
|
}
|
|
90
117
|
function refusal(command, code) {
|
|
91
118
|
return { id: command.id, type: "response", command: "warm", success: false, error: code };
|