@code-yeongyu/senpi 2026.11.6-3 → 2026.11.9-2
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 +34 -0
- package/dist/bundle/chunks/{chunk-5LKB732P.js → chunk-3TZQXZKH.js} +1 -1
- package/dist/bundle/chunks/{chunk-X53PD4ZC.js → chunk-45MVD7BU.js} +1 -1
- package/dist/bundle/chunks/{chunk-L7ZIS3MC.js → chunk-4JIBEZZE.js} +3 -3
- package/dist/bundle/chunks/{chunk-CF5OGBC4.js → chunk-4NYNMAYF.js} +1 -1
- package/dist/bundle/chunks/{chunk-6CEDL5S2.js → chunk-4UR6UFBH.js} +1 -1
- package/dist/bundle/chunks/{chunk-F5JLDCYD.js → chunk-527Q2UYM.js} +5 -5
- package/dist/bundle/chunks/{chunk-7DRWPZ3N.js → chunk-622VYTRI.js} +1 -1
- package/dist/bundle/chunks/{chunk-V2QAYGOK.js → chunk-7CFORZPE.js} +22 -22
- 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-4BATXPB7.js → chunk-K3UHGDUQ.js} +1 -1
- package/dist/bundle/chunks/{chunk-MJTCZUXS.js → chunk-LHSWAJJL.js} +1 -1
- package/dist/bundle/chunks/{chunk-SB6MXNO5.js → chunk-M7GF73FO.js} +2 -2
- package/dist/bundle/chunks/{chunk-CFEHJ4VU.js → chunk-NDSWTAYJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-JMQOLOC2.js → chunk-Q6NR6644.js} +1 -1
- package/dist/bundle/chunks/{chunk-EO2VPZ2K.js → chunk-SGAFIMAU.js} +1 -1
- package/dist/bundle/chunks/{chunk-BASS47UD.js → chunk-STGOYIXC.js} +1 -1
- package/dist/bundle/chunks/{chunk-DSWDJ7HT.js → chunk-WQ4RBPMS.js} +1 -1
- package/dist/bundle/chunks/chunk-XZDCMDUH.js +2 -0
- package/dist/bundle/chunks/{chunk-LUZVGPQ7.js → chunk-Y5L4UFMI.js} +1 -1
- package/dist/bundle/chunks/{cli-main-RWHDZKTQ.js → cli-main-XBEFQW5A.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-G5LYF62Q.js → hand-entry-QFEY3JMQ.js} +1 -1
- package/dist/bundle/chunks/{headless-host-ES74ZB6M.js → headless-host-YXUGONR3.js} +1 -1
- package/dist/bundle/chunks/{host-command-I2JZ5JM7.js → host-command-VHX2ILLM.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-Y52NFPP2.js → host-session-command-P3MRW63Z.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-PA7DGVS6.js → interactive-mode-YGL3QHU6.js} +1 -1
- package/dist/bundle/chunks/{main-OBL6375C.js → main-5SD4DHMC.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-NM6I5BWZ.js → multi-session-host-PGRHY3JP.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-GNRQ5MVR.js → package-manager-cli-SADLU7QX.js} +1 -1
- package/dist/bundle/chunks/{role-F56BS2ZQ.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-G4KIP7XS.js → rpc-mode-27TC7W6K.js} +1 -1
- package/dist/bundle/chunks/{run-attached-tui-XTGPAGYM.js → run-attached-tui-MXCC5IZR.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-L4M3TSQ3.js → schedule-command-MX56D6UE.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-V46WMCDF.js → session-control-endpoint-IL6ERD4U.js} +1 -1
- package/dist/bundle/chunks/{session-picker-AG6KSR5A.js → session-picker-6VRC25MJ.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +44 -44
- 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-services.d.ts +2 -0
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session.d.ts +8 -0
- package/dist/core/agent-session.js +51 -4
- 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.js +13 -9
- 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/redact.d.ts +10 -0
- package/dist/core/egress/redact.js +134 -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 +5 -3
- package/dist/core/egress/wire.js +5 -3
- 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/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 +2 -0
- package/dist/core/extensions/builtin/terminal/tools/bash.js +6 -6
- 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 +11 -2
- package/dist/core/extensions/session-control-types.d.ts +15 -0
- package/dist/core/extensions/session-control-types.js +11 -0
- package/dist/core/external-admission.d.ts +16 -2
- package/dist/core/external-admission.js +22 -4
- package/dist/core/retry-fallback/controller.d.ts +1 -0
- package/dist/core/retry-fallback/controller.js +3 -0
- package/dist/core/retry-fallback/unusable.d.ts +2 -0
- package/dist/core/retry-fallback/unusable.js +7 -1
- package/dist/core/retry-fallback/usage-limit.d.ts +6 -1
- package/dist/core/retry-fallback/usage-limit.js +9 -4
- package/dist/core/sdk.d.ts +6 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/terminal-settings.d.ts +1 -0
- package/dist/core/turn-origin.d.ts +4 -0
- package/dist/core/turn-origin.js +10 -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/connection-handler.js +14 -0
- package/dist/modes/rpc/effect-commands.d.ts +1 -1
- package/dist/modes/rpc/host-idle-handover.js +1 -0
- 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 +27 -0
- package/dist/modes/rpc/method-classes-schema-session.js +2 -0
- package/dist/modes/rpc/method-classes-schema.d.ts +27 -0
- package/dist/modes/rpc/method-classes.d.ts +1 -0
- package/dist/modes/rpc/method-classes.js +2 -0
- package/dist/modes/rpc/rpc-input-validation.js +4 -3
- package/dist/modes/rpc/rpc-response-types.d.ts +2 -1
- package/dist/modes/rpc/rpc-session-command-types.d.ts +2 -1
- package/dist/modes/rpc/tool-child/terminal-jobs.d.ts +1 -0
- package/dist/modes/rpc/tool-child/terminal-jobs.js +17 -1
- package/docs/engine-socket/fixtures/admission-routes/h-legacy.json +1 -1
- package/docs/engine-socket/fixtures/rpc-inbound-samples.json +19 -2
- package/docs/rpc.md +57 -0
- 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-UODSYQ2D.js +0 -2
|
@@ -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
|
|
@@ -6,6 +6,7 @@ export declare const ptyBashSchema: Type.TObject<{
|
|
|
6
6
|
timeout: Type.TOptional<Type.TNumber>;
|
|
7
7
|
description: Type.TOptional<Type.TString>;
|
|
8
8
|
run_in_background: Type.TOptional<Type.TBoolean>;
|
|
9
|
+
auto_reap_minutes: Type.TOptional<Type.TNumber>;
|
|
9
10
|
cols: Type.TOptional<Type.TNumber>;
|
|
10
11
|
rows: Type.TOptional<Type.TNumber>;
|
|
11
12
|
}>;
|
|
@@ -50,6 +51,7 @@ export declare function createPtyBashTool(ctx: TerminalToolContext): {
|
|
|
50
51
|
timeout: Type.TOptional<Type.TNumber>;
|
|
51
52
|
description: Type.TOptional<Type.TString>;
|
|
52
53
|
run_in_background: Type.TOptional<Type.TBoolean>;
|
|
54
|
+
auto_reap_minutes: Type.TOptional<Type.TNumber>;
|
|
53
55
|
cols: Type.TOptional<Type.TNumber>;
|
|
54
56
|
rows: Type.TOptional<Type.TNumber>;
|
|
55
57
|
}>;
|
|
@@ -6,6 +6,7 @@ import { stallThresholdMs, watchForStall } from "../../../../stall-warning.js";
|
|
|
6
6
|
import { formatTerminalToolOutput } from "../output-format.js";
|
|
7
7
|
import { BACKGROUND_START_GRACE_MS, DEFAULT_COLS, DEFAULT_ROWS, FOREGROUND_ENV_OVERRIDES, KILLED_SESSION_EXIT_GRACE_MS, TERMINAL_BASH_TOOL, } from "../shared.js";
|
|
8
8
|
import { errorResult, noticedResult, textResult, } from "./context.js";
|
|
9
|
+
import { backgroundStart, trackBackgroundSession } from "./background-auto-reap.js";
|
|
9
10
|
import { createForegroundDetachGate } from "./foreground-detach.js";
|
|
10
11
|
import { resolveForegroundWindowSeconds, SLEEP_WAIT_WINDOW_SECONDS } from "./foreground-window.js";
|
|
11
12
|
import { classifySleepWait } from "./sleep-wait.js";
|
|
@@ -19,6 +20,9 @@ export const ptyBashSchema = Type.Object({
|
|
|
19
20
|
run_in_background: Type.Optional(Type.Boolean({
|
|
20
21
|
description: "Run as a persistent background session: returns a bash_id immediately (read with bash_output, steer with bash_input, stop with kill_bash). A plain & job dies with the call's PTY hangup (native backend), and nohup at best leaves an untracked orphan (no bash_id, no log) that the call's timeout still group-kills. This flag is the only reliable way a job outlives the call.",
|
|
21
22
|
})),
|
|
23
|
+
auto_reap_minutes: Type.Optional(Type.Number({
|
|
24
|
+
description: "Background sessions only: stop this session (TERM, then KILL to its process group) once it has run this many minutes. Defaults to the terminal.backgroundAutoReapMinutes setting, which is off; 0 turns it off for this call.",
|
|
25
|
+
})),
|
|
22
26
|
cols: Type.Optional(Type.Number({ description: "PTY width in columns (default 120)." })),
|
|
23
27
|
rows: Type.Optional(Type.Number({ description: "PTY height in rows (default 40)." })),
|
|
24
28
|
});
|
|
@@ -306,9 +310,7 @@ async function runForeground(ctx, input, cols, rows, signal, cwd, onUpdate, tool
|
|
|
306
310
|
}
|
|
307
311
|
if (outcome === "detached") {
|
|
308
312
|
scheduleDetachedSweep(ctx, id, runtime, timeoutMs, startedAt);
|
|
309
|
-
ctx
|
|
310
|
-
if (ctx.onBackgroundExit)
|
|
311
|
-
runtime.session.onExit(() => ctx.onBackgroundExit?.(id, runtime));
|
|
313
|
+
trackBackgroundSession(ctx, id, runtime, backgroundStart(input, ctx.backgroundAutoReapMinutes, startedAt));
|
|
312
314
|
const delta = runtime.readDelta();
|
|
313
315
|
const partialOutput = formatTerminalToolOutput(delta.text).text || "(no output yet)";
|
|
314
316
|
const timeoutNote = input.timeout !== undefined && Number.isFinite(input.timeout)
|
|
@@ -373,9 +375,7 @@ async function runBackground(ctx, input, cols, rows, cwd, toolCallId, execCtx) {
|
|
|
373
375
|
cwd,
|
|
374
376
|
envOverrides: sessionEnvOverrides(ctx, execCtx),
|
|
375
377
|
}, toolCallId, execCtx);
|
|
376
|
-
ctx
|
|
377
|
-
if (ctx.onBackgroundExit)
|
|
378
|
-
runtime.session.onExit(() => ctx.onBackgroundExit?.(id, runtime));
|
|
378
|
+
trackBackgroundSession(ctx, id, runtime, backgroundStart(input, ctx.backgroundAutoReapMinutes, Date.now()));
|
|
379
379
|
// Capture any output the command emits within a short grace window (or its exit).
|
|
380
380
|
await Promise.race([delay(BACKGROUND_START_GRACE_MS), runtime.session.waitExit()]);
|
|
381
381
|
const early = formatTerminalToolOutput(runtime.readDelta().text).text;
|
|
@@ -27,11 +27,19 @@ export type TerminalBackgroundWake = {
|
|
|
27
27
|
readonly id: string;
|
|
28
28
|
readonly description: string;
|
|
29
29
|
readonly startedAtMs: number;
|
|
30
|
+
readonly command?: string;
|
|
31
|
+
readonly autoReapMinutes?: number;
|
|
30
32
|
} | {
|
|
31
33
|
readonly event: "background_exit";
|
|
32
34
|
readonly id: string;
|
|
33
35
|
readonly exit: TerminalExitView["exitResult"];
|
|
34
36
|
readonly output: string;
|
|
37
|
+
readonly autoReapedAfterMinutes?: number;
|
|
38
|
+
} | {
|
|
39
|
+
readonly event: "background_reap_failed";
|
|
40
|
+
readonly id: string;
|
|
41
|
+
readonly autoReapMinutes: number;
|
|
42
|
+
readonly reason: string;
|
|
35
43
|
};
|
|
36
44
|
export declare function lostSessionResult(id: string, generation: number): AgentToolResult<unknown>;
|
|
37
45
|
export declare function terminalChildJob(ctx: TerminalToolContext, kind: (typeof TERMINAL_CHILD_JOB_KINDS)[keyof typeof TERMINAL_CHILD_JOB_KINDS]): ToolChildJob;
|