@code-yeongyu/senpi 2026.11.15-2 → 2026.11.15-3
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 +19 -0
- package/dist/bundle/chunks/{chunk-Q4RJ4WLE.js → chunk-225TDKTA.js} +4 -4
- package/dist/bundle/chunks/{chunk-QSU2456D.js → chunk-3KR3CDGM.js} +1 -1
- package/dist/bundle/chunks/{chunk-AOTHPTMU.js → chunk-5GCZFK4X.js} +2 -2
- package/dist/bundle/chunks/{chunk-BMADOJHZ.js → chunk-5QIFH624.js} +1 -1
- package/dist/bundle/chunks/{chunk-THYIV26N.js → chunk-GQEXLLWJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-3RILB2OD.js → chunk-OJOPOUAY.js} +9 -9
- package/dist/bundle/chunks/{chunk-64FJ4QQJ.js → chunk-PPCAJQDJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-74HCKLRH.js → chunk-TEXE4HT6.js} +1 -1
- package/dist/bundle/chunks/{chunk-RRFDLGX6.js → chunk-TNYFCGI7.js} +156 -156
- package/dist/bundle/chunks/{cli-main-GNQY2LTU.js → cli-main-CV3ODX4A.js} +1 -1
- package/dist/bundle/chunks/{hand-entry-D3DNH6XL.js → hand-entry-2NN7ZTY6.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-ATBPY4M5.js → interactive-mode-TGHZWM2T.js} +1 -1
- package/dist/bundle/chunks/{main-OMFVSMUR.js → main-B7WEY4JW.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-ACQ5N4UB.js → multi-session-host-53YLL2OD.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-2VMRV4HL.js → package-manager-cli-2U54AVKW.js} +1 -1
- package/dist/bundle/chunks/{role-BTH5PVME.js → role-4IXZZAL4.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-XUK5SYKH.js → rpc-mode-RRYXENML.js} +1 -1
- package/dist/bundle/chunks/{runtime-session-VO3X4OE3.js → runtime-session-KKHEYZOH.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-Y5EIFFPY.js → session-control-endpoint-HVULSIUR.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +331 -331
- 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.d.ts +6 -0
- package/dist/core/agent-session.js +34 -3
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.d.ts +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/failover.js +22 -4
- package/dist/core/extensions/builtin/anthropic-subscription/index.js +7 -2
- package/dist/core/extensions/builtin/anthropic-subscription/limit-recheck.d.ts +15 -0
- package/dist/core/extensions/builtin/anthropic-subscription/limit-recheck.js +55 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-stream.d.ts +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-stream.js +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +8 -5
- package/dist/core/extensions/builtin/anthropic-subscription/stream.d.ts +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/stream.js +3 -1
- package/dist/core/extensions/builtin/terminal/extension-state.js +15 -0
- package/dist/core/extensions/builtin/terminal/notify.d.ts +2 -1
- package/dist/core/extensions/builtin/terminal/notify.js +15 -6
- package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +5 -1
- package/dist/core/extensions/builtin/terminal/session-bundle.js +41 -1
- package/dist/core/extensions/builtin/terminal/tools/context.d.ts +4 -0
- package/dist/core/extensions/loader.js +5 -0
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.js +5 -0
- package/dist/core/extensions/types.d.ts +7 -0
- package/dist/core/run-background-jobs.d.ts +33 -0
- package/dist/core/run-background-jobs.js +47 -0
- package/dist/core/tools/bash-core.d.ts +50 -0
- package/dist/core/tools/bash-core.js +169 -0
- package/dist/core/tools/bash-schema.d.ts +48 -0
- package/dist/core/tools/bash-schema.js +34 -0
- package/dist/core/tools/bash.d.ts +4 -70
- package/dist/core/tools/bash.js +32 -409
- package/dist/core/tools/coding-tools-core.d.ts +24 -0
- package/dist/core/tools/coding-tools-core.js +14 -0
- package/dist/core/tools/edit-core.d.ts +56 -0
- package/dist/core/tools/edit-core.js +175 -0
- package/dist/core/tools/edit-diff.d.ts +0 -5
- package/dist/core/tools/edit-diff.js +0 -32
- package/dist/core/tools/edit-preview.d.ts +7 -0
- package/dist/core/tools/edit-preview.js +34 -0
- package/dist/core/tools/edit.d.ts +5 -43
- package/dist/core/tools/edit.js +10 -158
- package/dist/core/tools/encoding.d.ts +10 -0
- package/dist/core/tools/encoding.js +58 -0
- package/dist/core/tools/file-tool-host.d.ts +25 -0
- package/dist/core/tools/file-tool-host.js +109 -0
- package/dist/core/tools/fs-error-code.d.ts +2 -0
- package/dist/core/tools/fs-error-code.js +15 -0
- package/dist/core/tools/machine-output-spill.d.ts +11 -0
- package/dist/core/tools/machine-output-spill.js +65 -0
- package/dist/core/tools/machine.d.ts +4 -24
- package/dist/core/tools/machine.js +4 -44
- package/dist/core/tools/output-accumulator-core.d.ts +84 -0
- package/dist/core/tools/output-accumulator-core.js +200 -0
- package/dist/core/tools/output-accumulator.d.ts +9 -61
- package/dist/core/tools/output-accumulator.js +90 -265
- package/dist/core/tools/path-utils.js +3 -48
- package/dist/core/tools/posix-signals.d.ts +3 -0
- package/dist/core/tools/posix-signals.js +113 -0
- package/dist/core/tools/read-core.d.ts +64 -0
- package/dist/core/tools/read-core.js +180 -0
- package/dist/core/tools/read-text.d.ts +23 -0
- package/dist/core/tools/read-text.js +90 -0
- package/dist/core/tools/read.d.ts +6 -47
- package/dist/core/tools/read.js +13 -218
- package/dist/core/tools/renderers/bash.d.ts +0 -1
- package/dist/core/tools/renderers/bash.js +0 -1
- package/dist/core/tools/renderers/edit.d.ts +1 -1
- package/dist/core/tools/renderers/edit.js +1 -1
- package/dist/core/tools/secret-output-redactor.js +2 -1
- package/dist/core/tools/shell-operations.d.ts +53 -0
- package/dist/core/tools/shell-operations.js +115 -0
- package/dist/core/tools/shell-output.d.ts +20 -0
- package/dist/core/tools/shell-output.js +152 -0
- package/dist/core/tools/tail-window.js +5 -7
- package/dist/core/tools/tool-machine.d.ts +30 -0
- package/dist/core/tools/tool-machine.js +47 -0
- package/dist/core/tools/truncate.js +15 -14
- package/dist/core/tools/write-core.d.ts +47 -0
- package/dist/core/tools/write-core.js +108 -0
- package/dist/core/tools/write-result.d.ts +0 -1
- package/dist/core/tools/write-result.js +0 -13
- package/dist/core/tools/write.d.ts +6 -31
- package/dist/core/tools/write.js +25 -77
- package/dist/core/turn-origin.d.ts +4 -2
- package/dist/core/turn-origin.js +15 -8
- package/dist/modes/interactive/session-control-session-commands.js +1 -1
- package/dist/modes/rpc/connection-handler.js +2 -2
- package/dist/modes/rpc/effect-commands.d.ts +1 -4
- package/dist/modes/rpc/method-classes-schema-session.d.ts +1 -0
- package/dist/modes/rpc/method-classes-schema-session.js +5 -1
- package/dist/modes/rpc/method-classes-schema.d.ts +1 -0
- package/dist/modes/rpc/pause-command-validation.js +2 -0
- package/dist/modes/rpc/rpc-mutation-response-types.d.ts +2 -4
- package/dist/modes/rpc/rpc-session-command-types.d.ts +1 -0
- package/dist/modes/rpc/session-control-actions-data.d.ts +7 -3
- package/dist/modes/rpc/session-control-actions-data.js +12 -6
- package/dist/tools-core.d.ts +14 -0
- package/dist/tools-core.js +9 -0
- package/dist/utils/image-mime.d.ts +4 -0
- package/dist/utils/image-mime.js +99 -0
- package/dist/utils/mime.d.ts +1 -1
- package/dist/utils/mime.js +2 -97
- package/docs/extensions.md +7 -1
- package/docs/rpc.md +14 -0
- package/package.json +11 -7
- package/vendor/senpi-machine-ops/files/local-files.js +34 -5
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { subscriptionLimits } from "../../../subscription-limits/limit-store.js";
|
|
2
|
+
import { currentPersonTurn } from "../../../turn-origin.js";
|
|
3
|
+
/**
|
|
4
|
+
* One allowance per account on an admitted person's turn (senpi#6727). Its identity follows the actual message,
|
|
5
|
+
* not a predicted end event: a fresh run drops it even when the previous retry was cancelled or blocked.
|
|
6
|
+
*/
|
|
7
|
+
export function registerLimitRecheck(pi) {
|
|
8
|
+
let sessionManager;
|
|
9
|
+
let turn;
|
|
10
|
+
let attempted;
|
|
11
|
+
const reset = () => {
|
|
12
|
+
sessionManager = undefined;
|
|
13
|
+
turn = undefined;
|
|
14
|
+
attempted = undefined;
|
|
15
|
+
};
|
|
16
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
17
|
+
sessionManager = ctx.sessionManager;
|
|
18
|
+
});
|
|
19
|
+
pi.on("session_start", reset);
|
|
20
|
+
return {
|
|
21
|
+
accounts: () => {
|
|
22
|
+
const activeTurn = sessionManager === undefined ? undefined : currentPersonTurn(sessionManager);
|
|
23
|
+
if (activeTurn !== turn) {
|
|
24
|
+
turn = activeTurn;
|
|
25
|
+
attempted = turn === undefined ? undefined : new Set();
|
|
26
|
+
}
|
|
27
|
+
return attempted;
|
|
28
|
+
},
|
|
29
|
+
reset,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** A rate-limit block with no successful usage reading newer than the block. Errors are not usage evidence. */
|
|
33
|
+
export function unverifiedLimitBlock(account, now) {
|
|
34
|
+
if (account.blockReason !== "rate_limit" || (account.blockedUntil ?? 0) <= now)
|
|
35
|
+
return false;
|
|
36
|
+
const limits = subscriptionLimits
|
|
37
|
+
.snapshot()
|
|
38
|
+
.find((entry) => entry.provider === "anthropic-subscription" && entry.account === account.name);
|
|
39
|
+
return (account.blockedAt === undefined ||
|
|
40
|
+
limits === undefined ||
|
|
41
|
+
limits.windows.length === 0 ||
|
|
42
|
+
Date.parse(limits.reportedAt) <= account.blockedAt);
|
|
43
|
+
}
|
|
44
|
+
/** The selection view alone is opened. Persistent state and model-specific blocks remain authoritative. */
|
|
45
|
+
export function recheckSelection(accounts, attempted, now) {
|
|
46
|
+
if (attempted === undefined)
|
|
47
|
+
return accounts;
|
|
48
|
+
return accounts.map((account) => {
|
|
49
|
+
if (attempted.has(account.name) || !unverifiedLimitBlock(account, now))
|
|
50
|
+
return account;
|
|
51
|
+
const { blockReason: _reason, blockedUntil: _until, blockedAt: _at, ...available } = account;
|
|
52
|
+
return available;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=limit-recheck.js.map
|
|
@@ -7,6 +7,7 @@ export type ResidentSessionStreamInput = {
|
|
|
7
7
|
model: Model<Api>;
|
|
8
8
|
context: Context;
|
|
9
9
|
streamOptions: SimpleStreamOptions;
|
|
10
|
+
recheckedAccounts?: Set<string>;
|
|
10
11
|
providerSettings: AnthropicSubscriptionProviderSettings;
|
|
11
12
|
pinnedAccount?: string;
|
|
12
13
|
buildOptions: Parameters<typeof queryWithAuthLane>[0]["buildOptions"];
|
|
@@ -209,6 +209,7 @@ function residentAuthLaneMessages(input) {
|
|
|
209
209
|
prompt: "",
|
|
210
210
|
query: getSdkBoundary().query,
|
|
211
211
|
providerSettings: input.providerSettings,
|
|
212
|
+
recheckedAccounts: input.recheckedAccounts,
|
|
212
213
|
env: input.streamOptions.env,
|
|
213
214
|
signal: input.streamOptions.signal,
|
|
214
215
|
sessionId: input.streamOptions.affinitySessionId ?? input.streamOptions.sessionId,
|
|
@@ -13,9 +13,11 @@ export function withAuthGuidance(error, message, ran, now = Date.now()) {
|
|
|
13
13
|
? allAccountsBlockedGuidance(error.soonestUnblockAt, error.blockReason, error.accounts)
|
|
14
14
|
: singleAccountUnavailableGuidance(error.unavailableAccount, error.soonestUnblockAt, error.blockReason);
|
|
15
15
|
// The model-limit sentence is how the fallback chain learns the limit binds this model only (senpi#5007).
|
|
16
|
+
// A pool that recovers on its own names its soonest retry, so the session's retry waits for it instead of
|
|
17
|
+
// spending its budget on local refusals that cannot change before then (senpi#6727).
|
|
16
18
|
return error.limitedModel === undefined
|
|
17
|
-
? guidance
|
|
18
|
-
: `${modelLimitSentence(error, error.limitedModel, now)}\n${guidance}${retryAfterMarker(error, now)}`;
|
|
19
|
+
? `${guidance}${retryAfterMarker(error.rateLimitedUntil, now)}`
|
|
20
|
+
: `${modelLimitSentence(error, error.limitedModel, now)}\n${guidance}${retryAfterMarker(error.soonestUnblockAt, now)}`;
|
|
19
21
|
}
|
|
20
22
|
const kind = classifySdkError(error).kind;
|
|
21
23
|
const account = errorAccountContext(error instanceof ClassifiedSdkError ? error.original : error);
|
|
@@ -31,7 +33,9 @@ export function withAuthGuidance(error, message, ran, now = Date.now()) {
|
|
|
31
33
|
const pool = error instanceof ClassifiedSdkError ? error.poolState : undefined;
|
|
32
34
|
const poolGuidance = pool && exhaustedPoolGuidance(pool.soonestUnblockAt, pool.accounts);
|
|
33
35
|
const hints = [guidance, versionGuidance, poolGuidance].filter((hint) => hint !== undefined);
|
|
34
|
-
|
|
36
|
+
const text = hints.length > 0 ? `${message}\n${hints.join("\n")}` : message;
|
|
37
|
+
// The last account's failure left no account to try: the next one is free at the pool's soonest retry (senpi#6727).
|
|
38
|
+
return `${text}${retryAfterMarker(pool?.rateLimitedUntil, now)}`;
|
|
35
39
|
}
|
|
36
40
|
/**
|
|
37
41
|
* Names the limit, the model and its reset as relative prose, the form `usageLimitResetMs` (the
|
|
@@ -49,8 +53,7 @@ function modelLimitSentence(error, model, now) {
|
|
|
49
53
|
return `${subject} hit the usage limit for model ${model}; it resets in ${Math.floor(minutes / 60)} hours ${minutes % 60} minutes, at ${new Date(until).toISOString()}.`;
|
|
50
54
|
}
|
|
51
55
|
/** The session's retry-delay reader only takes the marker at the very end of the text. */
|
|
52
|
-
function retryAfterMarker(
|
|
53
|
-
const until = error.soonestUnblockAt;
|
|
56
|
+
function retryAfterMarker(until, now) {
|
|
54
57
|
return until === undefined || !Number.isFinite(until) ? "" : ` (retry-after-ms: ${Math.max(0, until - now)})`;
|
|
55
58
|
}
|
|
56
59
|
//# sourceMappingURL=stream-guidance.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type Api, type AssistantMessageEventStream, type Context, type Model, type SimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
2
2
|
export declare const NATIVE_COMPACTION_WHILE_SENPI_OWNS = "Claude Code compacted this session natively although senpi owns compaction on this lane (compactionOwner: \"senpi\"); the turn was stopped so only one side compacts. Run /compact, or set anthropicSubscriptionProvider.compactionOwner to \"sdk\" to let Claude Code compact.";
|
|
3
|
-
export declare function streamAnthropicSubscription(model: Model<Api>, context: Context, options?: SimpleStreamOptions): AssistantMessageEventStream;
|
|
3
|
+
export declare function streamAnthropicSubscription(model: Model<Api>, context: Context, options?: SimpleStreamOptions, recheckedAccounts?: Set<string>): AssistantMessageEventStream;
|
|
4
4
|
//# sourceMappingURL=stream.d.ts.map
|
|
@@ -27,7 +27,7 @@ import { emptyOutput, errorMessage, mapStopReason, updateUsage } from "./stream-
|
|
|
27
27
|
import { toolWatch } from "./tool-watch.js";
|
|
28
28
|
import { resolveSdkTools } from "./tools.js";
|
|
29
29
|
export const NATIVE_COMPACTION_WHILE_SENPI_OWNS = 'Claude Code compacted this session natively although senpi owns compaction on this lane (compactionOwner: "senpi"); the turn was stopped so only one side compacts. Run /compact, or set anthropicSubscriptionProvider.compactionOwner to "sdk" to let Claude Code compact.';
|
|
30
|
-
export function streamAnthropicSubscription(model, context, options) {
|
|
30
|
+
export function streamAnthropicSubscription(model, context, options, recheckedAccounts) {
|
|
31
31
|
const stream = createAssistantMessageEventStream();
|
|
32
32
|
void (async () => {
|
|
33
33
|
const output = emptyOutput(model);
|
|
@@ -166,6 +166,7 @@ export function streamAnthropicSubscription(model, context, options) {
|
|
|
166
166
|
model,
|
|
167
167
|
context,
|
|
168
168
|
streamOptions: options,
|
|
169
|
+
recheckedAccounts,
|
|
169
170
|
providerSettings,
|
|
170
171
|
pinnedAccount: getSessionClaudeAccountPin(options.sessionId),
|
|
171
172
|
buildOptions,
|
|
@@ -193,6 +194,7 @@ export function streamAnthropicSubscription(model, context, options) {
|
|
|
193
194
|
onAttemptStart,
|
|
194
195
|
providerSettings,
|
|
195
196
|
env: options?.env,
|
|
197
|
+
recheckedAccounts,
|
|
196
198
|
signal: options?.signal,
|
|
197
199
|
sessionId: affinityKey,
|
|
198
200
|
model: model.id,
|
|
@@ -118,6 +118,20 @@ export function bundleSinks(pi, state) {
|
|
|
118
118
|
},
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
|
+
/** A background session belongs to the turn that started it, so interrupting that turn with `background: "stop"` stops it. */
|
|
122
|
+
function recordRunJob(state, id, description) {
|
|
123
|
+
const bundle = state.bundle;
|
|
124
|
+
if (bundle === null)
|
|
125
|
+
return;
|
|
126
|
+
const release = withTerminalContext(() => state.ctx?.registerRunBackgroundJob?.({
|
|
127
|
+
kind: "bash",
|
|
128
|
+
id,
|
|
129
|
+
description,
|
|
130
|
+
stop: (turnId) => bundle.stopForInterrupt(id, turnId),
|
|
131
|
+
}), () => { });
|
|
132
|
+
if (release !== undefined)
|
|
133
|
+
bundle.trackRunJob(id, release);
|
|
134
|
+
}
|
|
121
135
|
export function buildToolContext(pi, state) {
|
|
122
136
|
const requireBundle = () => {
|
|
123
137
|
// Lazily create a bundle so the tools work even when invoked directly (e.g. via the
|
|
@@ -170,6 +184,7 @@ export function buildToolContext(pi, state) {
|
|
|
170
184
|
...(command === undefined ? {} : { command }),
|
|
171
185
|
...(autoReapMinutes === undefined ? {} : { autoReapMinutes }),
|
|
172
186
|
});
|
|
187
|
+
recordRunJob(state, id, description);
|
|
173
188
|
const runtime = state.bundle?.manager.get(id);
|
|
174
189
|
if (runtime) {
|
|
175
190
|
const key = withTerminalContext(() => sessionKeyOf(state.ctx), () => { });
|
|
@@ -14,7 +14,7 @@ export interface TerminalNotifierDeps {
|
|
|
14
14
|
details?: unknown;
|
|
15
15
|
}, options: {
|
|
16
16
|
triggerTurn: boolean;
|
|
17
|
-
deliverAs: "steer" | "followUp";
|
|
17
|
+
deliverAs: "steer" | "followUp" | "nextTurn";
|
|
18
18
|
}) => void;
|
|
19
19
|
readonly getContext: () => ExtensionContext | undefined;
|
|
20
20
|
readonly getMode: () => NotifyMode;
|
|
@@ -24,6 +24,7 @@ export declare const NOTICE_TAIL_MAX_CHARS = 2000;
|
|
|
24
24
|
export interface TerminalNotificationDelivery {
|
|
25
25
|
readonly send: (content: string, options?: {
|
|
26
26
|
readonly forceWake?: boolean;
|
|
27
|
+
readonly nextTurn?: boolean;
|
|
27
28
|
readonly details?: unknown;
|
|
28
29
|
}) => void;
|
|
29
30
|
}
|
|
@@ -21,10 +21,12 @@ export function getTerminalNotificationDelivery(deps, customType = TERMINAL_NOTI
|
|
|
21
21
|
content,
|
|
22
22
|
display: false,
|
|
23
23
|
...(options?.details === undefined ? {} : { details: options.details }),
|
|
24
|
-
},
|
|
25
|
-
triggerTurn:
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
}, options?.nextTurn === true
|
|
25
|
+
? { triggerTurn: false, deliverAs: "nextTurn" }
|
|
26
|
+
: {
|
|
27
|
+
triggerTurn: true,
|
|
28
|
+
deliverAs: mode === "wake" || options?.forceWake === true ? "steer" : "followUp",
|
|
29
|
+
}),
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
function minutes(count) {
|
|
@@ -69,7 +71,11 @@ function buildNotice(id, runtime, trustText) {
|
|
|
69
71
|
tailSection = `\nFinal output:\n${shown}${note}`;
|
|
70
72
|
}
|
|
71
73
|
const trustSection = trustText === undefined ? "" : `\n${trustText}`;
|
|
72
|
-
|
|
74
|
+
const interrupted = runtime.stoppedByInterrupt;
|
|
75
|
+
const ended = interrupted === undefined
|
|
76
|
+
? `finished: ${status}${codeText}.`
|
|
77
|
+
: `was stopped when turn ${interrupted.turnId} was interrupted (${status}${codeText}).`;
|
|
78
|
+
return `<system-reminder>Background terminal session ${id} ${ended}${reapText}${tailSection}${trustSection}</system-reminder>`;
|
|
73
79
|
}
|
|
74
80
|
/**
|
|
75
81
|
* Notifies an interactive agent once when a background session completes.
|
|
@@ -108,7 +114,10 @@ export class TerminalNotifier {
|
|
|
108
114
|
exitCode: runtime.exitResult?.exitCode ?? null,
|
|
109
115
|
output: runtime.fullOutput(),
|
|
110
116
|
});
|
|
111
|
-
delivery.send(buildNotice(id, runtime, annotation?.text),
|
|
117
|
+
delivery.send(buildNotice(id, runtime, annotation?.text), {
|
|
118
|
+
...(runtime.stoppedByInterrupt === undefined ? {} : { nextTurn: true }),
|
|
119
|
+
...(annotation === undefined ? {} : { details: { trust: annotation.trust } }),
|
|
120
|
+
});
|
|
112
121
|
}
|
|
113
122
|
}
|
|
114
123
|
//# sourceMappingURL=notify.js.map
|
|
@@ -32,9 +32,13 @@ export declare class TerminalSessionBundle {
|
|
|
32
32
|
park(): void;
|
|
33
33
|
backgroundSnapshot(): readonly WakeSourceStateItem[];
|
|
34
34
|
notifyBackgroundStart(id: string, description: string, startedAtMs?: number, details?: BackgroundStartDetails): void;
|
|
35
|
+
/** Hold the release of the run job `id` was recorded as; its exit drops it. */
|
|
36
|
+
trackRunJob(id: string, release: () => void): void;
|
|
37
|
+
/** Stop a session the interrupted turn `turnId` started; its exit notice then reaches the next turn without waking. */
|
|
38
|
+
stopForInterrupt(id: string, turnId: string): Promise<void>;
|
|
35
39
|
/** The auto-reap's stop did not end a session still on the schedule: one notice; its age notices go on. */
|
|
36
40
|
notifyBackgroundReapFailed(id: string, autoReapMinutes: number, reason: string): void;
|
|
37
|
-
notifyBackgroundExit(id: string,
|
|
41
|
+
notifyBackgroundExit(id: string, exited: TerminalExitView): void;
|
|
38
42
|
teardown(): Promise<void>;
|
|
39
43
|
}
|
|
40
44
|
/** Park a bundle across a reload; at most one parked bundle per session, older ones torn down. */
|
|
@@ -19,6 +19,9 @@ export class TerminalSessionBundle {
|
|
|
19
19
|
#ages = new BackgroundAgeSchedule((event) => this.#dispatchAge(event));
|
|
20
20
|
/** Age events that came due while parked: only the newest per session is worth delivering. */
|
|
21
21
|
#parkedAges = new Map();
|
|
22
|
+
/** Background sessions an interrupted turn's stop ended, by the turn id: their exit notice does not wake. */
|
|
23
|
+
#interruptStops = new Map();
|
|
24
|
+
#runJobReleases = new Map();
|
|
22
25
|
#torndown = false;
|
|
23
26
|
constructor(options) {
|
|
24
27
|
this.manager = new TerminalManager(options);
|
|
@@ -71,6 +74,24 @@ export class TerminalSessionBundle {
|
|
|
71
74
|
this.#ages.add({ id, description, startedAtMs, ...details });
|
|
72
75
|
this.#sinks?.onBackgroundState(this.backgroundSnapshot());
|
|
73
76
|
}
|
|
77
|
+
/** Hold the release of the run job `id` was recorded as; its exit drops it. */
|
|
78
|
+
trackRunJob(id, release) {
|
|
79
|
+
if (this.#torndown)
|
|
80
|
+
return release();
|
|
81
|
+
this.#runJobReleases.set(id, release);
|
|
82
|
+
}
|
|
83
|
+
/** Stop a session the interrupted turn `turnId` started; its exit notice then reaches the next turn without waking. */
|
|
84
|
+
async stopForInterrupt(id, turnId) {
|
|
85
|
+
const runtime = this.manager.get(id);
|
|
86
|
+
if (runtime === null)
|
|
87
|
+
throw new Error(`${id} runs on the session's tool child, which this stop does not reach`);
|
|
88
|
+
this.#interruptStops.set(id, turnId);
|
|
89
|
+
const stopped = await this.manager.stop(id);
|
|
90
|
+
if (stopped && runtime.exited)
|
|
91
|
+
return;
|
|
92
|
+
this.#interruptStops.delete(id);
|
|
93
|
+
throw new Error(`${id} was still running after the stop (TERM, then KILL)`);
|
|
94
|
+
}
|
|
74
95
|
/** The auto-reap's stop did not end a session still on the schedule: one notice; its age notices go on. */
|
|
75
96
|
notifyBackgroundReapFailed(id, autoReapMinutes, reason) {
|
|
76
97
|
if (this.#torndown)
|
|
@@ -81,9 +102,14 @@ export class TerminalSessionBundle {
|
|
|
81
102
|
const event = backgroundAgeEvent("reap_failed", { ...subject, autoReapMinutes }, Date.now());
|
|
82
103
|
this.#dispatchAge({ ...event, reason });
|
|
83
104
|
}
|
|
84
|
-
notifyBackgroundExit(id,
|
|
105
|
+
notifyBackgroundExit(id, exited) {
|
|
85
106
|
if (this.#torndown)
|
|
86
107
|
return;
|
|
108
|
+
this.#runJobReleases.get(id)?.();
|
|
109
|
+
this.#runJobReleases.delete(id);
|
|
110
|
+
const interruptedTurn = this.#interruptStops.get(id);
|
|
111
|
+
this.#interruptStops.delete(id);
|
|
112
|
+
const runtime = interruptedTurn === undefined ? exited : stoppedByInterruptView(exited, interruptedTurn);
|
|
87
113
|
const subject = this.#ages.remove(id);
|
|
88
114
|
this.#parkedAges.delete(id);
|
|
89
115
|
if (subject && runtime.autoReapedAfterMinutes !== undefined) {
|
|
@@ -107,6 +133,10 @@ export class TerminalSessionBundle {
|
|
|
107
133
|
this.#parkedExits.clear();
|
|
108
134
|
this.#ages.dispose();
|
|
109
135
|
this.#parkedAges.clear();
|
|
136
|
+
for (const release of this.#runJobReleases.values())
|
|
137
|
+
release();
|
|
138
|
+
this.#runJobReleases.clear();
|
|
139
|
+
this.#interruptStops.clear();
|
|
110
140
|
this.monitors.dispose();
|
|
111
141
|
if (this.#backgrounds.size > 0) {
|
|
112
142
|
this.#backgrounds.clear();
|
|
@@ -137,6 +167,16 @@ export class TerminalSessionBundle {
|
|
|
137
167
|
this.#parkedMonitorEvents.shift();
|
|
138
168
|
}
|
|
139
169
|
}
|
|
170
|
+
function stoppedByInterruptView(runtime, turnId) {
|
|
171
|
+
return {
|
|
172
|
+
exitResult: runtime.exitResult,
|
|
173
|
+
...(runtime.autoReapedAfterMinutes === undefined
|
|
174
|
+
? {}
|
|
175
|
+
: { autoReapedAfterMinutes: runtime.autoReapedAfterMinutes }),
|
|
176
|
+
stoppedByInterrupt: { turnId },
|
|
177
|
+
fullOutput: () => runtime.fullOutput(),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
140
180
|
const parkedBundles = new Map();
|
|
141
181
|
/** Park a bundle across a reload; at most one parked bundle per session, older ones torn down. */
|
|
142
182
|
export async function parkBundle(sessionKey, bundle) {
|
|
@@ -64,6 +64,10 @@ export interface TerminalExitView {
|
|
|
64
64
|
readonly exitResult: Pick<TerminalSessionExit, "exitCode" | "signal" | "cancelled" | "timedOut"> | null;
|
|
65
65
|
/** Set when the opt-in auto-reap stopped the session at this age (senpi#6201). */
|
|
66
66
|
readonly autoReapedAfterMinutes?: number;
|
|
67
|
+
/** Set when `interrupt { background: "stop" }` on turn `turnId` stopped the session: its notice does not wake. */
|
|
68
|
+
readonly stoppedByInterrupt?: {
|
|
69
|
+
readonly turnId: string;
|
|
70
|
+
};
|
|
67
71
|
fullOutput(): string;
|
|
68
72
|
}
|
|
69
73
|
export interface TerminalLaunchBarrier {
|
|
@@ -843,6 +843,11 @@ function createExtensionAPI(extension, runtime, cwd, eventBus, session) {
|
|
|
843
843
|
if (state !== "loading")
|
|
844
844
|
return;
|
|
845
845
|
runtime.assertActive();
|
|
846
|
+
// Checked before anything is applied: another extension may have registered a device machine while this load's
|
|
847
|
+
// registration was staged (from an event fired during this factory). The load then fails whole, nothing half-applied.
|
|
848
|
+
if (deviceMachineStaged && runtime.deviceMachine !== undefined) {
|
|
849
|
+
throw new DeviceMachineConflictError(runtime.deviceMachine.extensionPath, extension.path);
|
|
850
|
+
}
|
|
846
851
|
for (const [name, value] of pendingFlagValues) {
|
|
847
852
|
if (!runtime.flagValues.has(name))
|
|
848
853
|
runtime.flagValues.set(name, value);
|
|
@@ -169,6 +169,7 @@ export declare class ExtensionRunner {
|
|
|
169
169
|
private engineTurnsHeldFn;
|
|
170
170
|
private noteHeldWakeFn;
|
|
171
171
|
private pauseActions;
|
|
172
|
+
private registerRunBackgroundJobFn;
|
|
172
173
|
private questionInput;
|
|
173
174
|
private personAnswerPending;
|
|
174
175
|
private isProjectTrustedFn;
|
|
@@ -534,6 +534,7 @@ export class ExtensionRunner {
|
|
|
534
534
|
fenceRun: contextActions.fenceRun,
|
|
535
535
|
releaseRunFence: contextActions.releaseRunFence,
|
|
536
536
|
};
|
|
537
|
+
this.registerRunBackgroundJobFn = contextActions.registerRunBackgroundJob;
|
|
537
538
|
this.questionInput = contextActions.questionInput;
|
|
538
539
|
this.isProjectTrustedFn = contextActions.isProjectTrusted;
|
|
539
540
|
this.getSignalFn = contextActions.getSignal;
|
|
@@ -1345,6 +1346,10 @@ export class ExtensionRunner {
|
|
|
1345
1346
|
runner.assertActive();
|
|
1346
1347
|
runner.pauseActions.releaseRunFence?.(runRef);
|
|
1347
1348
|
},
|
|
1349
|
+
registerRunBackgroundJob: (job) => {
|
|
1350
|
+
runner.assertActive();
|
|
1351
|
+
return runner.registerRunBackgroundJobFn?.(job) ?? (() => { });
|
|
1352
|
+
},
|
|
1348
1353
|
isProjectTrusted: () => {
|
|
1349
1354
|
runner.assertActive();
|
|
1350
1355
|
return runner.isProjectTrustedFn();
|
|
@@ -55,6 +55,7 @@ import type { ExtensionKernelTools } from "./kernel-tools-context.ts";
|
|
|
55
55
|
import type { SessionControlActions, SessionControlWakeEvent } from "./session-control-types.ts";
|
|
56
56
|
import type { SessionDialogApi } from "./session-dialog-api.ts";
|
|
57
57
|
import type { UserActivityKind } from "./user-activity.ts";
|
|
58
|
+
import type { RunBackgroundJob } from "../run-background-jobs.ts";
|
|
58
59
|
export type { ExecOptions, ExecResult } from "../exec.ts";
|
|
59
60
|
export type { AppKeybinding, KeybindingsManager } from "../keybindings.ts";
|
|
60
61
|
export type { BuildSystemPromptOptions, NormalizedBuildSystemPromptOptions } from "../system-prompt.ts";
|
|
@@ -483,6 +484,11 @@ export interface ExtensionContext {
|
|
|
483
484
|
kind: "dag" | "task" | "workpool" | "team";
|
|
484
485
|
id: string;
|
|
485
486
|
}): void;
|
|
487
|
+
/**
|
|
488
|
+
* Record a background job the running turn started, so `interrupt { background: "stop" }` on that turn stops it.
|
|
489
|
+
* Outside a turn nothing is recorded. The returned release drops the job once it ended on its own.
|
|
490
|
+
*/
|
|
491
|
+
registerRunBackgroundJob?(job: RunBackgroundJob): () => void;
|
|
486
492
|
/** Whether project-local trust is active for this context. */
|
|
487
493
|
isProjectTrusted(): boolean;
|
|
488
494
|
/** The current abort signal, or undefined when the agent is not streaming. */
|
|
@@ -2956,6 +2962,7 @@ export interface ExtensionContextActions {
|
|
|
2956
2962
|
kind: "dag" | "task" | "workpool" | "team";
|
|
2957
2963
|
id: string;
|
|
2958
2964
|
}) => void;
|
|
2965
|
+
registerRunBackgroundJob?: (job: RunBackgroundJob) => () => void;
|
|
2959
2966
|
/** Internal delivery for a human-settled builtin question, never exposed on ExtensionAPI. */
|
|
2960
2967
|
questionInput?: () => void;
|
|
2961
2968
|
isProjectTrusted: () => boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The background jobs each turn started (a bash command run with `run_in_background` or one that auto-detached),
|
|
3
|
+
* keyed by the turn id (`turn_epoch`) that was running when the job became background. `interrupt { background:
|
|
4
|
+
* "stop" }` stops the jobs of the turn it interrupted, so nothing that turn left running wakes the session later.
|
|
5
|
+
* A job leaves the registry when it ends on its own (`release`) or once it is stopped.
|
|
6
|
+
*/
|
|
7
|
+
/** A job the turn started; `stop` ends it without waking the session and rejects with the reason it could not. */
|
|
8
|
+
export interface RunBackgroundJob {
|
|
9
|
+
readonly kind: "bash";
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly description?: string;
|
|
12
|
+
stop(turnId: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface StoppedRunBackgroundJob {
|
|
15
|
+
readonly kind: RunBackgroundJob["kind"];
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly description?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface FailedRunBackgroundJob extends StoppedRunBackgroundJob {
|
|
20
|
+
readonly reason: string;
|
|
21
|
+
}
|
|
22
|
+
/** What `interrupt { background: "stop" }` stopped, and each job it could not stop with the reason. */
|
|
23
|
+
export interface RunBackgroundStopOutcome {
|
|
24
|
+
readonly stopped: readonly StoppedRunBackgroundJob[];
|
|
25
|
+
readonly failed: readonly FailedRunBackgroundJob[];
|
|
26
|
+
}
|
|
27
|
+
export declare class RunBackgroundJobs {
|
|
28
|
+
#private;
|
|
29
|
+
/** Record `job` under `turnId`; the returned release drops it once it ended on its own. */
|
|
30
|
+
register(turnId: string, job: RunBackgroundJob): () => void;
|
|
31
|
+
stop(turnId: string): Promise<RunBackgroundStopOutcome>;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=run-background-jobs.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The background jobs each turn started (a bash command run with `run_in_background` or one that auto-detached),
|
|
3
|
+
* keyed by the turn id (`turn_epoch`) that was running when the job became background. `interrupt { background:
|
|
4
|
+
* "stop" }` stops the jobs of the turn it interrupted, so nothing that turn left running wakes the session later.
|
|
5
|
+
* A job leaves the registry when it ends on its own (`release`) or once it is stopped.
|
|
6
|
+
*/
|
|
7
|
+
function named(job) {
|
|
8
|
+
return { kind: job.kind, id: job.id, ...(job.description === undefined ? {} : { description: job.description }) };
|
|
9
|
+
}
|
|
10
|
+
export class RunBackgroundJobs {
|
|
11
|
+
#byTurn = new Map();
|
|
12
|
+
/** Record `job` under `turnId`; the returned release drops it once it ended on its own. */
|
|
13
|
+
register(turnId, job) {
|
|
14
|
+
let jobs = this.#byTurn.get(turnId);
|
|
15
|
+
if (jobs === undefined) {
|
|
16
|
+
jobs = new Set();
|
|
17
|
+
this.#byTurn.set(turnId, jobs);
|
|
18
|
+
}
|
|
19
|
+
jobs.add(job);
|
|
20
|
+
return () => this.#drop(turnId, job);
|
|
21
|
+
}
|
|
22
|
+
async stop(turnId) {
|
|
23
|
+
const jobs = [...(this.#byTurn.get(turnId) ?? [])];
|
|
24
|
+
this.#byTurn.delete(turnId);
|
|
25
|
+
const stopped = [];
|
|
26
|
+
const failed = [];
|
|
27
|
+
for (const job of jobs) {
|
|
28
|
+
try {
|
|
29
|
+
await job.stop(turnId);
|
|
30
|
+
stopped.push(named(job));
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
failed.push({ ...named(job), reason: error instanceof Error ? error.message : String(error) });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { stopped, failed };
|
|
37
|
+
}
|
|
38
|
+
#drop(turnId, job) {
|
|
39
|
+
const jobs = this.#byTurn.get(turnId);
|
|
40
|
+
if (jobs === undefined)
|
|
41
|
+
return;
|
|
42
|
+
jobs.delete(job);
|
|
43
|
+
if (jobs.size === 0)
|
|
44
|
+
this.#byTurn.delete(turnId);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=run-background-jobs.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ExecShell } from "../../../vendor/senpi-machine-ops/index.js";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { ShellTrust } from "../../engine-trust/shell-trust.ts";
|
|
4
|
+
import type { ToolDefinition } from "../extensions/types.ts";
|
|
5
|
+
import { type BashSpawnHook, type BashToolDetails, bashSchema, type ShellToolConfig } from "./bash-schema.ts";
|
|
6
|
+
import { type OutputSpillFactory } from "./output-accumulator-core.ts";
|
|
7
|
+
import { type BashOperations } from "./shell-operations.ts";
|
|
8
|
+
import type { ToolMachine } from "./tool-machine.ts";
|
|
9
|
+
/** Annotates a failed command's result with the engine trust status; undefined leaves the result as is. */
|
|
10
|
+
export type ShellTrustAnnotator = (result: {
|
|
11
|
+
exitCode: number;
|
|
12
|
+
output: string;
|
|
13
|
+
}) => {
|
|
14
|
+
text: string | undefined;
|
|
15
|
+
trust: ShellTrust;
|
|
16
|
+
} | undefined;
|
|
17
|
+
export interface ShellToolCoreOptions {
|
|
18
|
+
/** Runs the policy gate once per call. */
|
|
19
|
+
machine: ToolMachine;
|
|
20
|
+
/** Where the command runs. */
|
|
21
|
+
operations: BashOperations;
|
|
22
|
+
/** Where output past the display limits is kept; the path appears in the model's truncation notice. */
|
|
23
|
+
outputSpill: OutputSpillFactory;
|
|
24
|
+
/** The base environment before the session's `PI_*` values. Default: empty. */
|
|
25
|
+
shellEnv?: () => NodeJS.ProcessEnv;
|
|
26
|
+
/** Default: no annotation. */
|
|
27
|
+
annotateTrust?: ShellTrustAnnotator;
|
|
28
|
+
/** Redact credential-shaped output before streaming or spilling. Default: false. */
|
|
29
|
+
redactSecretsInOutput?: boolean;
|
|
30
|
+
/** Command prefix prepended to every command (for example shell setup commands) */
|
|
31
|
+
commandPrefix?: string;
|
|
32
|
+
/** Expose current Pi session metadata as PI_* environment variables. Default: true */
|
|
33
|
+
exposeSessionEnvironment?: boolean;
|
|
34
|
+
/** Hook to adjust command, cwd, or env before execution */
|
|
35
|
+
spawnHook?: BashSpawnHook;
|
|
36
|
+
}
|
|
37
|
+
/** A shell tool's model-facing definition and execution, without renderers: the shared core of bash and powershell. */
|
|
38
|
+
export declare function createShellToolCore(cwd: string, config: ShellToolConfig, options: ShellToolCoreOptions): ToolDefinition<typeof bashSchema, BashToolDetails | undefined>;
|
|
39
|
+
export interface CoreBashToolOptions extends Omit<ShellToolCoreOptions, "operations" | "outputSpill" | "annotateTrust" | "shellEnv"> {
|
|
40
|
+
/** Where the command runs. Default: `process.exec` on `machine`, in `shell`. */
|
|
41
|
+
operations?: BashOperations;
|
|
42
|
+
/** The shell for the default operations. Default: the machine's own (`/bin/sh -c` on POSIX). */
|
|
43
|
+
shell?: ExecShell;
|
|
44
|
+
/** Default: a file under `/tmp` on `machine` (see `createMachineOutputSpill`). */
|
|
45
|
+
outputSpill?: OutputSpillFactory;
|
|
46
|
+
}
|
|
47
|
+
/** The bash tool for a host whose only effect channel is `machine`. */
|
|
48
|
+
export declare function createCoreBashToolDefinition(cwd: string, options: CoreBashToolOptions): ToolDefinition<typeof bashSchema, BashToolDetails | undefined>;
|
|
49
|
+
export declare function createCoreBashTool(cwd: string, options: CoreBashToolOptions): AgentTool<typeof bashSchema>;
|
|
50
|
+
//# sourceMappingURL=bash-core.d.ts.map
|