@code-yeongyu/senpi 2026.10.18 → 2026.10.19
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 +47 -0
- package/dist/bundle/chunks/{anthropic-messages-DDBWM4JZ.js → anthropic-messages-EMTBHJMM.js} +2 -2
- package/dist/bundle/chunks/{args-2MYPVFPH.js → args-LWR7VFKS.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-3UG5TQU7.js → azure-openai-responses-SNWKFDXK.js} +1 -1
- package/dist/bundle/chunks/chatgpt-subscription.js +1 -1
- package/dist/bundle/chunks/{chunk-SOF44GFZ.js → chunk-35M5MVUZ.js} +149 -145
- package/dist/bundle/chunks/chunk-3DNYAO3P.js +2 -0
- package/dist/bundle/chunks/{chunk-T2MHSEQ2.js → chunk-5AOKDC6G.js} +1 -1
- package/dist/bundle/chunks/{chunk-MYXNQ7YZ.js → chunk-7QPFFAJT.js} +3 -3
- package/dist/bundle/chunks/{chunk-R36U2DJC.js → chunk-AUZXSLZN.js} +1 -1
- package/dist/bundle/chunks/{chunk-PMTG4YNS.js → chunk-BOCN744P.js} +1 -1
- package/dist/bundle/chunks/{chunk-XTAWER7O.js → chunk-CICJOV6I.js} +1 -1
- package/dist/bundle/chunks/{chunk-4UCLDXEF.js → chunk-FM7NDZR5.js} +1 -1
- package/dist/bundle/chunks/{chunk-TBRWPNA4.js → chunk-FSENE7JK.js} +2 -2
- package/dist/bundle/chunks/{chunk-TUD7XRIS.js → chunk-G7LLAQXI.js} +3 -3
- package/dist/bundle/chunks/{chunk-BGD3MZ47.js → chunk-GOKAWZUO.js} +1 -1
- package/dist/bundle/chunks/{chunk-XC6UG5SI.js → chunk-IM6ZWYTW.js} +2 -2
- package/dist/bundle/chunks/{chunk-KHATAB7P.js → chunk-KM4FMQII.js} +1 -1
- package/dist/bundle/chunks/chunk-LAWS3FYQ.js +11 -0
- package/dist/bundle/chunks/{chunk-W6YQYGE2.js → chunk-MVNVDGYF.js} +1 -1
- package/dist/bundle/chunks/{chunk-W4BO6S7F.js → chunk-PMGEQYEP.js} +2 -2
- package/dist/bundle/chunks/chunk-QGJLZ6BI.js +2 -0
- package/dist/bundle/chunks/{chunk-5RQNCWS2.js → chunk-QJ6MUFXY.js} +1 -1
- package/dist/bundle/chunks/{chunk-57Q7Q627.js → chunk-QRGAMVEE.js} +1 -1
- package/dist/bundle/chunks/{chunk-W2257FYP.js → chunk-QXEWAKXA.js} +1 -1
- package/dist/bundle/chunks/{chunk-UUMKAA7S.js → chunk-RL5QMGQD.js} +9 -9
- package/dist/bundle/chunks/{chunk-D2UQYZVK.js → chunk-SHKXJWK3.js} +1 -1
- package/dist/bundle/chunks/chunk-T36LJ6MW.js +3 -0
- package/dist/bundle/chunks/{chunk-S4V72F76.js → chunk-VCD7FD64.js} +1 -1
- package/dist/bundle/chunks/{chunk-MYPTZCPF.js → chunk-VEKNDMUA.js} +1 -1
- package/dist/bundle/chunks/{chunk-IRBAGET4.js → chunk-VPV7ADF3.js} +4 -4
- package/dist/bundle/chunks/{chunk-6DDAEDY7.js → chunk-WEHIEACU.js} +1 -1
- package/dist/bundle/chunks/{chunk-C2HSPFXN.js → chunk-WQLMQGOW.js} +1 -1
- package/dist/bundle/chunks/{chunk-QZIKKUUP.js → chunk-X6TWPBI6.js} +2 -2
- package/dist/bundle/chunks/{chunk-VYWODOAM.js → chunk-YUMRZIKJ.js} +6 -6
- package/dist/bundle/chunks/chunk-ZPSJQHDK.js +395 -0
- package/dist/bundle/chunks/{cli-main-MVVPTGMS.js → cli-main-EABMHWFV.js} +1 -1
- package/dist/bundle/chunks/{cloudflare-workers-ai-system-one-H4ZPUBX4.js → cloudflare-workers-ai-system-one-PF7FU63D.js} +1 -1
- package/dist/bundle/chunks/{google-generative-ai-YLAYNQLO.js → google-generative-ai-UFCHYLRE.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-THFXM5R2.js → google-vertex-2FWDY7XX.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-H4ERPCR5.js → help-fast-path-BYWJN6YX.js} +1 -1
- package/dist/bundle/chunks/{host-command-2CMRIKC3.js → host-command-5T34JLBP.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-6Y2EKT4U.js → host-lifecycle-GSRIQP6Z.js} +1 -1
- package/dist/bundle/chunks/host-lifecycle.js +5 -5
- package/dist/bundle/chunks/{host-session-command-MVHY4TKA.js → host-session-command-LF6JODPX.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-2ZQ43N73.js → interactive-mode-GMWHBIUC.js} +1 -1
- package/dist/bundle/chunks/{list-tips-AWDAUHF2.js → list-tips-VSWED3PT.js} +1 -1
- package/dist/bundle/chunks/{mistral-conversations-GUGTVRCL.js → mistral-conversations-QHZLDTSP.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-NO4PIOVG.js → multi-session-host-3J7STKGD.js} +6 -6
- package/dist/bundle/chunks/openai-codex-responses-7YOBPZ2N.js +10 -0
- package/dist/bundle/chunks/{openai-completions-I5N2UIEK.js → openai-completions-HSZPYXEA.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-SGLUJVZW.js → openai-responses-7W2KF3W7.js} +1 -1
- package/dist/bundle/chunks/openrouter.js +1 -1
- package/dist/bundle/chunks/{package-manager-cli-HTZQXYNI.js → package-manager-cli-LWQZSWKL.js} +1 -1
- package/dist/bundle/chunks/radius.js +1 -1
- package/dist/bundle/chunks/{rotation-stream-TOOIK43C.js → rotation-stream-QRRWL73X.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-6ZWGPICX.js → rpc-mode-6LSWRKXR.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-2YW4B655.js → schedule-command-7CXMW4WK.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-XC2LPLCD.js → session-control-endpoint-E265WAIQ.js} +1 -1
- package/dist/bundle/chunks/{session-picker-7W7GIRJT.js → session-picker-XRB3LP5W.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +315 -310
- package/dist/bundle/chunks/{typesafe-system-one-VBWWTJTF.js → typesafe-system-one-YA75435G.js} +1 -1
- 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-run-continuation.d.ts +24 -0
- package/dist/core/agent-run-continuation.js +57 -0
- package/dist/core/agent-session.d.ts +38 -1
- package/dist/core/agent-session.js +122 -13
- package/dist/core/extension-handler-stall-report.d.ts +11 -0
- package/dist/core/extension-handler-stall-report.js +26 -0
- package/dist/core/extensions/builtin/anthropic-subscription/account-command.js +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.d.ts +13 -1
- package/dist/core/extensions/builtin/anthropic-subscription/affinity.js +15 -4
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/executable.d.ts +5 -0
- package/dist/core/extensions/builtin/anthropic-subscription/executable.js +11 -1
- package/dist/core/extensions/builtin/anthropic-subscription/failover.d.ts +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/failover.js +3 -2
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.d.ts +8 -0
- package/dist/core/extensions/builtin/anthropic-subscription/guidance.js +29 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +2 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.js +4 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream-guidance.js +4 -2
- package/dist/core/extensions/handler-budget.d.ts +84 -0
- package/dist/core/extensions/handler-budget.js +136 -0
- package/dist/core/extensions/runner.d.ts +23 -0
- package/dist/core/extensions/runner.js +184 -99
- package/dist/core/footer-data-provider.js +2 -1
- package/dist/core/keybindings.d.ts +1 -1
- package/dist/core/keybindings.js +3 -2
- package/dist/core/model-resolver.js +1 -1
- package/dist/core/pending-input-journal.d.ts +6 -1
- package/dist/core/pending-input-journal.js +90 -34
- package/dist/core/pending-input-public.d.ts +11 -0
- package/dist/core/pending-input-public.js +10 -0
- package/dist/core/pending-input-session-port.d.ts +15 -0
- package/dist/core/pending-input-session-port.js +93 -0
- package/dist/core/pending-input-session.d.ts +108 -0
- package/dist/core/pending-input-session.js +220 -0
- package/dist/core/pending-input-store.d.ts +32 -1
- package/dist/core/pending-input-store.js +74 -26
- package/dist/core/pending-input-types.d.ts +25 -4
- package/dist/core/pending-input.js +36 -18
- package/dist/core/session-entry-file.js +1 -0
- package/dist/core/session-log.js +1 -1
- package/dist/core/session-manager.d.ts +1 -0
- package/dist/core/session-manager.js +5 -1
- package/dist/core/session-move/bundle.d.ts +39 -0
- package/dist/core/session-move/bundle.js +108 -0
- package/dist/core/session-move/export.d.ts +18 -0
- package/dist/core/session-move/export.js +83 -0
- package/dist/core/session-move/finish.d.ts +24 -0
- package/dist/core/session-move/finish.js +81 -0
- package/dist/core/session-move/import.d.ts +32 -0
- package/dist/core/session-move/import.js +124 -0
- package/dist/core/session-move/move-ledger.d.ts +47 -0
- package/dist/core/session-move/move-ledger.js +151 -0
- package/dist/core/session-move/ownership.d.ts +61 -0
- package/dist/core/session-move/ownership.js +89 -0
- package/dist/core/session-move/requeue.d.ts +8 -0
- package/dist/core/session-move/requeue.js +18 -0
- package/dist/core/session-move/write-guard.d.ts +22 -0
- package/dist/core/session-move/write-guard.js +44 -0
- package/dist/core/settings-manager.d.ts +13 -0
- package/dist/core/settings-manager.js +17 -0
- package/dist/core/turn-deadline.d.ts +2 -0
- package/dist/core/turn-deadline.js +10 -3
- package/dist/modes/interactive/components/ask-user-async-widget.d.ts +7 -0
- package/dist/modes/interactive/components/ask-user-async-widget.js +20 -6
- package/dist/modes/rpc/connection-handler.js +3 -0
- package/dist/modes/rpc/effect-commands.js +3 -4
- package/dist/modes/rpc/host-decision.d.ts +1 -1
- package/dist/modes/rpc/host-decision.js +2 -0
- package/dist/modes/rpc/host-ensure-liveness.d.ts +8 -0
- package/dist/modes/rpc/host-ensure-liveness.js +18 -1
- package/dist/modes/rpc/host-ensure-types.d.ts +2 -0
- package/dist/modes/rpc/host-ensure.js +8 -2
- package/dist/modes/rpc/host-lifecycle-proxy.d.ts +9 -2
- package/dist/modes/rpc/host-lifecycle-proxy.js +36 -8
- package/dist/modes/rpc/host-observe-request.js +1 -0
- package/dist/modes/rpc/multi-session-host.js +3 -33
- package/dist/modes/rpc/rpc-client.d.ts +10 -0
- package/dist/modes/rpc/rpc-client.js +11 -0
- package/dist/modes/rpc/rpc-input-validation.js +8 -0
- package/dist/modes/rpc/rpc-types.d.ts +25 -1
- package/dist/modes/rpc/session-command-activity.js +1 -0
- package/dist/modes/rpc/session-control-actions-data.js +17 -8
- package/dist/modes/rpc/session-held.js +1 -0
- package/dist/modes/rpc/session-release.js +16 -1
- package/dist/modes/rpc/socket-ownership.d.ts +17 -0
- package/dist/modes/rpc/socket-ownership.js +52 -0
- package/dist/modes/rpc/tool-child/admission.d.ts +42 -0
- package/dist/modes/rpc/tool-child/admission.js +81 -0
- package/dist/modes/rpc/tool-child/admission.test.d.ts +2 -0
- package/dist/modes/rpc/tool-child/admission.test.js +166 -0
- package/dist/modes/rpc/tool-child/codec.d.ts +21 -0
- package/dist/modes/rpc/tool-child/codec.js +137 -0
- package/dist/modes/rpc/tool-child/codec.test.d.ts +2 -0
- package/dist/modes/rpc/tool-child/codec.test.js +234 -0
- package/dist/modes/rpc/tool-child/context.d.ts +42 -0
- package/dist/modes/rpc/tool-child/context.js +23 -0
- package/dist/modes/rpc/tool-child/flow-control.d.ts +24 -0
- package/dist/modes/rpc/tool-child/flow-control.js +80 -0
- package/dist/modes/rpc/tool-child/payload-ref-paths.d.ts +9 -0
- package/dist/modes/rpc/tool-child/payload-ref-paths.js +45 -0
- package/dist/modes/rpc/tool-child/payload-ref.d.ts +58 -0
- package/dist/modes/rpc/tool-child/payload-ref.js +166 -0
- package/dist/modes/rpc/tool-child/payload-ref.test.d.ts +2 -0
- package/dist/modes/rpc/tool-child/payload-ref.test.js +254 -0
- package/dist/modes/rpc/tool-child/payload-wire.d.ts +13 -0
- package/dist/modes/rpc/tool-child/payload-wire.js +22 -0
- package/dist/modes/rpc/tool-child/protocol-guard.d.ts +9 -0
- package/dist/modes/rpc/tool-child/protocol-guard.js +128 -0
- package/dist/modes/rpc/tool-child/protocol.d.ts +192 -0
- package/dist/modes/rpc/tool-child/protocol.js +54 -0
- package/dist/utils/wsl.d.ts +2 -2
- package/dist/utils/wsl.js +4 -4
- package/docs/providers.md +2 -0
- package/docs/rpc.md +20 -2
- package/docs/settings.md +1 -0
- package/docs/terminal-setup.md +2 -0
- package/package.json +10 -6
- package/dist/bundle/chunks/chunk-COJK5FDH.js +0 -2
- package/dist/bundle/chunks/chunk-ES3GHZE5.js +0 -395
- package/dist/bundle/chunks/chunk-IDXW4FAO.js +0 -2
- package/dist/bundle/chunks/chunk-PFNBY43W.js +0 -2
- package/dist/bundle/chunks/chunk-SA2ZLWBD.js +0 -11
- package/dist/bundle/chunks/openai-codex-responses-5LRKZCBW.js +0 -10
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host budget for extension event handlers (senpi#3650). Past `warnMs` the host reports a handler as stalled; at
|
|
3
|
+
* `timeoutMs`, or when the turn deadline releases the turn it belongs to, the host stops waiting on it and moves on
|
|
4
|
+
* (the handler keeps running detached). A value of 0 disables that limit.
|
|
5
|
+
*/
|
|
6
|
+
export interface ExtensionHandlerBudget {
|
|
7
|
+
readonly warnMs: number;
|
|
8
|
+
readonly timeoutMs: number;
|
|
9
|
+
}
|
|
10
|
+
export type ExtensionHandlerStallStage = "warn" | "timeout" | "released" | "late_failure";
|
|
11
|
+
export interface ExtensionHandlerStall {
|
|
12
|
+
readonly extensionPath: string;
|
|
13
|
+
readonly event: string;
|
|
14
|
+
readonly stage: ExtensionHandlerStallStage;
|
|
15
|
+
readonly elapsedMs: number;
|
|
16
|
+
readonly timeoutMs: number;
|
|
17
|
+
readonly error?: string;
|
|
18
|
+
}
|
|
19
|
+
export type HandlerStopVerdict = "timed_out" | "released";
|
|
20
|
+
export type BudgetedHandlerOutcome<T> = {
|
|
21
|
+
readonly kind: "settled";
|
|
22
|
+
readonly value: T;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: "failed";
|
|
25
|
+
readonly error: unknown;
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: "timed_out";
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: "released";
|
|
30
|
+
};
|
|
31
|
+
/** Waits on running handlers, tagged with the turn that owns them so a deadline releases only its own turn's waits. */
|
|
32
|
+
export declare class HeldHandlerWaits {
|
|
33
|
+
private readonly releases;
|
|
34
|
+
hold(scope: number, release: () => void): () => void;
|
|
35
|
+
release(scope: number): number;
|
|
36
|
+
}
|
|
37
|
+
export interface HeldHandlerSlot {
|
|
38
|
+
readonly waits: HeldHandlerWaits;
|
|
39
|
+
readonly scope: () => number;
|
|
40
|
+
}
|
|
41
|
+
export interface BudgetedHandlerRun<T> {
|
|
42
|
+
readonly extensionPath: string;
|
|
43
|
+
readonly event: string;
|
|
44
|
+
readonly budget: ExtensionHandlerBudget;
|
|
45
|
+
readonly run: (signal: AbortSignal) => Promise<T> | T;
|
|
46
|
+
readonly onStall?: (stall: ExtensionHandlerStall) => void;
|
|
47
|
+
readonly now?: () => number;
|
|
48
|
+
}
|
|
49
|
+
/** One handler under the budget, with an abort signal it can observe (the `session_shutdown` path). */
|
|
50
|
+
export declare function runBudgetedHandler<T>(options: BudgetedHandlerRun<T>): Promise<BudgetedHandlerOutcome<T>>;
|
|
51
|
+
export type HandlerCallOutcome = {
|
|
52
|
+
readonly ok: true;
|
|
53
|
+
readonly value: unknown;
|
|
54
|
+
} | {
|
|
55
|
+
readonly ok: false;
|
|
56
|
+
readonly error: unknown;
|
|
57
|
+
};
|
|
58
|
+
export interface BudgetedSequence<R> {
|
|
59
|
+
readonly event: string;
|
|
60
|
+
readonly length: number;
|
|
61
|
+
extensionPath(index: number): string;
|
|
62
|
+
/** Calls handler `index`; returns its result or promise, unwrapped by the sequence with a plain `await`. */
|
|
63
|
+
start(index: number): unknown;
|
|
64
|
+
/** Applies a handler's outcome; may return a promise only when it has async host work of its own. */
|
|
65
|
+
settle(index: number, outcome: HandlerCallOutcome): void | Promise<void>;
|
|
66
|
+
skip(index: number, verdict: HandlerStopVerdict, budget: ExtensionHandlerBudget): void;
|
|
67
|
+
/** Ends the walk early (for example a retired runner); the sequence then resolves with `finish()`. */
|
|
68
|
+
halted?(): boolean;
|
|
69
|
+
finish(): R;
|
|
70
|
+
}
|
|
71
|
+
export interface SequenceOptions {
|
|
72
|
+
readonly budget: ExtensionHandlerBudget;
|
|
73
|
+
readonly held?: HeldHandlerSlot;
|
|
74
|
+
readonly onStall?: (stall: ExtensionHandlerStall) => void;
|
|
75
|
+
readonly now?: () => number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Walks an emission's handlers under the budget. A handler that settles in time is awaited exactly as a direct
|
|
79
|
+
* `await handler()` would be, so a timely emission keeps its microtask timing. A handler past its cap, or released
|
|
80
|
+
* by the turn deadline, is skipped: the walk resumes at the next handler in a new run that settles the emission,
|
|
81
|
+
* and the abandoned run stops when its handler ever returns. A failure after the host stopped waiting is reported.
|
|
82
|
+
*/
|
|
83
|
+
export declare function runBudgetedSequence<R>(sequence: BudgetedSequence<R>, options: SequenceOptions): Promise<R>;
|
|
84
|
+
//# sourceMappingURL=handler-budget.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** Waits on running handlers, tagged with the turn that owns them so a deadline releases only its own turn's waits. */
|
|
2
|
+
export class HeldHandlerWaits {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.releases = new Map();
|
|
5
|
+
}
|
|
6
|
+
hold(scope, release) {
|
|
7
|
+
this.releases.set(release, scope);
|
|
8
|
+
return () => this.releases.delete(release);
|
|
9
|
+
}
|
|
10
|
+
release(scope) {
|
|
11
|
+
const owned = [...this.releases].filter(([, owner]) => owner === scope).map(([release]) => release);
|
|
12
|
+
for (const release of owned) {
|
|
13
|
+
this.releases.delete(release);
|
|
14
|
+
release();
|
|
15
|
+
}
|
|
16
|
+
return owned.length;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const describeError = (error) => (error instanceof Error ? error.message : String(error));
|
|
20
|
+
function watchHandler(options, stop, held) {
|
|
21
|
+
const { budget, extensionPath, event, now } = options;
|
|
22
|
+
const startedAt = now();
|
|
23
|
+
const stall = (stage) => options.onStall?.({ extensionPath, event, stage, elapsedMs: now() - startedAt, timeoutMs: budget.timeoutMs });
|
|
24
|
+
let stopped = false;
|
|
25
|
+
const end = (verdict) => {
|
|
26
|
+
if (stopped)
|
|
27
|
+
return;
|
|
28
|
+
stopped = true;
|
|
29
|
+
disarm();
|
|
30
|
+
stall(verdict === "timed_out" ? "timeout" : "released");
|
|
31
|
+
stop(verdict);
|
|
32
|
+
};
|
|
33
|
+
const warnTimer = budget.warnMs > 0 ? setTimeout(() => stall("warn"), budget.warnMs) : undefined;
|
|
34
|
+
const capTimer = budget.timeoutMs > 0 ? setTimeout(() => end("timed_out"), budget.timeoutMs) : undefined;
|
|
35
|
+
const unhold = held?.waits.hold(held.scope(), () => end("released"));
|
|
36
|
+
function disarm() {
|
|
37
|
+
clearTimeout(warnTimer);
|
|
38
|
+
clearTimeout(capTimer);
|
|
39
|
+
unhold?.();
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
startedAt,
|
|
43
|
+
disarm: () => {
|
|
44
|
+
stopped = true;
|
|
45
|
+
disarm();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** One handler under the budget, with an abort signal it can observe (the `session_shutdown` path). */
|
|
50
|
+
export async function runBudgetedHandler(options) {
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
// The handler outcome is captured before any race, so a late rejection after a timeout is already handled.
|
|
53
|
+
let started;
|
|
54
|
+
try {
|
|
55
|
+
started = Promise.resolve(options.run(controller.signal));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
started = Promise.reject(error);
|
|
59
|
+
}
|
|
60
|
+
const settled = started.then((value) => ({ kind: "settled", value }), (error) => ({ kind: "failed", error }));
|
|
61
|
+
if (options.budget.warnMs <= 0 && options.budget.timeoutMs <= 0)
|
|
62
|
+
return settled;
|
|
63
|
+
let watch;
|
|
64
|
+
const outcome = await new Promise((resolve) => {
|
|
65
|
+
watch = watchHandler({ ...options, now: options.now ?? Date.now }, (verdict) => {
|
|
66
|
+
controller.abort(new Error(`${options.event} handler ${verdict === "timed_out" ? "timed out" : "released"}`));
|
|
67
|
+
resolve({ kind: verdict });
|
|
68
|
+
});
|
|
69
|
+
void settled.then(resolve);
|
|
70
|
+
});
|
|
71
|
+
watch?.disarm();
|
|
72
|
+
return outcome;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Walks an emission's handlers under the budget. A handler that settles in time is awaited exactly as a direct
|
|
76
|
+
* `await handler()` would be, so a timely emission keeps its microtask timing. A handler past its cap, or released
|
|
77
|
+
* by the turn deadline, is skipped: the walk resumes at the next handler in a new run that settles the emission,
|
|
78
|
+
* and the abandoned run stops when its handler ever returns. A failure after the host stopped waiting is reported.
|
|
79
|
+
*/
|
|
80
|
+
export function runBudgetedSequence(sequence, options) {
|
|
81
|
+
const now = options.now ?? Date.now;
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
let generation = 0;
|
|
84
|
+
const run = async (from, owner) => {
|
|
85
|
+
for (let index = from; index < sequence.length; index += 1) {
|
|
86
|
+
if (owner !== generation)
|
|
87
|
+
return;
|
|
88
|
+
if (sequence.halted?.())
|
|
89
|
+
break;
|
|
90
|
+
const extensionPath = sequence.extensionPath(index);
|
|
91
|
+
const watchOptions = {
|
|
92
|
+
extensionPath,
|
|
93
|
+
event: sequence.event,
|
|
94
|
+
budget: options.budget,
|
|
95
|
+
onStall: options.onStall,
|
|
96
|
+
now,
|
|
97
|
+
};
|
|
98
|
+
const watch = watchHandler(watchOptions, (verdict) => {
|
|
99
|
+
if (owner !== generation)
|
|
100
|
+
return;
|
|
101
|
+
generation += 1;
|
|
102
|
+
sequence.skip(index, verdict, options.budget);
|
|
103
|
+
run(index + 1, generation).catch(reject);
|
|
104
|
+
}, options.held);
|
|
105
|
+
let outcome;
|
|
106
|
+
try {
|
|
107
|
+
outcome = { ok: true, value: await sequence.start(index) };
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
outcome = { ok: false, error };
|
|
111
|
+
}
|
|
112
|
+
watch.disarm();
|
|
113
|
+
if (owner !== generation) {
|
|
114
|
+
if (!outcome.ok) {
|
|
115
|
+
options.onStall?.({
|
|
116
|
+
extensionPath,
|
|
117
|
+
event: sequence.event,
|
|
118
|
+
stage: "late_failure",
|
|
119
|
+
elapsedMs: now() - watch.startedAt,
|
|
120
|
+
timeoutMs: options.budget.timeoutMs,
|
|
121
|
+
error: describeError(outcome.error),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const settling = sequence.settle(index, outcome);
|
|
127
|
+
if (settling !== undefined)
|
|
128
|
+
await settling;
|
|
129
|
+
}
|
|
130
|
+
if (owner === generation)
|
|
131
|
+
resolve(sequence.finish());
|
|
132
|
+
};
|
|
133
|
+
run(0, 0).catch(reject);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=handler-budget.js.map
|
|
@@ -13,6 +13,7 @@ import type { ModelRegistry } from "../model-registry.ts";
|
|
|
13
13
|
import type { SessionManager } from "../session-manager.ts";
|
|
14
14
|
import type { BuildSystemPromptOptions } from "../system-prompt.ts";
|
|
15
15
|
import type { VirtualModelDefinition } from "../virtual-models.ts";
|
|
16
|
+
import { type ExtensionHandlerBudget, type ExtensionHandlerStall } from "./handler-budget.ts";
|
|
16
17
|
import type { AgentBeforeSettleEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BoundaryContextPreview, ContextEvent, ContextWithSystemEvent, EntryRenderer, EntryRendererOptions, Extension, ExtensionActions, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFlag, ExtensionMode, ExtensionRuntime, ExtensionShortcut, ExtensionToolContext, ExtensionUIContext, InputEvent, InputEventResult, InputSource, LoadExtensionsResult, MarkdownTransformer, MessageEndEvent, MessageRenderer, ModelSelectEvent, ModelSelectEventResult, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventResult, ProviderConfig, ProviderRequestPreparation, RegisteredMcpServerDeclaration, RegisteredTool, ReplacedSessionContext, ResolvedCommand, ResourcesDiscoverEvent, SessionBeforeCompactResult, SessionBeforeForkResult, SessionBeforeReloadResult, SessionBeforeSwitchResult, SessionBeforeTreeResult, SessionBoundaryDraft, SessionShutdownEvent, ToolCallEvent, ToolCallEventResult, ToolResultEvent, ToolResultEventResult, TurnEndEvent, UserBashEvent, UserBashEventResult } from "./types.ts";
|
|
17
18
|
export declare class ExtensionRpcRequestNotFoundError extends Error {
|
|
18
19
|
constructor(name: string);
|
|
@@ -122,6 +123,11 @@ export declare class ExtensionRunner {
|
|
|
122
123
|
private modelRegistry;
|
|
123
124
|
private eventBus;
|
|
124
125
|
private errorListeners;
|
|
126
|
+
private eventHandlerBudget;
|
|
127
|
+
private handlerStallListener;
|
|
128
|
+
private handlerTurnScope;
|
|
129
|
+
private reportedInvalidHandlerBudget;
|
|
130
|
+
private readonly heldHandlerWaits;
|
|
125
131
|
private getModel;
|
|
126
132
|
private getServiceTier;
|
|
127
133
|
private getEffectiveServiceTier;
|
|
@@ -226,6 +232,23 @@ export declare class ExtensionRunner {
|
|
|
226
232
|
get isActive(): boolean;
|
|
227
233
|
private assertActive;
|
|
228
234
|
onError(listener: ExtensionErrorListener): () => void;
|
|
235
|
+
/**
|
|
236
|
+
* The host's budget for handlers of the queued lifecycle and boundary events, who hears their stalls, and the turn
|
|
237
|
+
* that owns a wait started now (the turn deadline releases only its own turn's waits).
|
|
238
|
+
*/
|
|
239
|
+
setEventHandlerBudget(budget: () => ExtensionHandlerBudget, onStall: (stall: ExtensionHandlerStall) => void, turnScope?: () => number): void;
|
|
240
|
+
/** Stops waiting on the given turn's handlers that are still running (the turn deadline's release). */
|
|
241
|
+
releaseHeldHandlers(turn: number): number;
|
|
242
|
+
/**
|
|
243
|
+
* Resolved once per emission, before any handler starts. An invalid setting must never leave handlers unawaited,
|
|
244
|
+
* so it is reported once and the shipped defaults apply, as for `session_shutdown`.
|
|
245
|
+
*/
|
|
246
|
+
private resolveEventHandlerBudget;
|
|
247
|
+
private walkHandlers;
|
|
248
|
+
private reportStoppedHandler;
|
|
249
|
+
private reportHandlerFailure;
|
|
250
|
+
/** Agent lifecycle events: every handler runs, serially, under the host budget (senpi#3650). */
|
|
251
|
+
private dispatchLifecycle;
|
|
229
252
|
emitError(error: ExtensionError): void;
|
|
230
253
|
hasHandlers(eventType: string): boolean;
|
|
231
254
|
private getToolHookStatusMessage;
|
|
@@ -6,11 +6,12 @@ import { getAgentDir } from "../../config.js";
|
|
|
6
6
|
import { theme } from "../../modes/interactive/theme/theme.js";
|
|
7
7
|
import { stripAnsi } from "../../utils/ansi.js";
|
|
8
8
|
import { createEventBus, EXTENSION_RPC_EVENT_CHANNEL } from "../event-bus.js";
|
|
9
|
-
import { DEFAULT_SESSION_SHUTDOWN_HANDLER_TIMEOUT_MS, DEFAULT_SESSION_SHUTDOWN_HANDLER_WARN_MS, SettingsManager, } from "../settings-manager.js";
|
|
9
|
+
import { DEFAULT_EXTENSION_EVENT_HANDLER_TIMEOUT_MS, DEFAULT_EXTENSION_EVENT_HANDLER_WARN_MS, DEFAULT_SESSION_SHUTDOWN_HANDLER_TIMEOUT_MS, DEFAULT_SESSION_SHUTDOWN_HANDLER_WARN_MS, SettingsManager, } from "../settings-manager.js";
|
|
10
10
|
import { goalFilePath } from "./builtin/goal/persistence.js";
|
|
11
11
|
import { goalStoreRef } from "./builtin/goal/store-ref.js";
|
|
12
12
|
import { disableServerSearch } from "./builtin/server-search-fallback.js";
|
|
13
13
|
import { dispatchContextMessages } from "./context-dispatch.js";
|
|
14
|
+
import { HeldHandlerWaits, runBudgetedHandler, runBudgetedSequence, } from "./handler-budget.js";
|
|
14
15
|
import { kernelToolsStorage } from "./kernel-tools-context.js";
|
|
15
16
|
import { drainPendingProviderRegistrations } from "./loader.js";
|
|
16
17
|
import { SessionStartTurnGate } from "./session-start-turn-gate.js";
|
|
@@ -101,6 +102,23 @@ function sanitizedToolHookStatusMessage(message) {
|
|
|
101
102
|
.replace(/\s+/g, " ")
|
|
102
103
|
.trim());
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Agent lifecycle events the session awaits on its serial event queue before it persists a turn (senpi#3650).
|
|
107
|
+
* Their handlers observe; none may hold the queue indefinitely, so each runs under the host's event handler budget.
|
|
108
|
+
*/
|
|
109
|
+
const QUEUED_LIFECYCLE_EVENTS = new Set([
|
|
110
|
+
"agent_start",
|
|
111
|
+
"agent_end",
|
|
112
|
+
"agent_settled",
|
|
113
|
+
"turn_start",
|
|
114
|
+
"message_start",
|
|
115
|
+
"message_update",
|
|
116
|
+
"message_end",
|
|
117
|
+
"tool_execution_start",
|
|
118
|
+
"tool_execution_update",
|
|
119
|
+
"tool_execution_end",
|
|
120
|
+
]);
|
|
121
|
+
const UNBOUNDED_HANDLER_BUDGET = { warnMs: 0, timeoutMs: 0 };
|
|
104
122
|
/**
|
|
105
123
|
* Helper function to emit session_shutdown event to extensions.
|
|
106
124
|
* Returns true if the event was emitted, false if there were no handlers.
|
|
@@ -118,6 +136,16 @@ export async function emitSessionShutdownEvent(extensionRunner, event) {
|
|
|
118
136
|
function snapshotEventHandlers(extensions, event) {
|
|
119
137
|
return extensions.map((ext) => ({ ext, handlers: ext.handlers.get(event)?.slice() ?? [] }));
|
|
120
138
|
}
|
|
139
|
+
/** The same snapshot as `snapshotEventHandlers`, in dispatch order, one entry per handler. */
|
|
140
|
+
function flattenHandlers(extensions, event) {
|
|
141
|
+
return snapshotEventHandlers(extensions, event).flatMap(({ ext, handlers }) => handlers.map((handler) => ({ path: ext.path, handler })));
|
|
142
|
+
}
|
|
143
|
+
function entryAt(entries, index) {
|
|
144
|
+
const entry = entries[index];
|
|
145
|
+
if (entry === undefined)
|
|
146
|
+
throw new RangeError(`no extension handler at index ${index}`);
|
|
147
|
+
return entry;
|
|
148
|
+
}
|
|
121
149
|
export async function emitProjectTrustEvent(extensionsResult, event, ctx) {
|
|
122
150
|
const errors = [];
|
|
123
151
|
for (const { ext, handlers } of snapshotEventHandlers(extensionsResult.extensions, "project_trust")) {
|
|
@@ -203,6 +231,10 @@ export class ExtensionRunner {
|
|
|
203
231
|
constructor(extensions, runtime, cwd, sessionManager, modelRegistry, eventBus = createEventBus()) {
|
|
204
232
|
this.mode = "print";
|
|
205
233
|
this.errorListeners = new Set();
|
|
234
|
+
this.eventHandlerBudget = () => UNBOUNDED_HANDLER_BUDGET;
|
|
235
|
+
this.handlerTurnScope = () => 0;
|
|
236
|
+
this.reportedInvalidHandlerBudget = false;
|
|
237
|
+
this.heldHandlerWaits = new HeldHandlerWaits();
|
|
206
238
|
this.getModel = () => undefined;
|
|
207
239
|
this.getServiceTier = () => undefined;
|
|
208
240
|
this.getEffectiveServiceTier = () => this.getServiceTier();
|
|
@@ -676,6 +708,78 @@ export class ExtensionRunner {
|
|
|
676
708
|
this.errorListeners.add(listener);
|
|
677
709
|
return () => this.errorListeners.delete(listener);
|
|
678
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* The host's budget for handlers of the queued lifecycle and boundary events, who hears their stalls, and the turn
|
|
713
|
+
* that owns a wait started now (the turn deadline releases only its own turn's waits).
|
|
714
|
+
*/
|
|
715
|
+
setEventHandlerBudget(budget, onStall, turnScope = () => 0) {
|
|
716
|
+
this.eventHandlerBudget = budget;
|
|
717
|
+
this.handlerStallListener = onStall;
|
|
718
|
+
this.handlerTurnScope = turnScope;
|
|
719
|
+
}
|
|
720
|
+
/** Stops waiting on the given turn's handlers that are still running (the turn deadline's release). */
|
|
721
|
+
releaseHeldHandlers(turn) {
|
|
722
|
+
return this.heldHandlerWaits.release(turn);
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Resolved once per emission, before any handler starts. An invalid setting must never leave handlers unawaited,
|
|
726
|
+
* so it is reported once and the shipped defaults apply, as for `session_shutdown`.
|
|
727
|
+
*/
|
|
728
|
+
resolveEventHandlerBudget() {
|
|
729
|
+
try {
|
|
730
|
+
return this.eventHandlerBudget();
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
if (!this.reportedInvalidHandlerBudget) {
|
|
734
|
+
this.reportedInvalidHandlerBudget = true;
|
|
735
|
+
console.warn(`Using the default extension event handler budget (warn ${DEFAULT_EXTENSION_EVENT_HANDLER_WARN_MS}ms, hard cap ${DEFAULT_EXTENSION_EVENT_HANDLER_TIMEOUT_MS}ms): ${err instanceof Error ? err.message : String(err)}`);
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
warnMs: DEFAULT_EXTENSION_EVENT_HANDLER_WARN_MS,
|
|
739
|
+
timeoutMs: DEFAULT_EXTENSION_EVENT_HANDLER_TIMEOUT_MS,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
walkHandlers(sequence) {
|
|
744
|
+
return runBudgetedSequence(sequence, {
|
|
745
|
+
budget: this.resolveEventHandlerBudget(),
|
|
746
|
+
held: { waits: this.heldHandlerWaits, scope: this.handlerTurnScope },
|
|
747
|
+
onStall: (stall) => this.handlerStallListener?.(stall),
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
reportStoppedHandler(extensionPath, event, verdict, budget) {
|
|
751
|
+
const error = verdict === "timed_out"
|
|
752
|
+
? `handler timed out after ${budget.timeoutMs}ms`
|
|
753
|
+
: "handler released by the turn deadline";
|
|
754
|
+
this.emitError({ extensionPath, event, error });
|
|
755
|
+
}
|
|
756
|
+
reportHandlerFailure(extensionPath, event, err) {
|
|
757
|
+
this.emitError({
|
|
758
|
+
extensionPath,
|
|
759
|
+
event,
|
|
760
|
+
error: err instanceof Error ? err.message : String(err),
|
|
761
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
/** Agent lifecycle events: every handler runs, serially, under the host budget (senpi#3650). */
|
|
765
|
+
dispatchLifecycle(event) {
|
|
766
|
+
const entries = flattenHandlers(this.extensions, event.type);
|
|
767
|
+
return this.walkHandlers({
|
|
768
|
+
event: event.type,
|
|
769
|
+
length: entries.length,
|
|
770
|
+
extensionPath: (index) => entryAt(entries, index).path,
|
|
771
|
+
start: (index) => {
|
|
772
|
+
const { path, handler } = entryAt(entries, index);
|
|
773
|
+
return handler(event, this.createContext(path));
|
|
774
|
+
},
|
|
775
|
+
settle: (index, outcome) => {
|
|
776
|
+
if (!outcome.ok)
|
|
777
|
+
this.reportHandlerFailure(entryAt(entries, index).path, event.type, outcome.error);
|
|
778
|
+
},
|
|
779
|
+
skip: (index, verdict, budget) => this.reportStoppedHandler(entryAt(entries, index).path, event.type, verdict, budget),
|
|
780
|
+
finish: () => undefined,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
679
783
|
emitError(error) {
|
|
680
784
|
for (const listener of this.errorListeners) {
|
|
681
785
|
listener(error);
|
|
@@ -1186,34 +1290,36 @@ export class ExtensionRunner {
|
|
|
1186
1290
|
throw err;
|
|
1187
1291
|
}
|
|
1188
1292
|
let valid = true;
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1293
|
+
const handlers = flattenHandlers(this.extensions, baseEvent.type);
|
|
1294
|
+
// Boundary handlers hold the agent loop's turn end and the run's settle, so they run under the same host budget
|
|
1295
|
+
// as the lifecycle events and the turn deadline can release them (senpi#3650).
|
|
1296
|
+
return this.walkHandlers({
|
|
1297
|
+
event: baseEvent.type,
|
|
1298
|
+
length: handlers.length,
|
|
1299
|
+
extensionPath: (index) => entryAt(handlers, index).path,
|
|
1300
|
+
halted: () => !this.isActive,
|
|
1301
|
+
start: (index) => {
|
|
1193
1302
|
const event = {
|
|
1194
1303
|
...baseEvent,
|
|
1195
1304
|
entries,
|
|
1196
1305
|
continue: shouldContinue,
|
|
1197
1306
|
context,
|
|
1198
1307
|
};
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1308
|
+
return entryAt(handlers, index).handler(event, ctx);
|
|
1309
|
+
},
|
|
1310
|
+
settle: async (index, outcome) => {
|
|
1311
|
+
if (!this.isActive)
|
|
1312
|
+
return;
|
|
1313
|
+
const { path } = entryAt(handlers, index);
|
|
1314
|
+
if (outcome.ok) {
|
|
1315
|
+
const handlerResult = outcome.value;
|
|
1203
1316
|
if (handlerResult?.entries !== undefined)
|
|
1204
1317
|
entries = handlerResult.entries;
|
|
1205
1318
|
if (handlerResult?.continue !== undefined)
|
|
1206
1319
|
shouldContinue = handlerResult.continue;
|
|
1207
1320
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
return retired();
|
|
1211
|
-
this.emitError({
|
|
1212
|
-
extensionPath: ext.path,
|
|
1213
|
-
event: baseEvent.type,
|
|
1214
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1215
|
-
stack: err instanceof Error ? err.stack : undefined,
|
|
1216
|
-
});
|
|
1321
|
+
else {
|
|
1322
|
+
this.reportHandlerFailure(path, baseEvent.type, outcome.error);
|
|
1217
1323
|
}
|
|
1218
1324
|
try {
|
|
1219
1325
|
context = await buildContext(entries);
|
|
@@ -1221,18 +1327,19 @@ export class ExtensionRunner {
|
|
|
1221
1327
|
}
|
|
1222
1328
|
catch (err) {
|
|
1223
1329
|
if (!this.isActive)
|
|
1224
|
-
return
|
|
1330
|
+
return;
|
|
1225
1331
|
valid = false;
|
|
1226
1332
|
this.emitError({
|
|
1227
|
-
extensionPath:
|
|
1333
|
+
extensionPath: path,
|
|
1228
1334
|
event: baseEvent.type,
|
|
1229
1335
|
error: `Invalid boundary entries: ${err instanceof Error ? err.message : String(err)}`,
|
|
1230
1336
|
stack: err instanceof Error ? err.stack : undefined,
|
|
1231
1337
|
});
|
|
1232
1338
|
}
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1339
|
+
},
|
|
1340
|
+
skip: (index, verdict, budget) => this.reportStoppedHandler(entryAt(handlers, index).path, baseEvent.type, verdict, budget),
|
|
1341
|
+
finish: () => valid && this.isActive ? { entries, continue: shouldContinue, context, valid: true } : retired(),
|
|
1342
|
+
});
|
|
1236
1343
|
}
|
|
1237
1344
|
isSessionBeforeEvent(event) {
|
|
1238
1345
|
return (event.type === "session_before_switch" ||
|
|
@@ -1275,53 +1382,26 @@ export class ExtensionRunner {
|
|
|
1275
1382
|
* exactly as before.
|
|
1276
1383
|
*/
|
|
1277
1384
|
async runSessionShutdownHandler(extensionPath, event, handler, budget) {
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1385
|
+
const outcome = await runBudgetedHandler({
|
|
1386
|
+
extensionPath,
|
|
1387
|
+
event: "session_shutdown",
|
|
1388
|
+
budget,
|
|
1389
|
+
run: (signal) => handler({ ...event, signal }, this.createContext(extensionPath)),
|
|
1390
|
+
onStall: (stall) => {
|
|
1391
|
+
if (stall.stage !== "warn")
|
|
1392
|
+
return;
|
|
1393
|
+
const capNote = budget.timeoutMs > 0 ? ` (hard cap ${budget.timeoutMs}ms)` : "";
|
|
1394
|
+
console.warn(`Extension ${extensionPath} is still running its session_shutdown handler after ${stall.elapsedMs}ms${capNote}.`);
|
|
1395
|
+
},
|
|
1287
1396
|
});
|
|
1288
|
-
if (
|
|
1289
|
-
let warnTimer;
|
|
1290
|
-
let capTimer;
|
|
1291
|
-
let timedOut = false;
|
|
1292
|
-
try {
|
|
1293
|
-
timedOut = await new Promise((resolve) => {
|
|
1294
|
-
if (budget.warnMs > 0) {
|
|
1295
|
-
warnTimer = setTimeout(() => {
|
|
1296
|
-
const capNote = budget.timeoutMs > 0 ? ` (hard cap ${budget.timeoutMs}ms)` : "";
|
|
1297
|
-
console.warn(`Extension ${extensionPath} is still running its session_shutdown handler after ${Date.now() - startedAt}ms${capNote}.`);
|
|
1298
|
-
}, budget.warnMs);
|
|
1299
|
-
}
|
|
1300
|
-
if (budget.timeoutMs > 0) {
|
|
1301
|
-
capTimer = setTimeout(() => resolve(true), budget.timeoutMs);
|
|
1302
|
-
}
|
|
1303
|
-
void settled.then(() => resolve(false));
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
finally {
|
|
1307
|
-
clearTimeout(warnTimer);
|
|
1308
|
-
clearTimeout(capTimer);
|
|
1309
|
-
}
|
|
1310
|
-
if (timedOut) {
|
|
1311
|
-
controller.abort(new Error(`session_shutdown handler timed out after ${budget.timeoutMs}ms`));
|
|
1312
|
-
this.emitError({
|
|
1313
|
-
extensionPath,
|
|
1314
|
-
event: "session_shutdown",
|
|
1315
|
-
error: `handler timed out after ${budget.timeoutMs}ms`,
|
|
1316
|
-
});
|
|
1317
|
-
return;
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
else {
|
|
1321
|
-
await settled;
|
|
1322
|
-
}
|
|
1323
|
-
if (outcome && !outcome.ok) {
|
|
1397
|
+
if (outcome.kind === "failed")
|
|
1324
1398
|
throw outcome.error;
|
|
1399
|
+
if (outcome.kind === "timed_out") {
|
|
1400
|
+
this.emitError({
|
|
1401
|
+
extensionPath,
|
|
1402
|
+
event: "session_shutdown",
|
|
1403
|
+
error: `handler timed out after ${budget.timeoutMs}ms`,
|
|
1404
|
+
});
|
|
1325
1405
|
}
|
|
1326
1406
|
}
|
|
1327
1407
|
emit(event) {
|
|
@@ -1329,14 +1409,17 @@ export class ExtensionRunner {
|
|
|
1329
1409
|
// Turns requested from any session_start handler start after the last one returns (senpi#1972).
|
|
1330
1410
|
if (event.type === "session_start")
|
|
1331
1411
|
return this.sessionStartTurns.dispatch(() => this.dispatchEmit(event));
|
|
1412
|
+
if (QUEUED_LIFECYCLE_EVENTS.has(event.type)) {
|
|
1413
|
+
return this.dispatchLifecycle(event);
|
|
1414
|
+
}
|
|
1332
1415
|
return this.dispatchEmit(event);
|
|
1333
1416
|
}
|
|
1334
1417
|
async dispatchEmit(event) {
|
|
1335
1418
|
let result;
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1339
|
-
// The budget is resolved lazily so runners with no shutdown handler read no settings.
|
|
1419
|
+
// Host-bounded events: a hung session_shutdown handler must not hold Ctrl+C / quit / reload / new / resume
|
|
1420
|
+
// hostage (agent lifecycle events take `dispatchLifecycle`, senpi#3650). Every other event still awaits its
|
|
1421
|
+
// handlers without a cap (ask-user and approval dialogs legitimately block).
|
|
1422
|
+
// The shutdown budget is resolved lazily so runners with no shutdown handler read no settings.
|
|
1340
1423
|
const isSessionShutdown = event.type === "session_shutdown";
|
|
1341
1424
|
let shutdownBudget;
|
|
1342
1425
|
for (const { ext, handlers } of snapshotEventHandlers(this.extensions, event.type)) {
|
|
@@ -1416,40 +1499,42 @@ export class ExtensionRunner {
|
|
|
1416
1499
|
}
|
|
1417
1500
|
return result;
|
|
1418
1501
|
}
|
|
1419
|
-
|
|
1502
|
+
emitMessageEnd(event) {
|
|
1420
1503
|
let currentMessage = event.message;
|
|
1421
1504
|
let modified = false;
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
currentMessage = handlerResult.message;
|
|
1438
|
-
modified = true;
|
|
1505
|
+
const entries = flattenHandlers(this.extensions, "message_end");
|
|
1506
|
+
return this.walkHandlers({
|
|
1507
|
+
event: "message_end",
|
|
1508
|
+
length: entries.length,
|
|
1509
|
+
extensionPath: (index) => entryAt(entries, index).path,
|
|
1510
|
+
start: (index) => {
|
|
1511
|
+
const { path, handler } = entryAt(entries, index);
|
|
1512
|
+
const currentEvent = { ...event, message: currentMessage };
|
|
1513
|
+
return handler(currentEvent, this.createContext(path));
|
|
1514
|
+
},
|
|
1515
|
+
settle: (index, outcome) => {
|
|
1516
|
+
const { path } = entryAt(entries, index);
|
|
1517
|
+
if (!outcome.ok) {
|
|
1518
|
+
this.reportHandlerFailure(path, "message_end", outcome.error);
|
|
1519
|
+
return;
|
|
1439
1520
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1521
|
+
const handlerResult = outcome.value;
|
|
1522
|
+
if (!handlerResult?.message)
|
|
1523
|
+
return;
|
|
1524
|
+
if (handlerResult.message.role !== currentMessage.role) {
|
|
1443
1525
|
this.emitError({
|
|
1444
|
-
extensionPath:
|
|
1526
|
+
extensionPath: path,
|
|
1445
1527
|
event: "message_end",
|
|
1446
|
-
error: message,
|
|
1447
|
-
stack,
|
|
1528
|
+
error: "message_end handlers must return a message with the same role",
|
|
1448
1529
|
});
|
|
1530
|
+
return;
|
|
1449
1531
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1532
|
+
currentMessage = handlerResult.message;
|
|
1533
|
+
modified = true;
|
|
1534
|
+
},
|
|
1535
|
+
skip: (index, verdict, budget) => this.reportStoppedHandler(entryAt(entries, index).path, "message_end", verdict, budget),
|
|
1536
|
+
finish: () => (modified ? currentMessage : undefined),
|
|
1537
|
+
});
|
|
1453
1538
|
}
|
|
1454
1539
|
async emitToolResult(event) {
|
|
1455
1540
|
const currentEvent = { ...event };
|
|
@@ -2,6 +2,7 @@ import { execFile } from "child_process";
|
|
|
2
2
|
import { existsSync, readFileSync, statSync, unwatchFile, watchFile } from "fs";
|
|
3
3
|
import { dirname, join, resolve } from "path";
|
|
4
4
|
import { closeWatcher, FS_WATCH_RETRY_DELAY_MS, watchWithErrorHandler } from "../utils/fs-watch.js";
|
|
5
|
+
import { isWSL } from "../utils/wsl.js";
|
|
5
6
|
/**
|
|
6
7
|
* Find git metadata paths by walking up from cwd.
|
|
7
8
|
* Handles both regular git repos (.git is a directory) and worktrees (.git is a file).
|
|
@@ -61,7 +62,7 @@ function resolveBranchWithGitAsync(repoDir) {
|
|
|
61
62
|
});
|
|
62
63
|
}
|
|
63
64
|
function isWslEnvironment() {
|
|
64
|
-
return process.platform === "linux" &&
|
|
65
|
+
return process.platform === "linux" && isWSL();
|
|
65
66
|
}
|
|
66
67
|
function isWindowsMountedRepoPath(repoDir) {
|
|
67
68
|
return /^\/mnt\/[a-z](?:\/|$)/i.test(repoDir);
|