@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
|
@@ -5,6 +5,8 @@ export const CONTINUATION_HOLD_STATE_EVENT = "continuation_hold_state";
|
|
|
5
5
|
/** Live widgets of a session's long background jobs; payload `JobWidgetState` (job-widgets/view-types.ts). */
|
|
6
6
|
export const JOB_WIDGET_STATE_EVENT = "job_widget_state";
|
|
7
7
|
export const MONITOR_NOTIFICATION_CUSTOM_TYPE = "senpi-monitor:notification";
|
|
8
|
+
/** A background terminal session's age notice or auto-reap; payload `TerminalBackgroundAgeEvent` (senpi#6201). */
|
|
9
|
+
export const TERMINAL_BACKGROUND_AGE_EVENT = "terminal_background_age";
|
|
8
10
|
/** True for a delivered monitor notification that carries a person's words (omo#10280). */
|
|
9
11
|
export function isHumanInputMonitorNotification(message) {
|
|
10
12
|
if (typeof message !== "object" || message === null)
|
|
@@ -13,7 +13,7 @@ import { INTERNAL_PERMISSION_TOOLS } from "./internal-tools.js";
|
|
|
13
13
|
import { handleNoUI } from "./non-interactive.js";
|
|
14
14
|
import { createBuiltinParserRegistry, permissionRequestsFor, toolOwnedPermissionRequests, } from "./parsers.js";
|
|
15
15
|
import { sharedPathIdentityResolver } from "./path-identity-host.js";
|
|
16
|
-
import { registerPermissionPresetReader } from "./preset-registry.js";
|
|
16
|
+
import { readDecisionPresetOverride, registerPermissionPresetReader } from "./preset-registry.js";
|
|
17
17
|
import { showPermissionPrompt } from "./prompt.js";
|
|
18
18
|
import { PermissionService } from "./service.js";
|
|
19
19
|
import { loadPermissionSettings } from "./settings.js";
|
|
@@ -184,7 +184,7 @@ export default function permissionSystemExtension(pi) {
|
|
|
184
184
|
activeSettingsManager = settingsManager;
|
|
185
185
|
const permissionFlag = pi.getFlag("permission");
|
|
186
186
|
loadedPermissionFlag = permissionFlag;
|
|
187
|
-
const permissionPresetFlag =
|
|
187
|
+
const permissionPresetFlag = decisionPresetFlag();
|
|
188
188
|
loadedPresetFlag = { value: permissionPresetFlag };
|
|
189
189
|
cliRuleset = typeof permissionFlag === "string" ? parsePermissionFlag(permissionFlag) : [];
|
|
190
190
|
const cliPreset = typeof permissionPresetFlag === "string" ? parsePermissionPresetFlag(permissionPresetFlag) : undefined;
|
|
@@ -202,6 +202,13 @@ export default function permissionSystemExtension(pi) {
|
|
|
202
202
|
if (cellSession)
|
|
203
203
|
updateCellEnforcement(cellSession, staticRuleset, activePreset);
|
|
204
204
|
};
|
|
205
|
+
// The core's delegated-default override wins over a valid or absent preset flag; an invalid one keeps refusing.
|
|
206
|
+
const decisionPresetFlag = () => {
|
|
207
|
+
const configured = pi.getFlag("permission-preset");
|
|
208
|
+
if (typeof configured === "string" && !parsePermissionPresetFlag(configured))
|
|
209
|
+
return configured;
|
|
210
|
+
return (cellSession === undefined ? undefined : readDecisionPresetOverride(cellSession)) ?? configured;
|
|
211
|
+
};
|
|
205
212
|
const applyToolDenials = (onlyWhenNarrowed = false) => {
|
|
206
213
|
const allTools = pi.getAllTools().map((tool) => tool.name);
|
|
207
214
|
const disabledTools = disabled(allTools, staticRuleset);
|
|
@@ -215,8 +222,7 @@ export default function permissionSystemExtension(pi) {
|
|
|
215
222
|
// the rules follow from the next tool call on, and a failed reload refuses calls like session_start.
|
|
216
223
|
const followPresetFlag = (cwd, agentDir) => {
|
|
217
224
|
if (loadedPresetFlag === undefined ||
|
|
218
|
-
(loadedPresetFlag.value === pi.getFlag("permission
|
|
219
|
-
loadedPermissionFlag === pi.getFlag("permission"))) {
|
|
225
|
+
(loadedPresetFlag.value === decisionPresetFlag() && loadedPermissionFlag === pi.getFlag("permission"))) {
|
|
220
226
|
return;
|
|
221
227
|
}
|
|
222
228
|
const sessionApproved = service ? service.getSessionApproved() : [];
|
|
@@ -8,5 +8,7 @@ type PresetReader = () => PermissionPresetName | undefined;
|
|
|
8
8
|
export declare function registerPermissionPresetReader(session: object, read: PresetReader): () => void;
|
|
9
9
|
/** `undefined` when no permission system runs for the session, or its setup failed. */
|
|
10
10
|
export declare function readPermissionPreset(session: object): PermissionPresetName | undefined;
|
|
11
|
+
export declare function registerDecisionPresetOverride(session: object, read: PresetReader): void;
|
|
12
|
+
export declare function readDecisionPresetOverride(session: object): PermissionPresetName | undefined;
|
|
11
13
|
export {};
|
|
12
14
|
//# sourceMappingURL=preset-registry.d.ts.map
|
|
@@ -10,4 +10,15 @@ export function registerPermissionPresetReader(session, read) {
|
|
|
10
10
|
export function readPermissionPreset(session) {
|
|
11
11
|
return readers.get(session)?.();
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* The preset a gated decision must use instead of the configured one, read at EACH decision (the core's
|
|
15
|
+
* delegated-default rule: `ask` for delegated-origin work on a session no client named a mode for).
|
|
16
|
+
*/
|
|
17
|
+
const decisionOverrides = new WeakMap();
|
|
18
|
+
export function registerDecisionPresetOverride(session, read) {
|
|
19
|
+
decisionOverrides.set(session, read);
|
|
20
|
+
}
|
|
21
|
+
export function readDecisionPresetOverride(session) {
|
|
22
|
+
return decisionOverrides.get(session)?.();
|
|
23
|
+
}
|
|
13
24
|
//# sourceMappingURL=preset-registry.js.map
|
|
@@ -17,4 +17,6 @@ export declare function loadPermissionSettings(settingsManager: SettingsManager,
|
|
|
17
17
|
approved: Ruleset;
|
|
18
18
|
preset: PermissionPresetName;
|
|
19
19
|
};
|
|
20
|
+
/** The preset `loadPermissionSettings` would enforce, without loading rules; throws on an invalid settings preset. */
|
|
21
|
+
export declare function resolvePermissionPreset(settingsManager: SettingsManager, cliPresetOverride?: PermissionPresetName): PermissionPresetName;
|
|
20
22
|
//# sourceMappingURL=settings.d.ts.map
|
|
@@ -27,9 +27,18 @@ export function loadPermissionSettings(settingsManager, cliOverride, projectDir,
|
|
|
27
27
|
const cliPresetRuleset = cliPresetOverride && cliPresetOverride !== DEFAULT_PERMISSION_PRESET ? rulesForPreset(cliPresetOverride) : [];
|
|
28
28
|
const staticRuleset = merge(rulesForPreset(DEFAULT_PERMISSION_PRESET), globalPresetRuleset, globalRuleset, projectPresetRuleset, projectRuleset, cliPresetRuleset, cliOverride);
|
|
29
29
|
const approved = agentDir === undefined ? [] : loadApproved(agentDir, projectDir);
|
|
30
|
-
const preset = cliPresetOverride
|
|
30
|
+
const preset = pickPermissionPreset(cliPresetOverride, projectPreset, globalPreset);
|
|
31
31
|
return { staticRuleset, approved, preset };
|
|
32
32
|
}
|
|
33
|
+
function pickPermissionPreset(cliPreset, projectPreset, globalPreset) {
|
|
34
|
+
return cliPreset ?? projectPreset ?? globalPreset ?? DEFAULT_PERMISSION_PRESET;
|
|
35
|
+
}
|
|
36
|
+
/** The preset `loadPermissionSettings` would enforce, without loading rules; throws on an invalid settings preset. */
|
|
37
|
+
export function resolvePermissionPreset(settingsManager, cliPresetOverride) {
|
|
38
|
+
const globalSettings = settingsManager.getGlobalSettings();
|
|
39
|
+
const projectSettings = settingsManager.getProjectSettings();
|
|
40
|
+
return pickPermissionPreset(cliPresetOverride, parseSettingsPreset(projectSettings.permissionPreset, "project"), parseSettingsPreset(globalSettings.permissionPreset, "global"));
|
|
41
|
+
}
|
|
33
42
|
function parseSettingsPreset(value, scope) {
|
|
34
43
|
if (value === undefined) {
|
|
35
44
|
return undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Age notices of still-running background sessions (senpi#6201): one at 10 minutes, then one every 30 minutes (10, 40,
|
|
3
|
+
* 70, ...). One schedule per session bundle serves every session with ONE unref'd timer armed for the earliest due
|
|
4
|
+
* notice, so it never keeps the process alive and costs nothing per session; it is cleared when the last session
|
|
5
|
+
* leaves and on dispose. A late timer (the machine slept) or a session registered after it started reports at most
|
|
6
|
+
* once for the milestones it missed, never a burst.
|
|
7
|
+
*/
|
|
8
|
+
import type { TerminalBackgroundAgeEvent } from "../monitor-state-event.ts";
|
|
9
|
+
export interface BackgroundAgeSubject {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly description: string;
|
|
12
|
+
readonly command?: string;
|
|
13
|
+
readonly startedAtMs: number;
|
|
14
|
+
readonly autoReapMinutes?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function backgroundAgeEvent(kind: TerminalBackgroundAgeEvent["kind"], subject: BackgroundAgeSubject, nowMs: number): TerminalBackgroundAgeEvent;
|
|
17
|
+
export declare class BackgroundAgeSchedule {
|
|
18
|
+
#private;
|
|
19
|
+
constructor(onDue: (event: TerminalBackgroundAgeEvent) => void);
|
|
20
|
+
add(subject: BackgroundAgeSubject): void;
|
|
21
|
+
get(id: string): BackgroundAgeSubject | undefined;
|
|
22
|
+
remove(id: string): BackgroundAgeSubject | undefined;
|
|
23
|
+
dispose(): void;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=background-age.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const MINUTE_MS = 60_000;
|
|
2
|
+
const BACKGROUND_AGE_FIRST_MS = 10 * MINUTE_MS;
|
|
3
|
+
const BACKGROUND_AGE_REPEAT_MS = 30 * MINUTE_MS;
|
|
4
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
5
|
+
/** The first age milestone strictly after `ageMs`. */
|
|
6
|
+
function nextAgeMilestoneMs(ageMs) {
|
|
7
|
+
if (ageMs < BACKGROUND_AGE_FIRST_MS)
|
|
8
|
+
return BACKGROUND_AGE_FIRST_MS;
|
|
9
|
+
const repeats = Math.floor((ageMs - BACKGROUND_AGE_FIRST_MS) / BACKGROUND_AGE_REPEAT_MS) + 1;
|
|
10
|
+
return BACKGROUND_AGE_FIRST_MS + repeats * BACKGROUND_AGE_REPEAT_MS;
|
|
11
|
+
}
|
|
12
|
+
export function backgroundAgeEvent(kind, subject, nowMs) {
|
|
13
|
+
const ageMs = Math.max(0, nowMs - subject.startedAtMs);
|
|
14
|
+
return {
|
|
15
|
+
kind,
|
|
16
|
+
id: subject.id,
|
|
17
|
+
description: subject.description,
|
|
18
|
+
...(subject.command === undefined ? {} : { command: subject.command }),
|
|
19
|
+
startedAtMs: subject.startedAtMs,
|
|
20
|
+
ageMs,
|
|
21
|
+
ageMinutes: Math.floor(ageMs / MINUTE_MS),
|
|
22
|
+
autoReapMinutes: subject.autoReapMinutes ?? null,
|
|
23
|
+
stop: { tool: "kill_bash", args: { bash_id: subject.id } },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export class BackgroundAgeSchedule {
|
|
27
|
+
#entries = new Map();
|
|
28
|
+
#onDue;
|
|
29
|
+
#timer;
|
|
30
|
+
constructor(onDue) {
|
|
31
|
+
this.#onDue = onDue;
|
|
32
|
+
}
|
|
33
|
+
add(subject) {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
this.#entries.set(subject.id, {
|
|
36
|
+
subject,
|
|
37
|
+
dueAtMs: subject.startedAtMs + nextAgeMilestoneMs(now - subject.startedAtMs),
|
|
38
|
+
});
|
|
39
|
+
this.#arm(now);
|
|
40
|
+
}
|
|
41
|
+
get(id) {
|
|
42
|
+
return this.#entries.get(id)?.subject;
|
|
43
|
+
}
|
|
44
|
+
remove(id) {
|
|
45
|
+
const entry = this.#entries.get(id);
|
|
46
|
+
if (!entry)
|
|
47
|
+
return undefined;
|
|
48
|
+
this.#entries.delete(id);
|
|
49
|
+
this.#arm(Date.now());
|
|
50
|
+
return entry.subject;
|
|
51
|
+
}
|
|
52
|
+
dispose() {
|
|
53
|
+
this.#entries.clear();
|
|
54
|
+
this.#clearTimer();
|
|
55
|
+
}
|
|
56
|
+
#fire() {
|
|
57
|
+
this.#timer = undefined;
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const due = [];
|
|
60
|
+
for (const entry of this.#entries.values()) {
|
|
61
|
+
if (entry.dueAtMs > now)
|
|
62
|
+
continue;
|
|
63
|
+
const ageMs = now - entry.subject.startedAtMs;
|
|
64
|
+
due.push(backgroundAgeEvent("running", entry.subject, now));
|
|
65
|
+
entry.dueAtMs = entry.subject.startedAtMs + nextAgeMilestoneMs(ageMs);
|
|
66
|
+
}
|
|
67
|
+
this.#arm(now);
|
|
68
|
+
for (const event of due)
|
|
69
|
+
this.#onDue(event);
|
|
70
|
+
}
|
|
71
|
+
#arm(now) {
|
|
72
|
+
this.#clearTimer();
|
|
73
|
+
let earliest;
|
|
74
|
+
for (const entry of this.#entries.values()) {
|
|
75
|
+
if (earliest === undefined || entry.dueAtMs < earliest)
|
|
76
|
+
earliest = entry.dueAtMs;
|
|
77
|
+
}
|
|
78
|
+
if (earliest === undefined)
|
|
79
|
+
return;
|
|
80
|
+
const timer = setTimeout(() => this.#fire(), Math.min(Math.max(0, earliest - now), MAX_TIMER_MS));
|
|
81
|
+
timer.unref?.();
|
|
82
|
+
this.#timer = timer;
|
|
83
|
+
}
|
|
84
|
+
#clearTimer() {
|
|
85
|
+
if (this.#timer === undefined)
|
|
86
|
+
return;
|
|
87
|
+
clearTimeout(this.#timer);
|
|
88
|
+
this.#timer = undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=background-age.js.map
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "../../types.ts";
|
|
3
3
|
import type { MonitorNotifier } from "./monitor-notify.ts";
|
|
4
4
|
import type { MonitorStatusTicker } from "./monitor-status-ticker.ts";
|
|
5
|
-
import type
|
|
5
|
+
import { type TerminalNotifier } from "./notify.ts";
|
|
6
6
|
import type { PersistenceState } from "./restore-session.ts";
|
|
7
7
|
import { type TerminalEventSinks, TerminalSessionBundle } from "./session-bundle.ts";
|
|
8
8
|
import type { ResolvedTerminalSettings } from "./settings.ts";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** The terminal extension's per-generation state, the bundle event sinks it binds, and the tool context over it. */
|
|
2
2
|
import { getShellEnv } from "../../../../utils/shell.js";
|
|
3
|
-
import { TERMINAL_MONITOR_ENDED_EVENT, TERMINAL_MONITOR_STATE_EVENT, WAKE_SOURCE_STATE_EVENT, } from "../monitor-state-event.js";
|
|
3
|
+
import { TERMINAL_BACKGROUND_AGE_EVENT, TERMINAL_MONITOR_ENDED_EVENT, TERMINAL_MONITOR_STATE_EVENT, WAKE_SOURCE_STATE_EVENT, } from "../monitor-state-event.js";
|
|
4
4
|
import { withTerminalContext } from "./context-lifetime.js";
|
|
5
5
|
import { tapBackgroundSession, untapBackgroundSession } from "./job-widget-tap.js";
|
|
6
6
|
import { removeMonitorStateDir, terminalStateDir } from "./monitor-state-dir.js";
|
|
7
|
+
import { describeBackgroundAge } from "./notify.js";
|
|
7
8
|
import { TerminalSessionBundle } from "./session-bundle.js";
|
|
8
9
|
/** Tests and SDK callers may hand partial contexts without a session manager. */
|
|
9
10
|
export function sessionKeyOf(ctx) {
|
|
@@ -101,6 +102,14 @@ export function bundleSinks(pi, state) {
|
|
|
101
102
|
}
|
|
102
103
|
record();
|
|
103
104
|
},
|
|
105
|
+
onBackgroundAge: (event) => {
|
|
106
|
+
pi.events?.emit(TERMINAL_BACKGROUND_AGE_EVENT, event);
|
|
107
|
+
pi.rpc?.emit(TERMINAL_BACKGROUND_AGE_EVENT, event);
|
|
108
|
+
withTerminalContext(() => state.ctx?.ui?.notify?.(describeBackgroundAge(event), event.kind === "running" ? "info" : "warning"), retire);
|
|
109
|
+
// A reap reaches the model through the session's completion notice, which names the reap.
|
|
110
|
+
if (event.kind !== "reaped")
|
|
111
|
+
state.notifier?.notifyAge(event);
|
|
112
|
+
},
|
|
104
113
|
onBackgroundExit: (id, runtime) => {
|
|
105
114
|
state.recordedBackgroundIds.delete(id);
|
|
106
115
|
if (state.manifestWriter)
|
|
@@ -146,6 +155,9 @@ export function buildToolContext(pi, state) {
|
|
|
146
155
|
get maxDurableMonitors() {
|
|
147
156
|
return state.settings.maxDurableMonitors;
|
|
148
157
|
},
|
|
158
|
+
get backgroundAutoReapMinutes() {
|
|
159
|
+
return state.settings.backgroundAutoReapMinutes;
|
|
160
|
+
},
|
|
149
161
|
get monitorRegistry() {
|
|
150
162
|
return requireBundle().monitors;
|
|
151
163
|
},
|
|
@@ -153,8 +165,11 @@ export function buildToolContext(pi, state) {
|
|
|
153
165
|
getSessionContext: () => state.ctx,
|
|
154
166
|
// Exit listeners registered before a reload reach the post-reload owner through the
|
|
155
167
|
// shared bundle, so this must dispatch via the bundle, never the instance notifier.
|
|
156
|
-
onBackgroundStart: (id, description, startedAtMs, command) => {
|
|
157
|
-
state.bundle?.notifyBackgroundStart(id, description, startedAtMs
|
|
168
|
+
onBackgroundStart: (id, description, startedAtMs, command, autoReapMinutes) => {
|
|
169
|
+
state.bundle?.notifyBackgroundStart(id, description, startedAtMs, {
|
|
170
|
+
...(command === undefined ? {} : { command }),
|
|
171
|
+
...(autoReapMinutes === undefined ? {} : { autoReapMinutes }),
|
|
172
|
+
});
|
|
158
173
|
const runtime = state.bundle?.manager.get(id);
|
|
159
174
|
if (runtime) {
|
|
160
175
|
const key = withTerminalContext(() => sessionKeyOf(state.ctx), () => { });
|
|
@@ -168,6 +183,9 @@ export function buildToolContext(pi, state) {
|
|
|
168
183
|
onBackgroundExit: (id, runtime) => {
|
|
169
184
|
state.bundle?.notifyBackgroundExit(id, runtime);
|
|
170
185
|
},
|
|
186
|
+
onBackgroundReapFailed: (id, autoReapMinutes, reason) => {
|
|
187
|
+
state.bundle?.notifyBackgroundReapFailed(id, autoReapMinutes, reason);
|
|
188
|
+
},
|
|
171
189
|
onMonitorRearmed: (id) => state.monitorNotifier?.rearm(id),
|
|
172
190
|
onMonitorsResumed: (ids) => state.monitorNotifier?.resume(ids),
|
|
173
191
|
ensurePersistence: () => state.ensurePersistence?.() ?? Promise.resolve(),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "../../types.ts";
|
|
2
|
+
import type { TerminalBackgroundAgeEvent } from "../monitor-state-event.ts";
|
|
2
3
|
import type { NotifyMode } from "./settings.ts";
|
|
3
4
|
import type { TerminalExitView } from "./tools/context.ts";
|
|
4
5
|
/** Modes that never wake the agent: one-shot, non-interactive runs. */
|
|
@@ -28,6 +29,8 @@ export interface TerminalNotificationDelivery {
|
|
|
28
29
|
}
|
|
29
30
|
/** Shared terminal-notification guard and notify-mode mapping. */
|
|
30
31
|
export declare function getTerminalNotificationDelivery(deps: TerminalNotifierDeps, customType?: string): TerminalNotificationDelivery | undefined;
|
|
32
|
+
/** One plain sentence (or two) for the TUI and the model: which session, how old, and how it stops. */
|
|
33
|
+
export declare function describeBackgroundAge(event: TerminalBackgroundAgeEvent): string;
|
|
31
34
|
/**
|
|
32
35
|
* Notifies an interactive agent once when a background session completes.
|
|
33
36
|
*
|
|
@@ -41,6 +44,9 @@ export declare class TerminalNotifier {
|
|
|
41
44
|
private retired;
|
|
42
45
|
constructor(deps: TerminalNotifierDeps);
|
|
43
46
|
notifyCompletion(id: string, runtime: TerminalExitView): void;
|
|
47
|
+
/** A still-running session's age notice (senpi#6201), delivered like a completion notice. */
|
|
48
|
+
notifyAge(event: TerminalBackgroundAgeEvent): void;
|
|
49
|
+
private retire;
|
|
44
50
|
private deliverCompletion;
|
|
45
51
|
}
|
|
46
52
|
//# sourceMappingURL=notify.d.ts.map
|
|
@@ -27,7 +27,34 @@ export function getTerminalNotificationDelivery(deps, customType = TERMINAL_NOTI
|
|
|
27
27
|
}),
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
function minutes(count) {
|
|
31
|
+
return `${count} minute${count === 1 ? "" : "s"}`;
|
|
32
|
+
}
|
|
33
|
+
function killCall(id) {
|
|
34
|
+
return `kill_bash({ bash_id: "${id}" })`;
|
|
35
|
+
}
|
|
36
|
+
/** One plain sentence (or two) for the TUI and the model: which session, how old, and how it stops. */
|
|
37
|
+
export function describeBackgroundAge(event) {
|
|
38
|
+
const label = event.command === undefined || event.command === event.description
|
|
39
|
+
? event.description
|
|
40
|
+
: `${event.description}: ${event.command}`;
|
|
41
|
+
const session = `Background terminal session ${event.id} (${label})`;
|
|
42
|
+
if (event.kind === "reaped") {
|
|
43
|
+
return `${session} was stopped automatically after ${minutes(event.autoReapMinutes ?? event.ageMinutes)} (auto-reap).`;
|
|
44
|
+
}
|
|
45
|
+
if (event.kind === "reap_failed") {
|
|
46
|
+
return `${session} is still running: auto-reap at ${minutes(event.autoReapMinutes ?? event.ageMinutes)} failed (${event.reason ?? "unknown reason"}) and will not retry; stop it with ${killCall(event.id)} if it is no longer needed.`;
|
|
47
|
+
}
|
|
48
|
+
const reap = event.autoReapMinutes === null
|
|
49
|
+
? "Nothing stops it automatically"
|
|
50
|
+
: `Auto-reap stops it at ${minutes(event.autoReapMinutes)}`;
|
|
51
|
+
return `${session} has been running for ${minutes(event.ageMinutes)}. ${reap}; stop it now with ${killCall(event.id)} if it is no longer needed.`;
|
|
52
|
+
}
|
|
30
53
|
function buildNotice(id, runtime, trustText) {
|
|
54
|
+
const reaped = runtime.autoReapedAfterMinutes;
|
|
55
|
+
const reapText = reaped === undefined
|
|
56
|
+
? ""
|
|
57
|
+
: ` It was stopped automatically after ${minutes(reaped)} (auto-reap: the auto_reap_minutes parameter or the terminal.backgroundAutoReapMinutes setting).`;
|
|
31
58
|
const status = describeExit(runtime) ?? "exited";
|
|
32
59
|
const code = runtime.exitResult?.exitCode;
|
|
33
60
|
const codeText = code === null || code === undefined ? "" : ` (exit code ${code})`;
|
|
@@ -42,7 +69,7 @@ function buildNotice(id, runtime, trustText) {
|
|
|
42
69
|
tailSection = `\nFinal output:\n${shown}${note}`;
|
|
43
70
|
}
|
|
44
71
|
const trustSection = trustText === undefined ? "" : `\n${trustText}`;
|
|
45
|
-
return `<system-reminder>Background terminal session ${id} finished: ${status}${codeText}.${tailSection}${trustSection}</system-reminder>`;
|
|
72
|
+
return `<system-reminder>Background terminal session ${id} finished: ${status}${codeText}.${reapText}${tailSection}${trustSection}</system-reminder>`;
|
|
46
73
|
}
|
|
47
74
|
/**
|
|
48
75
|
* Notifies an interactive agent once when a background session completes.
|
|
@@ -60,10 +87,17 @@ export class TerminalNotifier {
|
|
|
60
87
|
notifyCompletion(id, runtime) {
|
|
61
88
|
if (this.retired || this.notified.has(id))
|
|
62
89
|
return;
|
|
63
|
-
withTerminalContext(() => this.deliverCompletion(id, runtime), () =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
withTerminalContext(() => this.deliverCompletion(id, runtime), () => this.retire());
|
|
91
|
+
}
|
|
92
|
+
/** A still-running session's age notice (senpi#6201), delivered like a completion notice. */
|
|
93
|
+
notifyAge(event) {
|
|
94
|
+
if (this.retired || this.notified.has(event.id))
|
|
95
|
+
return;
|
|
96
|
+
withTerminalContext(() => getTerminalNotificationDelivery(this.deps)?.send(`<system-reminder>${describeBackgroundAge(event)}</system-reminder>`, { details: { backgroundAge: event } }), () => this.retire());
|
|
97
|
+
}
|
|
98
|
+
retire() {
|
|
99
|
+
this.retired = true;
|
|
100
|
+
this.notified.clear();
|
|
67
101
|
}
|
|
68
102
|
deliverCompletion(id, runtime) {
|
|
69
103
|
const delivery = getTerminalNotificationDelivery(this.deps);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WakeSourceStateItem } from "../monitor-state-event.ts";
|
|
1
|
+
import type { TerminalBackgroundAgeEvent, WakeSourceStateItem } from "../monitor-state-event.ts";
|
|
2
2
|
import { TerminalManager, type TerminalManagerOptions } from "./manager.ts";
|
|
3
3
|
import type { MonitorEndedEvent } from "./monitor-registry.ts";
|
|
4
4
|
import { type MonitorEvent, MonitorRegistry, type MonitorSnapshotEntry } from "./monitor-registry.ts";
|
|
@@ -9,6 +9,11 @@ export interface TerminalEventSinks {
|
|
|
9
9
|
readonly onMonitorEnded: (event: MonitorEndedEvent) => void;
|
|
10
10
|
readonly onBackgroundState: (snapshot: readonly WakeSourceStateItem[]) => void;
|
|
11
11
|
readonly onBackgroundExit: (id: string, runtime: TerminalExitView) => void;
|
|
12
|
+
readonly onBackgroundAge: (event: TerminalBackgroundAgeEvent) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface BackgroundStartDetails {
|
|
15
|
+
readonly command?: string;
|
|
16
|
+
readonly autoReapMinutes?: number;
|
|
12
17
|
}
|
|
13
18
|
/**
|
|
14
19
|
* The long-lived terminal runtime for one agent session: the PTY session manager plus
|
|
@@ -26,7 +31,9 @@ export declare class TerminalSessionBundle {
|
|
|
26
31
|
bind(sinks: TerminalEventSinks): void;
|
|
27
32
|
park(): void;
|
|
28
33
|
backgroundSnapshot(): readonly WakeSourceStateItem[];
|
|
29
|
-
notifyBackgroundStart(id: string, description: string, startedAtMs?: number): void;
|
|
34
|
+
notifyBackgroundStart(id: string, description: string, startedAtMs?: number, details?: BackgroundStartDetails): void;
|
|
35
|
+
/** The auto-reap's stop did not end a session still on the schedule: one notice; its age notices go on. */
|
|
36
|
+
notifyBackgroundReapFailed(id: string, autoReapMinutes: number, reason: string): void;
|
|
30
37
|
notifyBackgroundExit(id: string, runtime: TerminalExitView): void;
|
|
31
38
|
teardown(): Promise<void>;
|
|
32
39
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BackgroundAgeSchedule, backgroundAgeEvent } from "./background-age.js";
|
|
1
2
|
import { TerminalManager } from "./manager.js";
|
|
2
3
|
import { MonitorRegistry } from "./monitor-registry.js";
|
|
3
4
|
const MAX_PARKED_MONITOR_EVENTS = 100;
|
|
@@ -15,6 +16,9 @@ export class TerminalSessionBundle {
|
|
|
15
16
|
#parkedMonitorEndings = [];
|
|
16
17
|
#parkedExits = new Map();
|
|
17
18
|
#backgrounds = new Map();
|
|
19
|
+
#ages = new BackgroundAgeSchedule((event) => this.#dispatchAge(event));
|
|
20
|
+
/** Age events that came due while parked: only the newest per session is worth delivering. */
|
|
21
|
+
#parkedAges = new Map();
|
|
18
22
|
#torndown = false;
|
|
19
23
|
constructor(options) {
|
|
20
24
|
this.manager = new TerminalManager(options);
|
|
@@ -41,6 +45,8 @@ export class TerminalSessionBundle {
|
|
|
41
45
|
this.#parkedMonitorEndings = [];
|
|
42
46
|
const parkedExits = [...this.#parkedExits];
|
|
43
47
|
this.#parkedExits.clear();
|
|
48
|
+
const parkedAges = [...this.#parkedAges.values()];
|
|
49
|
+
this.#parkedAges.clear();
|
|
44
50
|
sinks.onMonitorState(this.monitors.snapshot());
|
|
45
51
|
sinks.onBackgroundState(this.backgroundSnapshot());
|
|
46
52
|
for (const event of parkedMonitorEvents)
|
|
@@ -49,6 +55,8 @@ export class TerminalSessionBundle {
|
|
|
49
55
|
sinks.onMonitorEnded(event);
|
|
50
56
|
for (const [id, runtime] of parkedExits)
|
|
51
57
|
sinks.onBackgroundExit(id, runtime);
|
|
58
|
+
for (const event of parkedAges)
|
|
59
|
+
sinks.onBackgroundAge(event);
|
|
52
60
|
}
|
|
53
61
|
park() {
|
|
54
62
|
this.#sinks = null;
|
|
@@ -56,15 +64,32 @@ export class TerminalSessionBundle {
|
|
|
56
64
|
backgroundSnapshot() {
|
|
57
65
|
return [...this.#backgrounds.values()];
|
|
58
66
|
}
|
|
59
|
-
notifyBackgroundStart(id, description, startedAtMs = Date.now()) {
|
|
67
|
+
notifyBackgroundStart(id, description, startedAtMs = Date.now(), details = {}) {
|
|
60
68
|
if (this.#torndown)
|
|
61
69
|
return;
|
|
62
70
|
this.#backgrounds.set(id, { id, description, startedAtMs });
|
|
71
|
+
this.#ages.add({ id, description, startedAtMs, ...details });
|
|
63
72
|
this.#sinks?.onBackgroundState(this.backgroundSnapshot());
|
|
64
73
|
}
|
|
74
|
+
/** The auto-reap's stop did not end a session still on the schedule: one notice; its age notices go on. */
|
|
75
|
+
notifyBackgroundReapFailed(id, autoReapMinutes, reason) {
|
|
76
|
+
if (this.#torndown)
|
|
77
|
+
return;
|
|
78
|
+
const subject = this.#ages.get(id);
|
|
79
|
+
if (!subject)
|
|
80
|
+
return;
|
|
81
|
+
const event = backgroundAgeEvent("reap_failed", { ...subject, autoReapMinutes }, Date.now());
|
|
82
|
+
this.#dispatchAge({ ...event, reason });
|
|
83
|
+
}
|
|
65
84
|
notifyBackgroundExit(id, runtime) {
|
|
66
85
|
if (this.#torndown)
|
|
67
86
|
return;
|
|
87
|
+
const subject = this.#ages.remove(id);
|
|
88
|
+
this.#parkedAges.delete(id);
|
|
89
|
+
if (subject && runtime.autoReapedAfterMinutes !== undefined) {
|
|
90
|
+
const reaped = { ...subject, autoReapMinutes: runtime.autoReapedAfterMinutes };
|
|
91
|
+
this.#dispatchAge(backgroundAgeEvent("reaped", reaped, Date.now()));
|
|
92
|
+
}
|
|
68
93
|
if (this.#backgrounds.delete(id))
|
|
69
94
|
this.#sinks?.onBackgroundState(this.backgroundSnapshot());
|
|
70
95
|
if (this.#sinks) {
|
|
@@ -80,6 +105,8 @@ export class TerminalSessionBundle {
|
|
|
80
105
|
return;
|
|
81
106
|
this.#parkedMonitorEvents = [];
|
|
82
107
|
this.#parkedExits.clear();
|
|
108
|
+
this.#ages.dispose();
|
|
109
|
+
this.#parkedAges.clear();
|
|
83
110
|
this.monitors.dispose();
|
|
84
111
|
if (this.#backgrounds.size > 0) {
|
|
85
112
|
this.#backgrounds.clear();
|
|
@@ -90,6 +117,14 @@ export class TerminalSessionBundle {
|
|
|
90
117
|
this.#sinks = null;
|
|
91
118
|
await this.manager.teardown();
|
|
92
119
|
}
|
|
120
|
+
#dispatchAge(event) {
|
|
121
|
+
if (this.#torndown)
|
|
122
|
+
return;
|
|
123
|
+
if (this.#sinks)
|
|
124
|
+
this.#sinks.onBackgroundAge(event);
|
|
125
|
+
else
|
|
126
|
+
this.#parkedAges.set(event.id, event);
|
|
127
|
+
}
|
|
93
128
|
#dispatchMonitorEvent(event) {
|
|
94
129
|
if (this.#torndown)
|
|
95
130
|
return;
|
|
@@ -27,6 +27,8 @@ export interface ResolvedTerminalSettings {
|
|
|
27
27
|
readonly monitorRunaway: MonitorRunawayGuard;
|
|
28
28
|
/** Admission cap on persistent (restart-surviving) monitors per session. */
|
|
29
29
|
readonly maxDurableMonitors: number | "unlimited";
|
|
30
|
+
/** Age in minutes at which a background session is stopped (senpi#6201); undefined means never (the default). */
|
|
31
|
+
readonly backgroundAutoReapMinutes: number | undefined;
|
|
30
32
|
}
|
|
31
33
|
export declare const TERMINAL_SETTINGS_DEFAULTS: ResolvedTerminalSettings;
|
|
32
34
|
/** Upper bound of a wake rate (setting or per-monitor override): one wake a second on average. */
|
|
@@ -20,6 +20,7 @@ export const TERMINAL_SETTINGS_DEFAULTS = {
|
|
|
20
20
|
minutes: DEFAULT_MONITOR_RUNAWAY_MINUTES,
|
|
21
21
|
},
|
|
22
22
|
maxDurableMonitors: "unlimited",
|
|
23
|
+
backgroundAutoReapMinutes: undefined,
|
|
23
24
|
};
|
|
24
25
|
/** Upper bound of a wake rate (setting or per-monitor override): one wake a second on average. */
|
|
25
26
|
export const MAX_MONITOR_WAKES_PER_HOUR = 3600;
|
|
@@ -42,6 +43,12 @@ function durableMonitorCap(value) {
|
|
|
42
43
|
return "unlimited";
|
|
43
44
|
return Math.trunc(value);
|
|
44
45
|
}
|
|
46
|
+
/** A whole number of minutes >= 1 turns auto-reap on; anything else (unset, 0, negative, non-numeric) leaves it off. */
|
|
47
|
+
function autoReapMinutes(value) {
|
|
48
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 1)
|
|
49
|
+
return undefined;
|
|
50
|
+
return Math.trunc(value);
|
|
51
|
+
}
|
|
45
52
|
/** Resolve terminal-tool config from a raw `terminal` settings block, filling defaults. */
|
|
46
53
|
export function resolveTerminalSettings(raw) {
|
|
47
54
|
if (!raw)
|
|
@@ -66,6 +73,7 @@ export function resolveTerminalSettings(raw) {
|
|
|
66
73
|
minutes: positiveInt(raw.monitorRunawayMinutes, TERMINAL_SETTINGS_DEFAULTS.monitorRunaway.minutes, 1440),
|
|
67
74
|
},
|
|
68
75
|
maxDurableMonitors: durableMonitorCap(raw.maxDurableMonitors),
|
|
76
|
+
backgroundAutoReapMinutes: autoReapMinutes(raw.backgroundAutoReapMinutes),
|
|
69
77
|
};
|
|
70
78
|
}
|
|
71
79
|
/** Load and merge terminal-tool settings from global + project settings.json. */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TerminalRuntimeSession } from "../runtime-session.ts";
|
|
2
|
+
import type { TerminalToolContext } from "./context.ts";
|
|
3
|
+
export interface BackgroundSessionStart {
|
|
4
|
+
readonly description: string;
|
|
5
|
+
readonly command: string;
|
|
6
|
+
readonly startedAtMs: number;
|
|
7
|
+
readonly autoReapMinutes: number | undefined;
|
|
8
|
+
}
|
|
9
|
+
export declare function backgroundStart(input: {
|
|
10
|
+
readonly command: string;
|
|
11
|
+
readonly description?: string;
|
|
12
|
+
readonly auto_reap_minutes?: number;
|
|
13
|
+
}, configuredMinutes: number | undefined, startedAtMs: number): BackgroundSessionStart;
|
|
14
|
+
export declare function trackBackgroundSession(ctx: TerminalToolContext, id: string, runtime: TerminalRuntimeSession, start: BackgroundSessionStart): void;
|
|
15
|
+
//# sourceMappingURL=background-auto-reap.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A session that runs on in the background (`run_in_background`, or a foreground command that auto-detached) is
|
|
3
|
+
* announced to the host and, only when opted into (senpi#6201), stopped at a configured age. The reap runs here, where
|
|
4
|
+
* the session's process group lives (this process, or the tool child), through `manager.stop`: the same TERM-then-KILL
|
|
5
|
+
* stop `kill_bash` uses. Its one timer is unref'd and cleared when the session exits; with no opt-in none is armed.
|
|
6
|
+
* A reap counts only when that stop resolved and the session really exited; a stop that rejects or leaves the session
|
|
7
|
+
* running is reported once (`onBackgroundReapFailed`) and never retried, and the session keeps running.
|
|
8
|
+
*/
|
|
9
|
+
import { getAgentDir } from "../../../../../config.js";
|
|
10
|
+
import { createSessionLogger } from "../../../../session-log.js";
|
|
11
|
+
const MINUTE_MS = 60_000;
|
|
12
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
13
|
+
/** The call's `auto_reap_minutes` wins over the setting; a whole number of minutes >= 1 reaps, 0 or less turns it off. */
|
|
14
|
+
function resolveAutoReapMinutes(requested, configured) {
|
|
15
|
+
const value = requested ?? configured;
|
|
16
|
+
if (value === undefined || !Number.isFinite(value))
|
|
17
|
+
return undefined;
|
|
18
|
+
const minutes = Math.trunc(value);
|
|
19
|
+
return minutes >= 1 ? minutes : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function backgroundStart(input, configuredMinutes, startedAtMs) {
|
|
22
|
+
return {
|
|
23
|
+
description: input.description ?? input.command,
|
|
24
|
+
command: input.command,
|
|
25
|
+
startedAtMs,
|
|
26
|
+
autoReapMinutes: resolveAutoReapMinutes(input.auto_reap_minutes, configuredMinutes),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function reapedView(runtime, minutes) {
|
|
30
|
+
return {
|
|
31
|
+
exitResult: runtime.exitResult,
|
|
32
|
+
autoReapedAfterMinutes: minutes,
|
|
33
|
+
fullOutput: () => runtime.fullOutput(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function trackBackgroundSession(ctx, id, runtime, start) {
|
|
37
|
+
ctx.onBackgroundStart?.(id, start.description, start.startedAtMs, start.command, start.autoReapMinutes);
|
|
38
|
+
const minutes = start.autoReapMinutes;
|
|
39
|
+
let reapedAfterMinutes;
|
|
40
|
+
/** The one reap attempt; the session's exit report waits for it, since the stop's own exit fires before it resolves. */
|
|
41
|
+
let reaping;
|
|
42
|
+
let timer;
|
|
43
|
+
const reap = async (age) => {
|
|
44
|
+
const failure = await ctx.manager.stop(id).then((stopped) => {
|
|
45
|
+
if (!stopped)
|
|
46
|
+
return `the terminal manager has no session ${id} to stop`;
|
|
47
|
+
return runtime.exited ? undefined : "the session was still running after the stop (TERM, then KILL)";
|
|
48
|
+
}, (error) => (error instanceof Error ? error.message : String(error)));
|
|
49
|
+
if (failure === undefined) {
|
|
50
|
+
reapedAfterMinutes = age;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const logger = ctx.sessionLogger ?? createSessionLogger(getAgentDir());
|
|
54
|
+
logger.warn("bash_auto_reap_failed", { bash_id: id, autoReapMinutes: age, error: failure });
|
|
55
|
+
if (!runtime.exited)
|
|
56
|
+
ctx.onBackgroundReapFailed?.(id, age, failure);
|
|
57
|
+
};
|
|
58
|
+
const arm = (deadlineMs) => {
|
|
59
|
+
timer = setTimeout(() => {
|
|
60
|
+
timer = undefined;
|
|
61
|
+
if (runtime.exited || minutes === undefined)
|
|
62
|
+
return;
|
|
63
|
+
if (Date.now() < deadlineMs) {
|
|
64
|
+
arm(deadlineMs);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Recorded before the stop starts: the exit it causes can fire from inside `stop` itself.
|
|
68
|
+
reaping = Promise.resolve().then(() => reap(minutes));
|
|
69
|
+
}, Math.min(Math.max(0, deadlineMs - Date.now()), MAX_TIMER_MS));
|
|
70
|
+
timer.unref?.();
|
|
71
|
+
};
|
|
72
|
+
if (minutes !== undefined && !runtime.exited)
|
|
73
|
+
arm(start.startedAtMs + minutes * MINUTE_MS);
|
|
74
|
+
if (minutes === undefined && !ctx.onBackgroundExit)
|
|
75
|
+
return;
|
|
76
|
+
runtime.session.onExit(() => {
|
|
77
|
+
if (timer !== undefined)
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
timer = undefined;
|
|
80
|
+
const report = () => ctx.onBackgroundExit?.(id, reapedAfterMinutes === undefined ? runtime : reapedView(runtime, reapedAfterMinutes));
|
|
81
|
+
if (reaping === undefined)
|
|
82
|
+
report();
|
|
83
|
+
else
|
|
84
|
+
void reaping.then(report);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=background-auto-reap.js.map
|