@code-yeongyu/senpi 2026.11.13 → 2026.11.15-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 +45 -0
- package/dist/bundle/chunks/{args-H56W53X6.js → args-JWBI5LNO.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-MNN2TINI.js → azure-openai-responses-PEMLRPJ5.js} +1 -1
- package/dist/bundle/chunks/{chunk-HZQHA4PA.js → chunk-3RILB2OD.js} +1 -1
- package/dist/bundle/chunks/{chunk-BKDXY6EM.js → chunk-5NECOJNK.js} +1 -1
- package/dist/bundle/chunks/{chunk-Y4PFEXFN.js → chunk-64FJ4QQJ.js} +1 -1
- package/dist/bundle/chunks/chunk-6QQL3SKM.js +2 -0
- package/dist/bundle/chunks/{chunk-7T2LAWZ4.js → chunk-73HGQTJN.js} +1 -1
- package/dist/bundle/chunks/{chunk-U4FAWC6C.js → chunk-74HCKLRH.js} +1 -1
- package/dist/bundle/chunks/chunk-75O3CWR5.js +2 -0
- package/dist/bundle/chunks/{chunk-AZ27TE3H.js → chunk-7BX3JQ7F.js} +1 -1
- package/dist/bundle/chunks/{chunk-NDI6N4NJ.js → chunk-7CXLNKEH.js} +1 -1
- package/dist/bundle/chunks/{chunk-E6ZBTSC6.js → chunk-A3LGSX7Z.js} +1 -1
- package/dist/bundle/chunks/chunk-AH2YAO7R.js +2 -0
- package/dist/bundle/chunks/{chunk-6FE42M54.js → chunk-BMADOJHZ.js} +2 -2
- package/dist/bundle/chunks/{chunk-7GD2EO6T.js → chunk-FYVQEZYN.js} +1 -1
- package/dist/bundle/chunks/{chunk-EQT4WNCW.js → chunk-HTWWKJJI.js} +2 -2
- package/dist/bundle/chunks/{chunk-SFACYIB6.js → chunk-IK3S2NDB.js} +1 -1
- package/dist/bundle/chunks/{chunk-V4VDIV3U.js → chunk-PWA75RQU.js} +1 -1
- package/dist/bundle/chunks/{chunk-IREUIMFE.js → chunk-PWTXZXHH.js} +1 -1
- package/dist/bundle/chunks/{chunk-KKB3K5CV.js → chunk-Q4RJ4WLE.js} +1 -1
- package/dist/bundle/chunks/{chunk-NXGSYUMV.js → chunk-QSU2456D.js} +2 -2
- package/dist/bundle/chunks/chunk-QVG5SUSV.js +2 -0
- package/dist/bundle/chunks/chunk-RLIY3AUC.js +2 -0
- package/dist/bundle/chunks/{chunk-I4N2OIY4.js → chunk-RRFDLGX6.js} +26 -26
- package/dist/bundle/chunks/{chunk-JLCZIEBC.js → chunk-THYIV26N.js} +1 -1
- package/dist/bundle/chunks/{chunk-NI2AOYP6.js → chunk-TPLEPLEB.js} +3 -2
- package/dist/bundle/chunks/{chunk-CU6HUYL7.js → chunk-WAGFTIT4.js} +1 -1
- package/dist/bundle/chunks/{chunk-UYPH6RHY.js → chunk-Z4PN2MDA.js} +2 -2
- package/dist/bundle/chunks/{chunk-JJYN347Z.js → chunk-ZJUL5ODW.js} +1 -1
- package/dist/bundle/chunks/{cli-main-MY53RXU3.js → cli-main-GNQY2LTU.js} +1 -1
- package/dist/bundle/chunks/credential-accounts-RXZIUKFM.js +2 -0
- package/dist/bundle/chunks/github-copilot.js +1 -1
- package/dist/bundle/chunks/{hand-entry-AXTVMVJK.js → hand-entry-D3DNH6XL.js} +1 -1
- package/dist/bundle/chunks/{headless-host-SU5O2KZ2.js → headless-host-OEK6W65T.js} +1 -1
- package/dist/bundle/chunks/{health-write-TN7EXGLI.js → health-write-44JERGEJ.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-TFQ4HJNO.js → help-fast-path-JWHUCE4E.js} +1 -1
- package/dist/bundle/chunks/{host-command-FG56Y53E.js → host-command-O2K2WIPO.js} +1 -1
- package/dist/bundle/chunks/{host-session-command-UFEOW7EP.js → host-session-command-K3MR6RJY.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-ATBPY4M5.js +2 -0
- package/dist/bundle/chunks/{login-health-XBTIORM4.js → login-health-V4B6LSCP.js} +1 -1
- package/dist/bundle/chunks/main-OMFVSMUR.js +2 -0
- package/dist/bundle/chunks/{multi-session-host-Q6SHI22A.js → multi-session-host-ACQ5N4UB.js} +6 -6
- package/dist/bundle/chunks/openai-codex-responses-G7ZXJBU6.js +10 -0
- package/dist/bundle/chunks/openai-responses-3TAF7WQF.js +5 -0
- package/dist/bundle/chunks/{package-manager-cli-LILAWI3Q.js → package-manager-cli-2VMRV4HL.js} +3 -3
- package/dist/bundle/chunks/{role-KODRTWH5.js → role-BTH5PVME.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-2KNROTYB.js → rotation-stream-ZGPXLY62.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-XUK5SYKH.js +2 -0
- package/dist/bundle/chunks/{run-attached-tui-OEFHV5CZ.js → run-attached-tui-RVVYEINN.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-VO4RP2KX.js → schedule-command-6TAAG2KU.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-CCVRJHSC.js → session-control-endpoint-Y5EIFFPY.js} +1 -1
- package/dist/bundle/chunks/{session-picker-VN5I2ZDI.js → session-picker-PRSZJYY4.js} +1 -1
- package/dist/bundle/chunks/session-slot-N4E4OSCX.js +2 -0
- package/dist/bundle/chunks/session-worker.js +74 -72
- package/dist/bundle/chunks/{state-store-7RVODZJU.js → state-store-I6MUHET3.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-session-runtime.js +3 -3
- package/dist/core/agent-session.d.ts +5 -3
- package/dist/core/agent-session.js +20 -8
- package/dist/core/credential-accounts.d.ts +5 -3
- package/dist/core/credential-accounts.js +8 -6
- package/dist/core/credential-pool/classify.js +5 -1
- package/dist/core/extensions/builtin/anthropic-subscription/content-blocks.d.ts +6 -0
- package/dist/core/extensions/builtin/anthropic-subscription/content-blocks.js +14 -0
- package/dist/core/extensions/builtin/anthropic-subscription/prompt-bridge.js +3 -1
- package/dist/core/extensions/builtin/anthropic-subscription/session-sync.js +4 -1
- package/dist/core/extensions/builtin/permission-system/index.js +6 -1
- package/dist/core/extensions/loader.js +17 -1
- package/dist/core/extensions/runner.js +16 -2
- package/dist/core/extensions/tool-call-signal.d.ts +8 -0
- package/dist/core/extensions/tool-call-signal.js +14 -0
- package/dist/core/extensions/two-sided-forwarding.d.ts +6 -0
- package/dist/core/extensions/types.d.ts +26 -1
- package/dist/core/extensions/types.js +10 -0
- package/dist/core/reasoning-baseline.d.ts +8 -1
- package/dist/core/reasoning-baseline.js +24 -0
- package/dist/core/retry-fallback/chains.js +5 -1
- package/dist/core/retry-fallback/controller.d.ts +6 -1
- package/dist/core/retry-fallback/controller.js +11 -3
- package/dist/core/retry-fallback/expansion.js +18 -4
- package/dist/core/retry-fallback/settings.d.ts +7 -6
- package/dist/core/retry-fallback/settings.js +9 -8
- package/dist/core/runtime/bootstrap.d.ts +3 -0
- package/dist/core/runtime/bootstrap.js +1 -0
- package/dist/core/session-cwd.d.ts +15 -1
- package/dist/core/session-cwd.js +22 -1
- package/dist/core/tools/bash.d.ts +12 -0
- package/dist/core/tools/bash.js +30 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/modes/rpc/connection-handler.js +4 -0
- package/dist/modes/rpc/method-classes-schema-session.d.ts +3 -0
- package/dist/modes/rpc/method-classes-schema-session.js +6 -2
- package/dist/modes/rpc/method-classes-schema.d.ts +3 -0
- package/dist/modes/rpc/project-expected-cwd.d.ts +29 -0
- package/dist/modes/rpc/project-expected-cwd.js +45 -0
- package/dist/modes/rpc/rpc-error-types.d.ts +7 -1
- package/dist/modes/rpc/rpc-error-types.js +6 -0
- package/dist/modes/rpc/rpc-session-command-types.d.ts +3 -0
- package/dist/modes/rpc/session-router-close.js +6 -1
- package/dist/modes/rpc/session-router-open.js +5 -0
- package/dist/modes/rpc/session-status.d.ts +1 -1
- package/dist/modes/rpc/session-status.js +2 -0
- package/dist/package-manager-cli.d.ts +14 -0
- package/dist/package-manager-cli.js +20 -4
- package/dist/utils/version-check.d.ts +10 -7
- package/dist/utils/version-check.js +24 -5
- package/docs/extensions.md +12 -0
- package/docs/rpc.md +28 -13
- package/docs/settings.md +1 -1
- package/package.json +7 -7
- package/dist/bundle/chunks/chunk-GLTOBXZY.js +0 -2
- package/dist/bundle/chunks/chunk-PBUV2Z3R.js +0 -2
- package/dist/bundle/chunks/chunk-WRBOKYPS.js +0 -2
- package/dist/bundle/chunks/chunk-YT2ZSZOY.js +0 -2
- package/dist/bundle/chunks/credential-accounts-ZB5IXHIG.js +0 -2
- package/dist/bundle/chunks/interactive-mode-PHES47E3.js +0 -2
- package/dist/bundle/chunks/main-PR4FI6HQ.js +0 -2
- package/dist/bundle/chunks/openai-codex-responses-VRGOAFEG.js +0 -10
- package/dist/bundle/chunks/openai-responses-I7PXRWCM.js +0 -4
- package/dist/bundle/chunks/rpc-mode-CGVK4SSA.js +0 -2
- package/dist/bundle/chunks/session-slot-GHJVOSG7.js +0 -2
|
@@ -55,7 +55,7 @@ import { createLifecycleApi } from "./lifecycle-api.js";
|
|
|
55
55
|
import { withProviderLoginStart } from "./provider-login-start.js";
|
|
56
56
|
import { createSessionDialogApi } from "./session-dialog-api.js";
|
|
57
57
|
import { StaleExtensionContextError } from "./stale-context-error.js";
|
|
58
|
-
import { DEFAULT_EXTENSION_SESSION_PROFILE, EMPTY_SESSION_CONTEXT } from "./types.js";
|
|
58
|
+
import { DEFAULT_EXTENSION_SESSION_PROFILE, DeviceMachineConflictError, EMPTY_SESSION_CONTEXT } from "./types.js";
|
|
59
59
|
function sessionProfile(options) {
|
|
60
60
|
return {
|
|
61
61
|
sessionKind: options?.sessionKind ?? "interactive",
|
|
@@ -209,6 +209,7 @@ export function createExtensionRuntime() {
|
|
|
209
209
|
}
|
|
210
210
|
};
|
|
211
211
|
const runtime = {
|
|
212
|
+
deviceMachine: undefined,
|
|
212
213
|
sendMessage: notInitialized,
|
|
213
214
|
sendUserMessage: notInitialized,
|
|
214
215
|
appendEntry: notInitialized,
|
|
@@ -349,6 +350,7 @@ function createExtensionAPI(extension, runtime, cwd, eventBus, session) {
|
|
|
349
350
|
}
|
|
350
351
|
runtime.assertActive();
|
|
351
352
|
};
|
|
353
|
+
let deviceMachineStaged = false;
|
|
352
354
|
const applyRuntimeChange = (change) => {
|
|
353
355
|
if (state === "loading")
|
|
354
356
|
pendingRuntimeChanges.push(change);
|
|
@@ -356,6 +358,7 @@ function createExtensionAPI(extension, runtime, cwd, eventBus, session) {
|
|
|
356
358
|
change();
|
|
357
359
|
};
|
|
358
360
|
const clearPending = () => {
|
|
361
|
+
deviceMachineStaged = false;
|
|
359
362
|
pendingFlagValues.clear();
|
|
360
363
|
pendingRuntimeChanges.length = 0;
|
|
361
364
|
loadingUnsubscribers.length = 0;
|
|
@@ -423,6 +426,19 @@ function createExtensionAPI(extension, runtime, cwd, eventBus, session) {
|
|
|
423
426
|
extension.handlers.delete(event);
|
|
424
427
|
};
|
|
425
428
|
},
|
|
429
|
+
registerDeviceMachine(machine) {
|
|
430
|
+
assertActive();
|
|
431
|
+
if (runtime.deviceMachine !== undefined) {
|
|
432
|
+
throw new DeviceMachineConflictError(runtime.deviceMachine.extensionPath, extension.path);
|
|
433
|
+
}
|
|
434
|
+
if (deviceMachineStaged)
|
|
435
|
+
throw new DeviceMachineConflictError(extension.path, extension.path);
|
|
436
|
+
deviceMachineStaged = true;
|
|
437
|
+
// Staged like provider registrations: a factory that throws after registering leaves no device behind.
|
|
438
|
+
applyRuntimeChange(() => {
|
|
439
|
+
runtime.deviceMachine = { machine, extensionPath: extension.path };
|
|
440
|
+
});
|
|
441
|
+
},
|
|
426
442
|
registerTool(tool) {
|
|
427
443
|
runtime.assertActive();
|
|
428
444
|
if (tool.name === "tool_search" && extension.sourceInfo.source !== "builtin") {
|
|
@@ -30,6 +30,7 @@ import { QuitDeadline } from "./shutdown-quit-budget.js";
|
|
|
30
30
|
import { StaleExtensionContextError } from "./stale-context-error.js";
|
|
31
31
|
import { RUNTIME_EXTENSION_PATH } from "./types.js";
|
|
32
32
|
import { servesForwardedHandler, toolEventHandlers, twoSidedExtensions, } from "./two-sided-forwarding.js";
|
|
33
|
+
import { combinedToolCallSignal } from "./tool-call-signal.js";
|
|
33
34
|
const RUNTIME_CONTEXT_MEMBERS = new Set([
|
|
34
35
|
"offload",
|
|
35
36
|
"ui",
|
|
@@ -48,6 +49,7 @@ const RUNTIME_CONTEXT_MEMBERS = new Set([
|
|
|
48
49
|
"getImageSettings",
|
|
49
50
|
"getRegisteredMcpServers",
|
|
50
51
|
"extensionConnectsMcpServers",
|
|
52
|
+
"signal",
|
|
51
53
|
]);
|
|
52
54
|
/** Session identity a context of a runner without a session does not have; dispatchers treat its absence as context-free. */
|
|
53
55
|
const SESSION_IDENTITY_MEMBERS = new Set(["sessionManager", "goalStoreFile"]);
|
|
@@ -2027,9 +2029,21 @@ export class ExtensionRunner {
|
|
|
2027
2029
|
async emitToolCall(event, options = {}) {
|
|
2028
2030
|
let result;
|
|
2029
2031
|
for (const { ext, handlers: own } of snapshotEventHandlers(this.extensions, "tool_call")) {
|
|
2030
|
-
const handlers = toolEventHandlers(ext, own, "tool_call", options,
|
|
2032
|
+
const handlers = toolEventHandlers(ext, own, "tool_call", options,
|
|
2033
|
+
// The forwarder carries the event only, never the call's signal (senpi#6679 L1): a handler served on the
|
|
2034
|
+
// loop side of a forward sees the loop-hook signal, the exception ToolEventDispatchOptions.signal documents.
|
|
2035
|
+
(index) => async () => options.forward?.toolCall(ext.path, index, event));
|
|
2031
2036
|
for (const handler of handlers) {
|
|
2032
|
-
const
|
|
2037
|
+
const baseContext = this.createContext();
|
|
2038
|
+
if (options.signal !== undefined) {
|
|
2039
|
+
const callSignal = combinedToolCallSignal(options.signal, baseContext.signal);
|
|
2040
|
+
Object.defineProperty(baseContext, "signal", {
|
|
2041
|
+
get: () => callSignal,
|
|
2042
|
+
configurable: true,
|
|
2043
|
+
enumerable: true,
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
const hookRun = this.beginToolHookRun(baseContext, {
|
|
2033
2047
|
hookName: "PreToolUse",
|
|
2034
2048
|
toolName: event.toolName,
|
|
2035
2049
|
toolCallId: event.toolCallId,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signal a nested call's hooks and tool run serve: the call's own signal combined with the turn's
|
|
3
|
+
* (senpi#6679 M1), so aborting either ends the call — and its permission prompt. A call signal not linked to
|
|
4
|
+
* the turn must not replace the turn's (the prompt would survive a whole-turn abort); the turn's must not
|
|
5
|
+
* silence the call's own (#6676). The union never aborts either original controller.
|
|
6
|
+
*/
|
|
7
|
+
export declare function combinedToolCallSignal(callSignal: AbortSignal | undefined, turnSignal: AbortSignal | undefined): AbortSignal | undefined;
|
|
8
|
+
//# sourceMappingURL=tool-call-signal.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signal a nested call's hooks and tool run serve: the call's own signal combined with the turn's
|
|
3
|
+
* (senpi#6679 M1), so aborting either ends the call — and its permission prompt. A call signal not linked to
|
|
4
|
+
* the turn must not replace the turn's (the prompt would survive a whole-turn abort); the turn's must not
|
|
5
|
+
* silence the call's own (#6676). The union never aborts either original controller.
|
|
6
|
+
*/
|
|
7
|
+
export function combinedToolCallSignal(callSignal, turnSignal) {
|
|
8
|
+
if (callSignal === undefined)
|
|
9
|
+
return turnSignal;
|
|
10
|
+
if (turnSignal === undefined || turnSignal === callSignal)
|
|
11
|
+
return callSignal;
|
|
12
|
+
return AbortSignal.any([callSignal, turnSignal]);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=tool-call-signal.js.map
|
|
@@ -27,6 +27,12 @@ export interface ToolEventDispatchOptions {
|
|
|
27
27
|
readonly forward?: ToolEventForwarder;
|
|
28
28
|
/** Where the loop runs: run only this one handler (a forwarded request). */
|
|
29
29
|
readonly only?: ForwardedHandler;
|
|
30
|
+
/**
|
|
31
|
+
* The call's own abort signal: a `tool_call` handler's `ctx.signal` for this call, combined with the turn's so
|
|
32
|
+
* aborting either ends it (senpi#6676, senpi#6679 M1). Forwarded handlers are the exception (senpi#6679 L1): the
|
|
33
|
+
* forwarder carries the event only, so a handler served on the loop side of a forward sees its loop-hook signal.
|
|
34
|
+
*/
|
|
35
|
+
readonly signal?: AbortSignal;
|
|
30
36
|
}
|
|
31
37
|
/**
|
|
32
38
|
* The extensions whose undeclared registrations span loop-side events (`loop` or `split` in the event placement) and
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - Interact with the user via UI primitives
|
|
9
9
|
*/
|
|
10
10
|
import type { PromptSizeReport } from "../prompt-size-report.ts";
|
|
11
|
+
import type { ToolMachine } from "../tools/machine.ts";
|
|
11
12
|
import type { InputOrigin } from "../input-origin.ts";
|
|
12
13
|
import type { LifecycleApi } from "../session-lifecycle-types.ts";
|
|
13
14
|
import type { UploadRedaction, UploadRedactionOptions } from "../share/upload-redaction.ts";
|
|
@@ -646,7 +647,7 @@ export interface ExtensionContext {
|
|
|
646
647
|
* `ExtensionRunner` built without a session manager has no `sessionManager` or `goalStoreFile`, and every other
|
|
647
648
|
* member outside this set throws a "loop-side and not bound" error.
|
|
648
649
|
*/
|
|
649
|
-
export type ExtensionRuntimeContext = Pick<ExtensionContext, "offload" | "ui" | "mode" | "hasUI" | "cwd" | "agentDir" | "loadedExtensionPaths" | "modelRegistry" | "isProjectTrusted" | "kernelTools" | "evalHandleHost" | "shutdown" | "getLookAtSettings" | "getAskUserSettings" | "getImageSettings" | "getRegisteredMcpServers" | "extensionConnectsMcpServers">;
|
|
650
|
+
export type ExtensionRuntimeContext = Pick<ExtensionContext, "offload" | "ui" | "mode" | "hasUI" | "cwd" | "agentDir" | "loadedExtensionPaths" | "modelRegistry" | "isProjectTrusted" | "kernelTools" | "evalHandleHost" | "shutdown" | "getLookAtSettings" | "getAskUserSettings" | "getImageSettings" | "getRegisteredMcpServers" | "extensionConnectsMcpServers" | "signal">;
|
|
650
651
|
/** Provider request prefix of the next user turn (see `ExtensionContext.getPromptCachePrefixRequest`). */
|
|
651
652
|
export interface PromptCachePrefixRequest {
|
|
652
653
|
readonly model: Model<Api>;
|
|
@@ -2253,6 +2254,13 @@ export interface ExtensionAPI {
|
|
|
2253
2254
|
description?: string;
|
|
2254
2255
|
handler: (ctx: ExtensionContext) => Promise<void> | void;
|
|
2255
2256
|
}): void;
|
|
2257
|
+
/**
|
|
2258
|
+
* Register the operation layer of the device this session's tools run on. Read when a routed tool executes; only
|
|
2259
|
+
* tools named in OMO_TOOLS_VIA_DAEMON use it (today: bash). One registration per session: a second one, from any
|
|
2260
|
+
* extension, throws DeviceMachineConflictError naming the extension that registered first, so two extensions
|
|
2261
|
+
* cannot silently fight over where commands run.
|
|
2262
|
+
*/
|
|
2263
|
+
registerDeviceMachine(machine: ToolMachine): void;
|
|
2256
2264
|
/** Register a CLI flag. */
|
|
2257
2265
|
registerFlag(name: string, options: {
|
|
2258
2266
|
description?: string;
|
|
@@ -2762,6 +2770,11 @@ export type SetSessionNameHandler = (name: string) => void;
|
|
|
2762
2770
|
export type GetSessionNameHandler = () => string | undefined;
|
|
2763
2771
|
export type ExecuteToolUpdateCallback<T = unknown> = AgentToolUpdateCallback<T>;
|
|
2764
2772
|
export interface ExecuteToolOptions<TDetails = unknown> {
|
|
2773
|
+
/**
|
|
2774
|
+
* The call's own abort signal: the call's RUN follows it alone, so a detached caller's work survives a
|
|
2775
|
+
* later turn's abort (senpi#6679 review M2). The call's PERMISSION PROMPT runs under its union with the
|
|
2776
|
+
* turn's signal: aborting the turn also ends an unanswered prompt even when this signal is unlinked (senpi#6679).
|
|
2777
|
+
*/
|
|
2765
2778
|
signal?: AbortSignal;
|
|
2766
2779
|
onUpdate?: ExecuteToolUpdateCallback<TDetails>;
|
|
2767
2780
|
/**
|
|
@@ -2837,6 +2850,11 @@ export type PendingProviderRegistration = ({
|
|
|
2837
2850
|
*/
|
|
2838
2851
|
export interface ExtensionRuntimeState {
|
|
2839
2852
|
flagValues: Map<string, boolean | string>;
|
|
2853
|
+
/** The device the session's tools run on, and the extension that registered it (registerDeviceMachine). */
|
|
2854
|
+
deviceMachine: {
|
|
2855
|
+
readonly machine: ToolMachine;
|
|
2856
|
+
readonly extensionPath: string;
|
|
2857
|
+
} | undefined;
|
|
2840
2858
|
/** Legacy provider-config registrations queued during extension loading, processed when runner binds. */
|
|
2841
2859
|
pendingProviderRegistrations: PendingProviderConfigRegistration[];
|
|
2842
2860
|
/** Native pi-ai provider registrations queued during extension loading, processed when runner binds. */
|
|
@@ -3128,4 +3146,11 @@ export interface ExtensionError {
|
|
|
3128
3146
|
error: string;
|
|
3129
3147
|
stack?: string;
|
|
3130
3148
|
}
|
|
3149
|
+
/** A second registerDeviceMachine in one session. */
|
|
3150
|
+
export declare class DeviceMachineConflictError extends Error {
|
|
3151
|
+
readonly code: "device_machine_conflict";
|
|
3152
|
+
readonly registeredBy: string;
|
|
3153
|
+
readonly attemptedBy: string;
|
|
3154
|
+
constructor(registeredBy: string, attemptedBy: string);
|
|
3155
|
+
}
|
|
3131
3156
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -98,4 +98,14 @@ export class ExecuteToolError extends Error {
|
|
|
98
98
|
* background session-title generation.
|
|
99
99
|
*/
|
|
100
100
|
export const RUNTIME_EXTENSION_PATH = "<runtime>";
|
|
101
|
+
/** A second registerDeviceMachine in one session. */
|
|
102
|
+
export class DeviceMachineConflictError extends Error {
|
|
103
|
+
constructor(registeredBy, attemptedBy) {
|
|
104
|
+
super(`registerDeviceMachine: extension "${attemptedBy}" tried to register a device machine, but extension "${registeredBy}" already registered one for this session`);
|
|
105
|
+
this.code = "device_machine_conflict";
|
|
106
|
+
this.name = "DeviceMachineConflictError";
|
|
107
|
+
this.registeredBy = registeredBy;
|
|
108
|
+
this.attemptedBy = attemptedBy;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
101
111
|
//# sourceMappingURL=types.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
1
|
+
import type { AgentMessage, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { SessionEntry } from "./session-manager.ts";
|
|
3
3
|
/**
|
|
4
4
|
* The request-level effort of every request while a session holds a `configuration_update`: the baseline
|
|
@@ -6,6 +6,13 @@ import type { SessionEntry } from "./session-manager.ts";
|
|
|
6
6
|
* (senpi#2094, senpi#4116). Without a valid baseline it is the live level.
|
|
7
7
|
*/
|
|
8
8
|
export declare function requestThinkingLevel(baseline: string | undefined, thinkingLevel: ThinkingLevel): ThinkingLevel;
|
|
9
|
+
/**
|
|
10
|
+
* The request context with the live level in force. On a flagged model the newest `configuration_update` is the
|
|
11
|
+
* effort a request carries, and a level that arrived without one (a fallback rung's, a favourite cycle's) would
|
|
12
|
+
* leave an older update in force. The live level rides the request only, never the session file, so a resumed
|
|
13
|
+
* session keeps the user's level (senpi#6655). It goes before the turn's user message, like a recorded update.
|
|
14
|
+
*/
|
|
15
|
+
export declare function withLiveConfigurationUpdate(messages: AgentMessage[], thinkingLevel: ThinkingLevel, timestamp: number): AgentMessage[];
|
|
9
16
|
/**
|
|
10
17
|
* The baseline a resumed session restores, replaying the rules `AgentSession` applies live: an in-model effort
|
|
11
18
|
* change (a `thinking_level_change` immediately followed by its `configuration_update`) pins the level that was
|
|
@@ -7,6 +7,30 @@ import { isValidThinkingLevel } from "../cli/args.js";
|
|
|
7
7
|
export function requestThinkingLevel(baseline, thinkingLevel) {
|
|
8
8
|
return baseline !== undefined && isValidThinkingLevel(baseline) ? baseline : thinkingLevel;
|
|
9
9
|
}
|
|
10
|
+
function isConfigurationUpdate(message) {
|
|
11
|
+
return message.role === "configurationUpdate";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The request context with the live level in force. On a flagged model the newest `configuration_update` is the
|
|
15
|
+
* effort a request carries, and a level that arrived without one (a fallback rung's, a favourite cycle's) would
|
|
16
|
+
* leave an older update in force. The live level rides the request only, never the session file, so a resumed
|
|
17
|
+
* session keeps the user's level (senpi#6655). It goes before the turn's user message, like a recorded update.
|
|
18
|
+
*/
|
|
19
|
+
export function withLiveConfigurationUpdate(messages, thinkingLevel, timestamp) {
|
|
20
|
+
const newestIndex = messages.findLastIndex(isConfigurationUpdate);
|
|
21
|
+
const newest = messages[newestIndex];
|
|
22
|
+
if (newest === undefined || !isConfigurationUpdate(newest) || newest.effort === thinkingLevel)
|
|
23
|
+
return messages;
|
|
24
|
+
const update = {
|
|
25
|
+
role: "configurationUpdate",
|
|
26
|
+
content: [],
|
|
27
|
+
effort: thinkingLevel,
|
|
28
|
+
timestamp,
|
|
29
|
+
};
|
|
30
|
+
const lastUserIndex = messages.findLastIndex((message) => message.role === "user");
|
|
31
|
+
const at = lastUserIndex > newestIndex ? lastUserIndex : messages.length;
|
|
32
|
+
return [...messages.slice(0, at), update, ...messages.slice(at)];
|
|
33
|
+
}
|
|
10
34
|
/**
|
|
11
35
|
* The baseline a resumed session restores, replaying the rules `AgentSession` applies live: an in-model effort
|
|
12
36
|
* change (a `thinking_level_change` immediately followed by its `configuration_update`) pins the level that was
|
|
@@ -130,7 +130,11 @@ export function canonicalizeFallbackChains(chains, lookup) {
|
|
|
130
130
|
const canonical = {};
|
|
131
131
|
const explicitKeys = new Set();
|
|
132
132
|
const tombstones = new Set();
|
|
133
|
-
|
|
133
|
+
// Two entries can name one rung (`k3` and `kimi-k3` both reach Kimi Code's `k3`); the first keeps its place.
|
|
134
|
+
const expandEntries = (entries, keySelector, sessionProvider) => [
|
|
135
|
+
...new Set(expandEach(entries, keySelector, sessionProvider)),
|
|
136
|
+
];
|
|
137
|
+
const expandEach = (entries, keySelector, sessionProvider) => entries.flatMap((entry) => {
|
|
134
138
|
const bare = parseBareSelector(entry);
|
|
135
139
|
if (bare) {
|
|
136
140
|
return rankFamilyModels(models, bare.family, tiers, { limit: MAX_PROVIDERS_PER_FAMILY, sessionProvider })
|
|
@@ -110,7 +110,12 @@ export declare class RetryFallbackController {
|
|
|
110
110
|
}, failureObserved?: boolean): Promise<boolean>;
|
|
111
111
|
private walkFallback;
|
|
112
112
|
private applyCandidate;
|
|
113
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* The chain governing `current`, when fallback is enabled. An active episode's chain wins over the
|
|
115
|
+
* rung's own chain, so the walk continues down the chain it started on: Opus 5.5 -> Fable 5.2 -> Kimi
|
|
116
|
+
* never detours through Fable's own chain, and a Fable session that reached Opus 5.5 never takes
|
|
117
|
+
* Opus 5.5's chain off Anthropic (senpi#6699). Outside an episode the model's own chain governs.
|
|
118
|
+
*/
|
|
114
119
|
private managedChainKey;
|
|
115
120
|
private nextCandidate;
|
|
116
121
|
private switchOrRelease;
|
|
@@ -392,13 +392,21 @@ export class RetryFallbackController {
|
|
|
392
392
|
this.deps.logger.info("fallback_applied", { from, to, chainKey: candidate.chainKey, reason, ...scope });
|
|
393
393
|
this.deps.emit({ type: "retry_fallback_applied", from, to, chainKey: candidate.chainKey, reason, ...scope });
|
|
394
394
|
}
|
|
395
|
-
/**
|
|
395
|
+
/**
|
|
396
|
+
* The chain governing `current`, when fallback is enabled. An active episode's chain wins over the
|
|
397
|
+
* rung's own chain, so the walk continues down the chain it started on: Opus 5.5 -> Fable 5.2 -> Kimi
|
|
398
|
+
* never detours through Fable's own chain, and a Fable session that reached Opus 5.5 never takes
|
|
399
|
+
* Opus 5.5's chain off Anthropic (senpi#6699). Outside an episode the model's own chain governs.
|
|
400
|
+
*/
|
|
396
401
|
managedChainKey(current) {
|
|
397
402
|
if (!this.deps.getSettings().modelFallback)
|
|
398
403
|
return undefined;
|
|
399
404
|
const chains = this.canonicalChains();
|
|
400
|
-
const
|
|
401
|
-
|
|
405
|
+
const active = this.state?.chainKey;
|
|
406
|
+
if (active && chains[active])
|
|
407
|
+
return active;
|
|
408
|
+
const own = resolveChainKey(current.model, current.thinkingLevel, chains, fastSiblingOption(this.deps.registry, current.model));
|
|
409
|
+
return own && chains[own] ? own : undefined;
|
|
402
410
|
}
|
|
403
411
|
nextCandidate(reserve = true, logDecision = reserve, refusedProvider) {
|
|
404
412
|
const current = this.deps.getCurrentSelector();
|
|
@@ -98,20 +98,34 @@ function withoutNamespace(modelId) {
|
|
|
98
98
|
const cut = Math.max(modelId.lastIndexOf("."), modelId.lastIndexOf("/"));
|
|
99
99
|
return cut === -1 ? modelId : modelId.slice(cut + 1);
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* The ids a model answers to as a family. Kimi Code serves the Moonshot models under bare
|
|
103
|
+
* ids (`k3.1-pro` is Moonshot's `kimi-k3.1-pro`), so a `kimi-` family reaches it too.
|
|
104
|
+
*/
|
|
105
|
+
function familyIds(model) {
|
|
106
|
+
const id = model.id.toLowerCase();
|
|
107
|
+
const ids = [id, withoutNamespace(id)];
|
|
108
|
+
return /^k\d/.test(id) ? [...ids, `kimi-${id}`] : ids;
|
|
109
|
+
}
|
|
110
|
+
/** A model's own id outranks the Kimi Code alias, which outranks a dash-suffixed variant. */
|
|
111
|
+
function exactness(model, family) {
|
|
112
|
+
const id = model.id.toLowerCase();
|
|
113
|
+
if (id === family || withoutNamespace(id) === family)
|
|
114
|
+
return 2;
|
|
115
|
+
return familyIds(model).includes(family) ? 1 : 0;
|
|
116
|
+
}
|
|
101
117
|
/**
|
|
102
118
|
* Conservative family match. A bare id matches its exact model and dash-suffixed
|
|
103
119
|
* variants (`k3` -> `k3-256k`) but never an arbitrary substring, so `claude-fable-5`
|
|
104
120
|
* cannot capture `not-claude-fable-5`.
|
|
105
121
|
*/
|
|
106
122
|
export function matchesFamily(model, family) {
|
|
107
|
-
|
|
108
|
-
return candidates.some((id) => id === family || id.startsWith(`${family}-`));
|
|
123
|
+
return familyIds(model).some((id) => id === family || id.startsWith(`${family}-`));
|
|
109
124
|
}
|
|
110
125
|
/** Exact id wins, then the shortest variant, then alphabetical. */
|
|
111
126
|
function pickVariant(models, family) {
|
|
112
127
|
const ranked = [...models].sort((left, right) => {
|
|
113
|
-
const exact =
|
|
114
|
-
Number(withoutNamespace(left.id.toLowerCase()) === family);
|
|
128
|
+
const exact = exactness(right, family) - exactness(left, family);
|
|
115
129
|
if (exact !== 0)
|
|
116
130
|
return exact;
|
|
117
131
|
if (left.id.length !== right.id.length)
|
|
@@ -33,14 +33,15 @@ export interface ResolvedRetryFallbackSettings {
|
|
|
33
33
|
* follows Fable 5 whichever provider serves it - the builtin Anthropic provider, the
|
|
34
34
|
* Anthropic Subscription extension, a gateway, or Bedrock.
|
|
35
35
|
*
|
|
36
|
-
* The
|
|
36
|
+
* The Fable ladders never leave Anthropic. A previous shipped default was removed because it
|
|
37
37
|
* led with cross-family rungs (`k3`, `kimi-k3`), which moved a Claude session onto another
|
|
38
|
-
* vendor mid-turn and could rank a guaranteed-refusal OAuth lane first
|
|
39
|
-
*
|
|
38
|
+
* vendor mid-turn and could rank a guaranteed-refusal OAuth lane first. There is still
|
|
39
|
+
* deliberately no wildcard lane.
|
|
40
40
|
*
|
|
41
|
-
* Opus 5.5
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* Opus 5.5 is the one chain that leaves Anthropic, by the owner's order (senpi#6699): Fable 5.2
|
|
42
|
+
* at medium effort, then Kimi K3.1 Pro at max (its highest level), and nothing after. No Opus
|
|
43
|
+
* rung follows, since Anthropic meters Opus on its own quota window and a rate limit on
|
|
44
|
+
* Opus 5.5 also holds every other Opus rung on that account (senpi#6385).
|
|
44
45
|
*
|
|
45
46
|
* Rungs stay on current-generation Opus: a Claude session must never step down to
|
|
46
47
|
* `claude-opus-4-8` or `claude-opus-4-6` (senpi#3727).
|
|
@@ -4,24 +4,25 @@
|
|
|
4
4
|
* follows Fable 5 whichever provider serves it - the builtin Anthropic provider, the
|
|
5
5
|
* Anthropic Subscription extension, a gateway, or Bedrock.
|
|
6
6
|
*
|
|
7
|
-
* The
|
|
7
|
+
* The Fable ladders never leave Anthropic. A previous shipped default was removed because it
|
|
8
8
|
* led with cross-family rungs (`k3`, `kimi-k3`), which moved a Claude session onto another
|
|
9
|
-
* vendor mid-turn and could rank a guaranteed-refusal OAuth lane first
|
|
10
|
-
*
|
|
9
|
+
* vendor mid-turn and could rank a guaranteed-refusal OAuth lane first. There is still
|
|
10
|
+
* deliberately no wildcard lane.
|
|
11
11
|
*
|
|
12
|
-
* Opus 5.5
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Opus 5.5 is the one chain that leaves Anthropic, by the owner's order (senpi#6699): Fable 5.2
|
|
13
|
+
* at medium effort, then Kimi K3.1 Pro at max (its highest level), and nothing after. No Opus
|
|
14
|
+
* rung follows, since Anthropic meters Opus on its own quota window and a rate limit on
|
|
15
|
+
* Opus 5.5 also holds every other Opus rung on that account (senpi#6385).
|
|
15
16
|
*
|
|
16
17
|
* Rungs stay on current-generation Opus: a Claude session must never step down to
|
|
17
18
|
* `claude-opus-4-8` or `claude-opus-4-6` (senpi#3727).
|
|
18
19
|
*/
|
|
19
20
|
export const DEFAULT_FALLBACK_CHAINS = {
|
|
20
|
-
//
|
|
21
|
+
// Fable rungs carry no explicit level so fallback inherits the session's current thinking effort (senpi#4103).
|
|
21
22
|
"claude-fable-5-2": ["claude-opus-5-5", "claude-opus-5"],
|
|
22
23
|
"claude-fable-5-1": ["claude-opus-5-5", "claude-opus-5"],
|
|
23
24
|
"claude-fable-5": ["claude-opus-5-5", "claude-opus-5"],
|
|
24
|
-
"claude-opus-5-5": ["claude-fable-5-2", "
|
|
25
|
+
"claude-opus-5-5": ["claude-fable-5-2:medium", "kimi-k3.1-pro:max"],
|
|
25
26
|
};
|
|
26
27
|
function cloneDefaultFallbackChains() {
|
|
27
28
|
const chains = {};
|
|
@@ -6,6 +6,7 @@ import type { SessionManager } from "../session-manager.ts";
|
|
|
6
6
|
import type { SettingsManager } from "../settings-manager.ts";
|
|
7
7
|
import { type SourceInfo } from "../source-info.ts";
|
|
8
8
|
import type { ToolDispatcher } from "../tool-dispatch/tool-dispatcher.ts";
|
|
9
|
+
import type { ToolMachine } from "../tools/machine.ts";
|
|
9
10
|
export interface ToolDefinitionEntry {
|
|
10
11
|
definition: ToolDefinition;
|
|
11
12
|
sourceInfo: SourceInfo;
|
|
@@ -24,6 +25,8 @@ export interface RuntimeBootstrapOptions {
|
|
|
24
25
|
sessionManager?: SessionManager;
|
|
25
26
|
/** Routes the core bash tool's process work; absent means the tool's own local operations. */
|
|
26
27
|
toolDispatcher?: ToolDispatcher;
|
|
28
|
+
/** The device this session works on (slice 1a seam; supplied by the relay transport in slice 1b). */
|
|
29
|
+
deviceMachine?: ToolMachine;
|
|
27
30
|
/** Used instead of the built-in tools when set. */
|
|
28
31
|
baseToolsOverride?: Record<string, AgentTool>;
|
|
29
32
|
/** Extension flag values applied to the shared extension runtime before the runner is created. */
|
|
@@ -33,6 +33,7 @@ export function bootstrapRuntime(options) {
|
|
|
33
33
|
shellPath,
|
|
34
34
|
...(sessionless ? { exposeSessionEnvironment: false } : {}),
|
|
35
35
|
...(options.toolDispatcher ? { operations: options.toolDispatcher.bashOperations({ shellPath }) } : {}),
|
|
36
|
+
deviceMachine: () => options.deviceMachine ?? extensionsResult.runtime.deviceMachine?.machine,
|
|
36
37
|
},
|
|
37
38
|
write: { filesystemPolicy },
|
|
38
39
|
edit: { filesystemPolicy },
|
|
@@ -14,6 +14,20 @@ export declare class MissingSessionCwdError extends Error {
|
|
|
14
14
|
readonly issue: SessionCwdIssue;
|
|
15
15
|
constructor(issue: SessionCwdIssue);
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
/** A session cwd that exists but is not a folder (a file, a socket): its tools could not run there. */
|
|
18
|
+
export interface SessionCwdNotDirectoryIssue {
|
|
19
|
+
sessionFile?: string;
|
|
20
|
+
sessionCwd: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class SessionCwdNotDirectoryError extends Error {
|
|
23
|
+
readonly issue: SessionCwdNotDirectoryIssue;
|
|
24
|
+
constructor(issue: SessionCwdNotDirectoryIssue);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Async on purpose: the RPC session path may not reach a synchronous filesystem call
|
|
28
|
+
* (test/suite/no-sync-in-session-path.test.ts), so the directory probe is awaited here rather
|
|
29
|
+
* than taken with statSync.
|
|
30
|
+
*/
|
|
31
|
+
export declare function assertSessionCwdExists(sessionManager: SessionCwdSource, fallbackCwd: string): Promise<void>;
|
|
18
32
|
export {};
|
|
19
33
|
//# sourceMappingURL=session-cwd.d.ts.map
|
package/dist/core/session-cwd.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
2
3
|
export function getMissingSessionCwdIssue(sessionManager, fallbackCwd) {
|
|
3
4
|
const sessionFile = sessionManager.getSessionFile();
|
|
4
5
|
if (!sessionFile) {
|
|
@@ -28,10 +29,30 @@ export class MissingSessionCwdError extends Error {
|
|
|
28
29
|
this.issue = issue;
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
|
-
export
|
|
32
|
+
export class SessionCwdNotDirectoryError extends Error {
|
|
33
|
+
constructor(issue) {
|
|
34
|
+
const sessionFile = issue.sessionFile ? `\nSession file: ${issue.sessionFile}` : "";
|
|
35
|
+
super(`Session working directory is not a directory: ${issue.sessionCwd}${sessionFile}`);
|
|
36
|
+
this.name = "SessionCwdNotDirectoryError";
|
|
37
|
+
this.issue = issue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Async on purpose: the RPC session path may not reach a synchronous filesystem call
|
|
42
|
+
* (test/suite/no-sync-in-session-path.test.ts), so the directory probe is awaited here rather
|
|
43
|
+
* than taken with statSync.
|
|
44
|
+
*/
|
|
45
|
+
export async function assertSessionCwdExists(sessionManager, fallbackCwd) {
|
|
32
46
|
const issue = getMissingSessionCwdIssue(sessionManager, fallbackCwd);
|
|
33
47
|
if (issue) {
|
|
34
48
|
throw new MissingSessionCwdError(issue);
|
|
35
49
|
}
|
|
50
|
+
const sessionCwd = sessionManager.getCwd();
|
|
51
|
+
// A missing cwd is the case above (or a session with no file, which keeps today's behaviour); one that exists must be a folder.
|
|
52
|
+
const stats = sessionCwd ? await stat(sessionCwd).catch(() => undefined) : undefined;
|
|
53
|
+
if (sessionCwd && stats !== undefined && !stats.isDirectory()) {
|
|
54
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
55
|
+
throw new SessionCwdNotDirectoryError({ ...(sessionFile ? { sessionFile } : {}), sessionCwd });
|
|
56
|
+
}
|
|
36
57
|
}
|
|
37
58
|
//# sourceMappingURL=session-cwd.js.map
|
|
@@ -70,6 +70,12 @@ export declare function createLocalBashOperations(options?: {
|
|
|
70
70
|
shellPath?: string;
|
|
71
71
|
machine?: ToolMachine;
|
|
72
72
|
}): BashOperations;
|
|
73
|
+
/** A tool routed to the session's device by OMO_TOOLS_VIA_DAEMON found no device to run on; it never falls back to local. */
|
|
74
|
+
export declare class DeviceUnavailableError extends Error {
|
|
75
|
+
readonly code: "device_unavailable";
|
|
76
|
+
readonly tool: string;
|
|
77
|
+
constructor(tool: string);
|
|
78
|
+
}
|
|
73
79
|
export interface BashSpawnContext {
|
|
74
80
|
command: string;
|
|
75
81
|
cwd: string;
|
|
@@ -91,6 +97,12 @@ export interface BashToolOptions {
|
|
|
91
97
|
spawnHook?: BashSpawnHook;
|
|
92
98
|
/** Operation layer and policy gate. Default: the process-wide local machine. */
|
|
93
99
|
machine?: ToolMachine;
|
|
100
|
+
/**
|
|
101
|
+
* The operation layer of the device this session works on. Slice 1a of the tools-via-daemon plan: an executor seam
|
|
102
|
+
* only, NOT yet via the daemon; the relay transport that supplies it is slice 1b. Used only when OMO_TOOLS_VIA_DAEMON
|
|
103
|
+
* names "bash"; with the flag on and no device machine, a call fails with DeviceUnavailableError and never runs locally.
|
|
104
|
+
*/
|
|
105
|
+
deviceMachine?: ToolMachine | (() => ToolMachine | undefined);
|
|
94
106
|
}
|
|
95
107
|
export type BashRenderState = {
|
|
96
108
|
startedAt: number | undefined;
|
package/dist/core/tools/bash.js
CHANGED
|
@@ -152,6 +152,19 @@ export function createLocalShellOperations(shellName, resolveShellConfig, machin
|
|
|
152
152
|
export function createLocalBashOperations(options) {
|
|
153
153
|
return createLocalShellOperations("bash", () => getShellConfig(options?.shellPath), options?.machine);
|
|
154
154
|
}
|
|
155
|
+
/** A tool routed to the session's device by OMO_TOOLS_VIA_DAEMON found no device to run on; it never falls back to local. */
|
|
156
|
+
export class DeviceUnavailableError extends Error {
|
|
157
|
+
constructor(tool) {
|
|
158
|
+
super(`${tool}: the session's device is unavailable, and OMO_TOOLS_VIA_DAEMON routes ${tool} there, so it does not run locally`);
|
|
159
|
+
this.code = "device_unavailable";
|
|
160
|
+
this.name = "DeviceUnavailableError";
|
|
161
|
+
this.tool = tool;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const toolsViaDaemon = (tool, env = process.env) => (env.OMO_TOOLS_VIA_DAEMON ?? "")
|
|
165
|
+
.split(",")
|
|
166
|
+
.map((name) => name.trim())
|
|
167
|
+
.includes(tool);
|
|
155
168
|
function resolveSpawnContext(command, cwd, spawnHook, exposeSessionEnvironment, ctx) {
|
|
156
169
|
const env = { ...getShellEnv() };
|
|
157
170
|
delete env.PI_SESSION_ID;
|
|
@@ -186,7 +199,23 @@ function resolveSpawnContext(command, cwd, spawnHook, exposeSessionEnvironment,
|
|
|
186
199
|
}
|
|
187
200
|
export function createShellToolDefinition(cwd, config, options) {
|
|
188
201
|
const machine = options?.machine ?? defaultToolMachine();
|
|
189
|
-
|
|
202
|
+
// OMO_TOOLS_VIA_DAEMON=bash (default off): run on the session's device, never on this machine. The flag decides even
|
|
203
|
+
// when the session injects its dispatcher's operations (bootstrap.ts always does), so the promise holds in a real session.
|
|
204
|
+
// Only the bash tool: powershell shares this definition with its own local operations and stays on them.
|
|
205
|
+
const viaDevice = config.name === "bash" && toolsViaDaemon("bash");
|
|
206
|
+
const deviceMachine = options?.deviceMachine;
|
|
207
|
+
// Resolved when the command runs, not when the tool is built: an extension may register the device later.
|
|
208
|
+
const resolveDevice = typeof deviceMachine === "function" ? deviceMachine : () => deviceMachine;
|
|
209
|
+
const ops = viaDevice
|
|
210
|
+
? {
|
|
211
|
+
exec: (command, execCwd, execOptions) => {
|
|
212
|
+
const device = resolveDevice();
|
|
213
|
+
if (device === undefined)
|
|
214
|
+
throw new DeviceUnavailableError(config.name);
|
|
215
|
+
return createLocalBashOperations({ shellPath: options?.shellPath, machine: device }).exec(command, execCwd, execOptions);
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
: (options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath, machine }));
|
|
190
219
|
const commandPrefix = options?.commandPrefix;
|
|
191
220
|
const exposeSessionEnvironment = options?.exposeSessionEnvironment ?? true;
|
|
192
221
|
const spawnHook = options?.spawnHook;
|
package/dist/index.d.ts
CHANGED
|
@@ -40,6 +40,10 @@ export { type CompactionModelOverride, type CompactionSettings, type DefaultProj
|
|
|
40
40
|
export { formatSkillsForPrompt, type LoadSkillsFromDirOptions, type LoadSkillsResult, loadSkills, loadSkillsFromDir, type Skill, type SkillFrontmatter, } from "./core/skills.ts";
|
|
41
41
|
export { createSyntheticSourceInfo } from "./core/source-info.ts";
|
|
42
42
|
export { type EditDiffResult, generateDiffString, generateUnifiedPatch } from "./core/tools/edit-diff.ts";
|
|
43
|
+
export type { ExecRequest as MachineExecRequest, ExecResult as MachineExecResult, MachineOps, OpContext, } from "../vendor/senpi-machine-ops/index.js";
|
|
44
|
+
export { DeviceMachineConflictError } from "./core/extensions/types.ts";
|
|
45
|
+
export { DeviceUnavailableError } from "./core/tools/bash.ts";
|
|
46
|
+
export { createToolMachine, type ToolMachine, type ToolPathPolicy } from "./core/tools/machine.ts";
|
|
43
47
|
export { type BashOperations, type BashSpawnContext, type BashSpawnHook, type BashToolDetails, type BashToolInput, type BashToolOptions, createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLocalBashOperations, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type EditOperations, type EditToolDetails, type EditToolInput, type EditToolOptions, type FindOperations, type FindToolDetails, type FindToolInput, type FindToolOptions, formatSize, type GrepOperations, type GrepToolDetails, type GrepToolInput, type GrepToolOptions, type LsOperations, type LsToolDetails, type LsToolInput, type LsToolOptions, type ReadOperations, type ReadToolDetails, type ReadToolInput, type ReadToolOptions, type ToolsOptions, type TruncationOptions, type TruncationResult, truncateHead, truncateLine, truncateTail, type WriteOperations, type WriteToolInput, type WriteToolOptions, withFileMutationQueue, } from "./core/tools/index.ts";
|
|
44
48
|
export { type CompactReadClassification, classifyRead, type ReadClassifier, registerReadClassifier, } from "./core/tools/read-classifiers.ts";
|
|
45
49
|
export { registerRpcRemoteSource, type RpcRemoteHandlers, type RpcRemoteOpenOptions, type RpcRemoteSettledTurn, type RpcRemoteSource, type RpcRemoteState, type RpcRemoteSubscription, type RpcRemoteTarget, } from "./core/extensions/builtin/terminal/rpc-remote-source.ts";
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,9 @@ export { SettingsManager, } from "./core/settings-manager.js";
|
|
|
47
47
|
export { formatSkillsForPrompt, loadSkills, loadSkillsFromDir, } from "./core/skills.js";
|
|
48
48
|
export { createSyntheticSourceInfo } from "./core/source-info.js";
|
|
49
49
|
export { generateDiffString, generateUnifiedPatch } from "./core/tools/edit-diff.js";
|
|
50
|
+
export { DeviceMachineConflictError } from "./core/extensions/types.js";
|
|
51
|
+
export { DeviceUnavailableError } from "./core/tools/bash.js";
|
|
52
|
+
export { createToolMachine } from "./core/tools/machine.js";
|
|
50
53
|
// Tools
|
|
51
54
|
export { createBashToolDefinition, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLocalBashOperations, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, truncateLine, truncateTail, withFileMutationQueue, } from "./core/tools/index.js";
|
|
52
55
|
export { classifyRead, registerReadClassifier, } from "./core/tools/read-classifiers.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { projectCwdMismatch } from "./project-expected-cwd.js";
|
|
1
2
|
import { isHostOwnedToolRuntimeCapability, toolRuntimeCapabilities, } from "../../cli/tool-runtime.js";
|
|
2
3
|
import { messagePathProjectRoot, resolveMessagePath } from "../../core/message-path-resolution.js";
|
|
3
4
|
import { startOpenTimer } from "../../core/open-timing.js";
|
|
@@ -1013,6 +1014,9 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
1013
1014
|
// Handle a single command
|
|
1014
1015
|
const handleCommand = async (command, requestContext) => {
|
|
1015
1016
|
const id = command.id;
|
|
1017
|
+
const cwdMismatch = projectCwdMismatch(command, session.sessionManager.getCwd());
|
|
1018
|
+
if (cwdMismatch !== undefined)
|
|
1019
|
+
return error(id, command.type, cwdMismatch.message, cwdMismatch.errorCode);
|
|
1016
1020
|
switch (command.type) {
|
|
1017
1021
|
case "get_protocol_info":
|
|
1018
1022
|
return {
|