@oh-my-pi/pi-coding-agent 17.2.1 → 17.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/dist/{CHANGELOG-dj46zzrm.md → CHANGELOG-c5hpqt9r.md} +61 -0
- package/dist/cli.js +3856 -3850
- package/dist/types/config/keybindings.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +16 -5
- package/dist/types/edit/hashline/block-resolver.d.ts +5 -5
- package/dist/types/edit/hashline/diff.d.ts +4 -4
- package/dist/types/eval/executor-base.d.ts +27 -0
- package/dist/types/eval/js/context-manager.d.ts +7 -0
- package/dist/types/eval/js/executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +64 -5
- package/dist/types/extensibility/extensions/types.d.ts +15 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +1 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
- package/dist/types/live/visualizer.d.ts +3 -1
- package/dist/types/modes/acp/acp-agent.d.ts +3 -3
- package/dist/types/modes/components/ask-dialog.d.ts +0 -1
- package/dist/types/modes/components/keybinding-hints.d.ts +1 -1
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +13 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/secrets/index.d.ts +22 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/codex-auto-reset.d.ts +182 -64
- package/dist/types/session/session-storage.d.ts +5 -2
- package/dist/types/session/session-tools.d.ts +2 -1
- package/dist/types/session/turn-recovery.d.ts +7 -1
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +27 -0
- package/dist/types/task/persisted-revive.d.ts +7 -0
- package/dist/types/tools/bash-interceptor.d.ts +1 -1
- package/dist/types/tools/browser/attach.d.ts +9 -1
- package/dist/types/tools/browser/launch.d.ts +50 -1
- package/dist/types/tools/browser/registry.d.ts +7 -0
- package/dist/types/tools/browser/shared-daemon.d.ts +26 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +8 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -0
- package/dist/types/tools/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/shell-tokenize.d.ts +11 -0
- package/dist/types/utils/image-loading.d.ts +2 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/src/commit/agentic/index.ts +2 -1
- package/src/commit/pipeline.ts +3 -2
- package/src/config/keybindings.ts +6 -1
- package/src/config/settings-schema.ts +16 -3
- package/src/edit/hashline/block-resolver.ts +5 -5
- package/src/edit/hashline/diff.ts +22 -15
- package/src/edit/hashline/execute.ts +14 -13
- package/src/edit/renderer.ts +3 -3
- package/src/eval/executor-base.ts +39 -0
- package/src/eval/jl/executor.ts +13 -2
- package/src/eval/js/context-manager.ts +97 -14
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/py/executor.ts +55 -15
- package/src/eval/rb/executor.ts +8 -1
- package/src/extensibility/extensions/runner.ts +121 -4
- package/src/extensibility/extensions/types.ts +15 -0
- package/src/extensibility/extensions/wrapper.ts +19 -2
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +5 -5
- package/src/launch/broker.ts +59 -41
- package/src/live/visualizer.ts +8 -1
- package/src/lsp/index.ts +51 -11
- package/src/main.ts +3 -0
- package/src/modes/acp/acp-agent.ts +16 -5
- package/src/modes/components/ask-dialog.ts +65 -23
- package/src/modes/components/assistant-message.ts +5 -10
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/keybinding-hints.ts +4 -13
- package/src/modes/components/status-line/segments.ts +4 -5
- package/src/modes/components/tool-execution.ts +4 -5
- package/src/modes/controllers/input-controller.ts +18 -40
- package/src/modes/controllers/live-command-controller.ts +1 -0
- package/src/modes/controllers/mcp-command-controller.ts +28 -11
- package/src/modes/interactive-mode.ts +59 -2
- package/src/modes/theme/theme.ts +13 -6
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/prompts/system/plan-mode-active.md +5 -5
- package/src/sdk.ts +47 -16
- package/src/secrets/index.ts +83 -3
- package/src/secrets/obfuscator.ts +70 -15
- package/src/session/agent-session-types.ts +7 -1
- package/src/session/agent-session.ts +394 -113
- package/src/session/codex-auto-reset.ts +594 -123
- package/src/session/eval-runner.ts +2 -0
- package/src/session/messages.ts +1 -1
- package/src/session/session-maintenance.ts +50 -3
- package/src/session/session-manager.ts +4 -3
- package/src/session/session-stats.ts +14 -3
- package/src/session/session-storage.ts +47 -15
- package/src/session/session-tools.ts +30 -2
- package/src/session/turn-recovery.ts +15 -2
- package/src/slash-commands/builtin-registry.ts +21 -15
- package/src/slash-commands/helpers/reset-usage.ts +2 -0
- package/src/system-prompt.ts +13 -1
- package/src/task/executor.ts +168 -15
- package/src/task/persisted-revive.ts +26 -1
- package/src/telemetry-export.ts +8 -4
- package/src/tools/bash-interceptor.ts +85 -8
- package/src/tools/bash.ts +12 -6
- package/src/tools/browser/attach.ts +35 -10
- package/src/tools/browser/launch.ts +110 -16
- package/src/tools/browser/registry.ts +140 -31
- package/src/tools/browser/shared-daemon.ts +190 -0
- package/src/tools/browser/tab-protocol.ts +8 -0
- package/src/tools/browser/tab-supervisor.ts +31 -4
- package/src/tools/browser/tab-worker.ts +32 -2
- package/src/tools/browser.ts +1 -1
- package/src/tools/output-meta.ts +16 -1
- package/src/tools/shell-tokenize.ts +101 -0
- package/src/utils/image-loading.ts +8 -3
- package/src/utils/shell-snapshot.ts +31 -21
- package/src/web/search/providers/codex.ts +31 -6
- package/src/web/search/providers/duckduckgo.ts +183 -15
- package/src/web/search/types.ts +6 -1
package/src/task/executor.ts
CHANGED
|
@@ -2200,6 +2200,135 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
2200
2200
|
};
|
|
2201
2201
|
}
|
|
2202
2202
|
|
|
2203
|
+
/** Inputs for {@link attachIrcWakeTurnMonitor}. */
|
|
2204
|
+
export interface IrcWakeTurnMonitorOptions {
|
|
2205
|
+
/** Registry id of the kept-alive subagent whose autonomous IRC wake turns are monitored. */
|
|
2206
|
+
id: string;
|
|
2207
|
+
index?: number;
|
|
2208
|
+
agent: AgentDefinition;
|
|
2209
|
+
description?: string;
|
|
2210
|
+
modelOverride?: string | string[];
|
|
2211
|
+
eventBus?: EventBus;
|
|
2212
|
+
parentToolCallId?: string;
|
|
2213
|
+
/** Fallback session file when the registry ref carries none. */
|
|
2214
|
+
sessionFile?: string;
|
|
2215
|
+
maxRuntimeMs?: number;
|
|
2216
|
+
outputSchema?: unknown;
|
|
2217
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
2218
|
+
outputSchemaSource?: StructuredSubagentSchemaSource;
|
|
2219
|
+
artifactsDir?: string;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
/**
|
|
2223
|
+
* Bracket a kept-alive subagent's autonomous IRC wake turns with a task run
|
|
2224
|
+
* monitor so RPC/collab subscribers see the same `subagent_lifecycle` /
|
|
2225
|
+
* `subagent_progress` frames a first run emits. Shared by the live executor
|
|
2226
|
+
* reviver and the persisted cold-revive path so a resumed process's parked
|
|
2227
|
+
* subagents are not blind spots. The observer runs after the session has
|
|
2228
|
+
* flushed its post-prompt settle (see {@link AgentSession.setIrcWakeTurnObserver}).
|
|
2229
|
+
*/
|
|
2230
|
+
export function attachIrcWakeTurnMonitor(session: AgentSession, options: IrcWakeTurnMonitorOptions): void {
|
|
2231
|
+
const { id, agent } = options;
|
|
2232
|
+
const index = options.index ?? 0;
|
|
2233
|
+
const maxRuntimeMs = options.maxRuntimeMs ?? 0;
|
|
2234
|
+
session.setIrcWakeTurnObserver(records => {
|
|
2235
|
+
const ircTask =
|
|
2236
|
+
records
|
|
2237
|
+
.map(record => {
|
|
2238
|
+
const body =
|
|
2239
|
+
record.details && typeof record.details === "object"
|
|
2240
|
+
? Reflect.get(record.details, "message")
|
|
2241
|
+
: undefined;
|
|
2242
|
+
return typeof body === "string" ? body : record.content;
|
|
2243
|
+
})
|
|
2244
|
+
.filter(Boolean)
|
|
2245
|
+
.join("\n\n") || "IRC follow-up";
|
|
2246
|
+
const turnStartTime = Date.now();
|
|
2247
|
+
const sessionFile = AgentRegistry.global().get(id)?.sessionFile ?? options.sessionFile ?? undefined;
|
|
2248
|
+
const turnMonitor = createSubagentRunMonitor({
|
|
2249
|
+
index,
|
|
2250
|
+
id,
|
|
2251
|
+
agent,
|
|
2252
|
+
task: ircTask,
|
|
2253
|
+
description: options.description,
|
|
2254
|
+
modelOverride: options.modelOverride,
|
|
2255
|
+
eventBus: options.eventBus,
|
|
2256
|
+
parentToolCallId: options.parentToolCallId,
|
|
2257
|
+
detached: true,
|
|
2258
|
+
sessionFile,
|
|
2259
|
+
softRequestBudget: 0,
|
|
2260
|
+
softRequestBudgetNotice: false,
|
|
2261
|
+
maxRuntimeMs,
|
|
2262
|
+
});
|
|
2263
|
+
|
|
2264
|
+
if (options.eventBus) {
|
|
2265
|
+
options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
|
|
2266
|
+
id,
|
|
2267
|
+
agent: agent.name,
|
|
2268
|
+
parentToolCallId: options.parentToolCallId,
|
|
2269
|
+
detached: true,
|
|
2270
|
+
agentSource: agent.source,
|
|
2271
|
+
description: options.description,
|
|
2272
|
+
status: "started",
|
|
2273
|
+
sessionFile,
|
|
2274
|
+
index,
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
turnMonitor.setActiveSession(session);
|
|
2279
|
+
const unsubscribeTurn = turnMonitor.attach(session);
|
|
2280
|
+
return async turnError => {
|
|
2281
|
+
unsubscribeTurn();
|
|
2282
|
+
const activeSession = turnMonitor.takeActiveSession();
|
|
2283
|
+
if (activeSession) turnMonitor.captureSalvage(activeSession);
|
|
2284
|
+
const lastAssistant = session.getLastAssistantMessage();
|
|
2285
|
+
const yielded = turnMonitor.yieldCalled();
|
|
2286
|
+
const runtimeLimitExceeded = turnMonitor.runtimeLimitExceeded();
|
|
2287
|
+
const aborted = runtimeLimitExceeded || (lastAssistant?.stopReason === "aborted" && !yielded);
|
|
2288
|
+
const error =
|
|
2289
|
+
lastAssistant?.stopReason === "error"
|
|
2290
|
+
? lastAssistant.errorMessage || "Subagent failed"
|
|
2291
|
+
: turnError !== undefined && !yielded
|
|
2292
|
+
? turnError instanceof Error
|
|
2293
|
+
? turnError.stack || turnError.message
|
|
2294
|
+
: String(turnError)
|
|
2295
|
+
: undefined;
|
|
2296
|
+
turnMonitor.finish();
|
|
2297
|
+
try {
|
|
2298
|
+
await finalizeRunResult({
|
|
2299
|
+
monitor: turnMonitor,
|
|
2300
|
+
done: {
|
|
2301
|
+
exitCode: aborted || error ? 1 : 0,
|
|
2302
|
+
error,
|
|
2303
|
+
aborted,
|
|
2304
|
+
abortReason: aborted ? turnMonitor.resolveAbortReasonText() : undefined,
|
|
2305
|
+
durationMs: Date.now() - turnStartTime,
|
|
2306
|
+
},
|
|
2307
|
+
index,
|
|
2308
|
+
id,
|
|
2309
|
+
agent,
|
|
2310
|
+
task: ircTask,
|
|
2311
|
+
modelOverride: options.modelOverride,
|
|
2312
|
+
outputSchema: options.outputSchema,
|
|
2313
|
+
outputSchemaMode: options.outputSchemaMode,
|
|
2314
|
+
outputSchemaSource: options.outputSchemaSource,
|
|
2315
|
+
artifactsDir: options.artifactsDir,
|
|
2316
|
+
eventBus: options.eventBus,
|
|
2317
|
+
parentToolCallId: options.parentToolCallId,
|
|
2318
|
+
detached: true,
|
|
2319
|
+
sessionFile,
|
|
2320
|
+
startTime: turnStartTime,
|
|
2321
|
+
});
|
|
2322
|
+
} catch (finalizeError) {
|
|
2323
|
+
logger.warn("IRC subagent turn finalization failed", {
|
|
2324
|
+
id,
|
|
2325
|
+
error: finalizeError instanceof Error ? finalizeError.message : String(finalizeError),
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2203
2332
|
/**
|
|
2204
2333
|
* Settle a subagent's registry lifecycle after a run: terminal teardown for
|
|
2205
2334
|
* hard aborts, unregister for one-shot helpers, park for isolated runs, and
|
|
@@ -2544,6 +2673,23 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2544
2673
|
}
|
|
2545
2674
|
});
|
|
2546
2675
|
};
|
|
2676
|
+
const installIrcWakeTurnMonitor = (target: AgentSession): void => {
|
|
2677
|
+
attachIrcWakeTurnMonitor(target, {
|
|
2678
|
+
id,
|
|
2679
|
+
index,
|
|
2680
|
+
agent,
|
|
2681
|
+
description: options.description,
|
|
2682
|
+
modelOverride,
|
|
2683
|
+
eventBus: options.eventBus,
|
|
2684
|
+
parentToolCallId: options.parentToolCallId,
|
|
2685
|
+
sessionFile: subtaskSessionFile,
|
|
2686
|
+
maxRuntimeMs,
|
|
2687
|
+
outputSchema,
|
|
2688
|
+
outputSchemaMode: options.outputSchemaMode,
|
|
2689
|
+
outputSchemaSource: options.outputSchemaSource,
|
|
2690
|
+
artifactsDir: options.artifactsDir,
|
|
2691
|
+
});
|
|
2692
|
+
};
|
|
2547
2693
|
|
|
2548
2694
|
const runSubagent = async (): Promise<{
|
|
2549
2695
|
exitCode: number;
|
|
@@ -2604,7 +2750,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2604
2750
|
}
|
|
2605
2751
|
checkAbort();
|
|
2606
2752
|
if (!registryFromParent) {
|
|
2607
|
-
|
|
2753
|
+
modelRegistry.refreshInBackground();
|
|
2608
2754
|
} else {
|
|
2609
2755
|
logger.debug("runSubagent: reusing parent modelRegistry; skipping refresh");
|
|
2610
2756
|
}
|
|
@@ -2685,6 +2831,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2685
2831
|
const effectiveThinkingLevel =
|
|
2686
2832
|
effortLevel ?? (explicitThinkingLevel ? resolvedThinkingLevel : (thinkingLevel ?? resolvedThinkingLevel));
|
|
2687
2833
|
resolvedAt = performance.now();
|
|
2834
|
+
const effectiveCwd = worktree ?? cwd;
|
|
2835
|
+
const sessionManagerPromise = sessionFile
|
|
2836
|
+
? SessionManager.open(sessionFile, undefined, undefined, {
|
|
2837
|
+
initialCwd: effectiveCwd,
|
|
2838
|
+
suppressBreadcrumb: true,
|
|
2839
|
+
})
|
|
2840
|
+
: Promise.resolve(SessionManager.inMemory(effectiveCwd));
|
|
2841
|
+
// Setup below can fail before this promise's consumption boundary.
|
|
2842
|
+
// Observe rejection immediately while preserving it for the later await.
|
|
2843
|
+
sessionManagerPromise.catch(() => {});
|
|
2688
2844
|
// Per-agent prewalk: the agent definition's `prewalk` frontmatter or the
|
|
2689
2845
|
// `task.agentPrewalk` settings override hands the subagent off to a
|
|
2690
2846
|
// fast/cheap target at its first edit/write — the same mechanism as the
|
|
@@ -2697,6 +2853,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2697
2853
|
agentPrewalk: resolveAgentPrewalkDefault(agent, settings.get("task.prewalk")),
|
|
2698
2854
|
});
|
|
2699
2855
|
if (prewalkPattern) {
|
|
2856
|
+
await awaitAbortable(modelRegistry.awaitBackgroundRefresh());
|
|
2700
2857
|
const resolvedPrewalk = resolveModelOverride([prewalkPattern], modelRegistry, settings);
|
|
2701
2858
|
const target = resolvedPrewalk.model;
|
|
2702
2859
|
if (!target || !modelRegistry.hasConfiguredAuth(target)) {
|
|
@@ -2718,20 +2875,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2718
2875
|
}
|
|
2719
2876
|
}
|
|
2720
2877
|
|
|
2721
|
-
const effectiveCwd = worktree ?? cwd;
|
|
2722
|
-
const sessionManager = sessionFile
|
|
2723
|
-
? await awaitAbortable(
|
|
2724
|
-
SessionManager.open(sessionFile, undefined, undefined, {
|
|
2725
|
-
initialCwd: effectiveCwd,
|
|
2726
|
-
suppressBreadcrumb: true,
|
|
2727
|
-
}),
|
|
2728
|
-
)
|
|
2729
|
-
: SessionManager.inMemory(effectiveCwd);
|
|
2730
|
-
if (options.parentArtifactManager) {
|
|
2731
|
-
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
2732
|
-
}
|
|
2733
|
-
sessionOpenedAt = performance.now();
|
|
2734
|
-
|
|
2735
2878
|
const restrictToolNames = options.restrictToolNames === true;
|
|
2736
2879
|
const enableMCP = !restrictToolNames && (options.enableMCP ?? true);
|
|
2737
2880
|
const mcpManager = enableMCP ? options.mcpManager : undefined;
|
|
@@ -2851,6 +2994,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2851
2994
|
},
|
|
2852
2995
|
});
|
|
2853
2996
|
|
|
2997
|
+
const sessionManager = await awaitAbortable(sessionManagerPromise);
|
|
2998
|
+
if (options.parentArtifactManager) {
|
|
2999
|
+
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
3000
|
+
}
|
|
3001
|
+
sessionOpenedAt = performance.now();
|
|
3002
|
+
|
|
2854
3003
|
const sessionPromise = createAgentSession(buildSubagentSessionOptions(sessionManager, null));
|
|
2855
3004
|
let session: AgentSession;
|
|
2856
3005
|
try {
|
|
@@ -2882,6 +3031,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2882
3031
|
buildSubagentSessionOptions(reopened, expectedAgentRef),
|
|
2883
3032
|
);
|
|
2884
3033
|
installRegistryStatusSync(revived);
|
|
3034
|
+
installIrcWakeTurnMonitor(revived);
|
|
2885
3035
|
return revived;
|
|
2886
3036
|
};
|
|
2887
3037
|
}
|
|
@@ -3053,6 +3203,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
3053
3203
|
const session = monitor.takeActiveSession();
|
|
3054
3204
|
if (session) {
|
|
3055
3205
|
monitor.captureSalvage(session);
|
|
3206
|
+
if (options.keepAlive !== false && worktree === undefined) {
|
|
3207
|
+
installIrcWakeTurnMonitor(session);
|
|
3208
|
+
}
|
|
3056
3209
|
await finalizeSubagentLifecycle({
|
|
3057
3210
|
id,
|
|
3058
3211
|
session,
|
|
@@ -9,7 +9,9 @@ import { createAgentSession } from "../sdk";
|
|
|
9
9
|
import type { AgentSession } from "../session/agent-session";
|
|
10
10
|
import type { AuthStorage } from "../session/auth-storage";
|
|
11
11
|
import { SessionManager } from "../session/session-manager";
|
|
12
|
-
import {
|
|
12
|
+
import type { EventBus } from "../utils/event-bus";
|
|
13
|
+
import { attachIrcWakeTurnMonitor, createMCPProxyTools, createSubagentSettings } from "./executor";
|
|
14
|
+
import type { AgentDefinition } from "./types";
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Ambient context the reviver needs at revive time. The top-level session is
|
|
@@ -24,6 +26,12 @@ export interface PersistedSubagentReviveContext {
|
|
|
24
26
|
settings: Settings;
|
|
25
27
|
/** LSP policy of the top-level session; revived subagents inherit it rather than defaulting on. */
|
|
26
28
|
enableLsp: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Shared event bus feeding RPC/collab subagent subscriptions. Passed through
|
|
31
|
+
* to the wake-turn monitor so an IRC send to a cold-revived subagent emits
|
|
32
|
+
* the same lifecycle/progress frames a live run does.
|
|
33
|
+
*/
|
|
34
|
+
eventBus?: EventBus;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
/**
|
|
@@ -135,6 +143,23 @@ export function createPersistedSubagentReviverFactory(
|
|
|
135
143
|
if (event.type === "agent_start") registry.setStatus(ref.id, "running", session);
|
|
136
144
|
else if (event.type === "agent_end") registry.setStatus(ref.id, "idle", session);
|
|
137
145
|
});
|
|
146
|
+
// Persisted files predate an agent-source field, so cold-revived frames
|
|
147
|
+
// report the runtime-neutral `user` source; name comes from the ref.
|
|
148
|
+
const wakeAgent: AgentDefinition = {
|
|
149
|
+
name: ref.displayName,
|
|
150
|
+
description: "",
|
|
151
|
+
systemPrompt: init.systemPrompt,
|
|
152
|
+
source: "user",
|
|
153
|
+
};
|
|
154
|
+
attachIrcWakeTurnMonitor(session, {
|
|
155
|
+
id: ref.id,
|
|
156
|
+
agent: wakeAgent,
|
|
157
|
+
eventBus: ctx.eventBus,
|
|
158
|
+
sessionFile,
|
|
159
|
+
outputSchema: init.outputSchema,
|
|
160
|
+
outputSchemaMode: init.outputSchemaMode,
|
|
161
|
+
artifactsDir: ctx.session.sessionFile?.slice(0, -6),
|
|
162
|
+
});
|
|
138
163
|
return session;
|
|
139
164
|
};
|
|
140
165
|
};
|
package/src/telemetry-export.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-ho
|
|
|
37
37
|
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
38
38
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
39
39
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
40
|
-
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
40
|
+
import { detectResources, envDetector, resourceFromAttributes } from "@opentelemetry/resources";
|
|
41
41
|
import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
42
42
|
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
43
43
|
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
@@ -146,9 +146,13 @@ export async function initTelemetryExport(): Promise<void> {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
async function registerProviders(signalConfig: SignalConfig): Promise<void> {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
// `envDetector` parses OTEL_RESOURCE_ATTRIBUTES (percent-decoded, per spec) and
|
|
150
|
+
// OTEL_SERVICE_NAME; merged last so both take precedence over the fallback
|
|
151
|
+
// service.name — with OTEL_SERVICE_NAME still winning service.name inside the
|
|
152
|
+
// detector itself.
|
|
153
|
+
const resource = resourceFromAttributes({ "service.name": SERVICE_NAME }).merge(
|
|
154
|
+
detectResources({ detectors: [envDetector] }),
|
|
155
|
+
);
|
|
152
156
|
|
|
153
157
|
if (signalConfig.trace) {
|
|
154
158
|
const exporter = new OTLPTraceExporter();
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* the specialized tools instead.
|
|
7
7
|
*/
|
|
8
8
|
import { type BashInterceptorRule, DEFAULT_BASH_INTERCEPTOR_RULES } from "../config/settings-schema";
|
|
9
|
+
import { extractFlatShellCommandSegments } from "./shell-tokenize";
|
|
9
10
|
|
|
10
11
|
export interface InterceptionResult {
|
|
11
12
|
/** If true, the bash command should be blocked */
|
|
@@ -32,6 +33,78 @@ function compileRules(rules: BashInterceptorRule[]): Array<{ rule: BashIntercept
|
|
|
32
33
|
return compiled;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/** Finds the end of a shell word, respecting quotes and escapes; returns null for incomplete syntax. */
|
|
37
|
+
function skipShellWord(command: string, start: number): number | null {
|
|
38
|
+
let inSingle = false;
|
|
39
|
+
let inDouble = false;
|
|
40
|
+
for (let i = start; i < command.length; i++) {
|
|
41
|
+
const ch = command[i];
|
|
42
|
+
if (inSingle) {
|
|
43
|
+
if (ch === "'") inSingle = false;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (inDouble) {
|
|
47
|
+
if (ch === "\\") {
|
|
48
|
+
if (i + 1 >= command.length) return null;
|
|
49
|
+
i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (ch === '"') inDouble = false;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === "'") {
|
|
56
|
+
inSingle = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (ch === '"') {
|
|
60
|
+
inDouble = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === "\\") {
|
|
64
|
+
if (i + 1 >= command.length) return null;
|
|
65
|
+
i++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (ch === " " || ch === "\t") return i;
|
|
69
|
+
}
|
|
70
|
+
return inSingle || inDouble ? null : command.length;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Removes leading `NAME=value` assignments without interpreting shell syntax. */
|
|
74
|
+
function withoutLeadingEnvironmentAssignments(command: string): string | null {
|
|
75
|
+
let index = 0;
|
|
76
|
+
let foundAssignment = false;
|
|
77
|
+
while (index < command.length) {
|
|
78
|
+
while (command[index] === " " || command[index] === "\t") index++;
|
|
79
|
+
const assignmentStart = index;
|
|
80
|
+
if (!/[A-Za-z_]/.test(command[index] ?? "")) break;
|
|
81
|
+
let nameEnd = index + 1;
|
|
82
|
+
while (/[A-Za-z0-9_]/.test(command[nameEnd] ?? "")) nameEnd++;
|
|
83
|
+
if (command[nameEnd] !== "=") {
|
|
84
|
+
return foundAssignment ? command.slice(assignmentStart).trimStart() : null;
|
|
85
|
+
}
|
|
86
|
+
const wordEnd = skipShellWord(command, nameEnd + 1);
|
|
87
|
+
if (wordEnd === null) return null;
|
|
88
|
+
foundAssignment = true;
|
|
89
|
+
index = wordEnd;
|
|
90
|
+
if (index === command.length) return null;
|
|
91
|
+
}
|
|
92
|
+
if (!foundAssignment) return null;
|
|
93
|
+
const commandWithoutAssignments = command.slice(index).trimStart();
|
|
94
|
+
return commandWithoutAssignments.length > 0 ? commandWithoutAssignments : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function interceptionCandidates(command: string): string[] {
|
|
98
|
+
const candidates = [command.trim()];
|
|
99
|
+
const segments = extractFlatShellCommandSegments(command);
|
|
100
|
+
candidates.push(...segments.map(segment => segment.trim()));
|
|
101
|
+
for (const segment of segments) {
|
|
102
|
+
const withoutAssignments = withoutLeadingEnvironmentAssignments(segment);
|
|
103
|
+
if (withoutAssignments) candidates.push(withoutAssignments);
|
|
104
|
+
}
|
|
105
|
+
return candidates;
|
|
106
|
+
}
|
|
107
|
+
|
|
35
108
|
/**
|
|
36
109
|
* Check if a bash command should be intercepted.
|
|
37
110
|
*
|
|
@@ -43,10 +116,10 @@ export function checkBashInterception(
|
|
|
43
116
|
command: string,
|
|
44
117
|
availableTools: string[],
|
|
45
118
|
rules: BashInterceptorRule[] = DEFAULT_BASH_INTERCEPTOR_RULES,
|
|
119
|
+
originalCommand = command,
|
|
46
120
|
): InterceptionResult {
|
|
47
|
-
// Normalize command for pattern matching
|
|
48
|
-
const normalizedCommand = command.trim();
|
|
49
121
|
const compiled = compileRules(rules);
|
|
122
|
+
const candidates = interceptionCandidates(command);
|
|
50
123
|
|
|
51
124
|
for (const { rule, regex } of compiled) {
|
|
52
125
|
// Only block if the suggested tool is actually available
|
|
@@ -54,12 +127,16 @@ export function checkBashInterception(
|
|
|
54
127
|
continue;
|
|
55
128
|
}
|
|
56
129
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
130
|
+
for (const candidate of candidates) {
|
|
131
|
+
// A configured global or sticky regex carries state across calls.
|
|
132
|
+
regex.lastIndex = 0;
|
|
133
|
+
if (regex.test(candidate)) {
|
|
134
|
+
return {
|
|
135
|
+
block: true,
|
|
136
|
+
message: `Blocked: ${rule.message}\n\nOriginal command: ${originalCommand}`,
|
|
137
|
+
suggestedTool: rule.tool,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
63
140
|
}
|
|
64
141
|
}
|
|
65
142
|
|
package/src/tools/bash.ts
CHANGED
|
@@ -843,13 +843,18 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
843
843
|
return { kind: "steer" };
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
+
// Cancellable threshold: a bare Bun.sleep(thresholdMs) leaves a live, ref'd
|
|
847
|
+
// timer for the full threshold after the command finishes (or abort/steer)
|
|
848
|
+
// wins the race first — delaying SDK/headless shutdown and accumulating
|
|
849
|
+
// timers under fast command rates. Settle a withResolvers promise from
|
|
850
|
+
// setTimeout so the finally can clear it regardless of which waiter wins.
|
|
851
|
+
const { promise: thresholdPromise, resolve: resolveThreshold } = Promise.withResolvers<{
|
|
852
|
+
kind: "running";
|
|
853
|
+
}>();
|
|
854
|
+
const thresholdTimer = setTimeout(() => resolveThreshold({ kind: "running" }), thresholdMs);
|
|
846
855
|
const waiters: Array<
|
|
847
856
|
Promise<ManagedBashJobCompletion | { kind: "running" } | { kind: "steer" } | { kind: "aborted" }>
|
|
848
|
-
> = [job.completion,
|
|
849
|
-
|
|
850
|
-
if (!signal && !steeringSignal) {
|
|
851
|
-
return await Promise.race(waiters);
|
|
852
|
-
}
|
|
857
|
+
> = [job.completion, thresholdPromise];
|
|
853
858
|
|
|
854
859
|
const { promise: abortedPromise, resolve: resolveAborted } = Promise.withResolvers<{ kind: "aborted" }>();
|
|
855
860
|
const onAbort = () => resolveAborted({ kind: "aborted" });
|
|
@@ -866,6 +871,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
866
871
|
try {
|
|
867
872
|
return await Promise.race(waiters);
|
|
868
873
|
} finally {
|
|
874
|
+
clearTimeout(thresholdTimer);
|
|
869
875
|
signal?.removeEventListener("abort", onAbort);
|
|
870
876
|
steeringSignal?.removeEventListener("abort", onSteer);
|
|
871
877
|
}
|
|
@@ -920,7 +926,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
920
926
|
const rules = this.session.settings.getBashInterceptorRules();
|
|
921
927
|
const commandsToCheck = rawCommand === command ? [command] : [rawCommand, command];
|
|
922
928
|
for (const commandToCheck of commandsToCheck) {
|
|
923
|
-
const interception = checkBashInterception(commandToCheck, ctx?.toolNames ?? [], rules);
|
|
929
|
+
const interception = checkBashInterception(commandToCheck, ctx?.toolNames ?? [], rules, rawCommand);
|
|
924
930
|
if (interception.block) {
|
|
925
931
|
throw new ToolError(interception.message ?? "Command blocked");
|
|
926
932
|
}
|
|
@@ -118,12 +118,23 @@ export async function findReusableCdp(
|
|
|
118
118
|
return null;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
export function shouldPreserveConnectedBrowserFocus(target?: string): boolean {
|
|
122
|
+
return !target;
|
|
123
|
+
}
|
|
124
|
+
|
|
121
125
|
/**
|
|
122
126
|
* Pick the best page target on an attached browser. Prefer discoverable page
|
|
123
127
|
* targets first so Chromium/Edge attach flows that hide pages from
|
|
124
128
|
* `browser.pages()` can still return a usable tab.
|
|
129
|
+
*
|
|
130
|
+
* `preferVisible` is for attaching to a browser a human is using: among equally
|
|
131
|
+
* usable tabs, take the one that is actually foregrounded rather than whichever
|
|
132
|
+
* target CDP happens to enumerate first.
|
|
125
133
|
*/
|
|
126
|
-
export async function pickElectronTarget(
|
|
134
|
+
export async function pickElectronTarget(
|
|
135
|
+
browser: Browser,
|
|
136
|
+
options: { matcher?: string; preferVisible?: boolean } = {},
|
|
137
|
+
): Promise<Page> {
|
|
127
138
|
const discoveredPages = await Promise.all(
|
|
128
139
|
browser.targets().map(async target => {
|
|
129
140
|
if (String(target.type()) !== "page") return null;
|
|
@@ -132,14 +143,14 @@ export async function pickElectronTarget(browser: Browser, matcher?: string): Pr
|
|
|
132
143
|
);
|
|
133
144
|
const usablePages = discoveredPages.filter((page): page is Page => page !== null);
|
|
134
145
|
if (usablePages.length > 0) {
|
|
135
|
-
return pickPageFromList(usablePages,
|
|
146
|
+
return pickPageFromList(usablePages, options);
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
const fallbackPages = await browser.pages();
|
|
139
150
|
if (!fallbackPages.length) {
|
|
140
151
|
throw new ToolError("No page targets available on the attached browser");
|
|
141
152
|
}
|
|
142
|
-
return pickPageFromList(fallbackPages,
|
|
153
|
+
return pickPageFromList(fallbackPages, options);
|
|
143
154
|
}
|
|
144
155
|
|
|
145
156
|
async function enrichPages(pages: Page[]): Promise<Array<{ page: Page; url: string; title: string }>> {
|
|
@@ -152,19 +163,33 @@ async function enrichPages(pages: Page[]): Promise<Array<{ page: Page; url: stri
|
|
|
152
163
|
);
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
async function pickPageFromList(pages: Page[], matcher?: string): Promise<Page> {
|
|
166
|
+
async function pickPageFromList(pages: Page[], options: { matcher?: string; preferVisible?: boolean }): Promise<Page> {
|
|
156
167
|
const enriched = await enrichPages(pages);
|
|
157
|
-
if (matcher) {
|
|
158
|
-
const needle = matcher.toLowerCase();
|
|
168
|
+
if (options.matcher) {
|
|
169
|
+
const needle = options.matcher.toLowerCase();
|
|
159
170
|
const hit = enriched.find(p => p.url.toLowerCase().includes(needle) || p.title.toLowerCase().includes(needle));
|
|
160
171
|
if (hit) return hit.page;
|
|
161
172
|
const summary = enriched.map(p => `- ${p.title || "(untitled)"} ${p.url}`).join("\n");
|
|
162
|
-
throw new ToolError(`No page target matched ${JSON.stringify(matcher)}. Available pages:\n${summary}`);
|
|
173
|
+
throw new ToolError(`No page target matched ${JSON.stringify(options.matcher)}. Available pages:\n${summary}`);
|
|
163
174
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
enriched[0]!.page
|
|
175
|
+
const usable = enriched.filter(
|
|
176
|
+
p => !ATTACH_TARGET_SKIP_PATTERN.test(p.url) && !ATTACH_TARGET_SKIP_PATTERN.test(p.title),
|
|
167
177
|
);
|
|
178
|
+
if (options.preferVisible && usable.length > 1) {
|
|
179
|
+
// Best-effort foreground probe; a tab that cannot answer counts as hidden.
|
|
180
|
+
const visibility = await Promise.all(
|
|
181
|
+
usable.map(async p => {
|
|
182
|
+
try {
|
|
183
|
+
return (await p.page.evaluate(() => document.visibilityState === "visible")) === true;
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
const foreground = visibility.indexOf(true);
|
|
190
|
+
if (foreground >= 0) return usable[foreground]!.page;
|
|
191
|
+
}
|
|
192
|
+
return usable[0]?.page ?? enriched[0]!.page;
|
|
168
193
|
}
|
|
169
194
|
|
|
170
195
|
/**
|