@oar-sh/oar 0.9.12 → 0.9.14
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/.github/extensions/web-relay/extension.mjs +9 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +15 -1
- package/.github/extensions/web-relay/skills/question-bridge.mjs +4 -1
- package/CHANGELOG.md +508 -0
- package/README.md +12 -12
- package/bin/oar.js +155 -94
- package/package.json +5 -5
- package/server/README.md +479 -157
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +10 -4
- package/server/claude-worker/claude-session-process.mjs +92 -15
- package/server/claude-worker/claude-turn-publisher.mjs +20 -1
- package/server/claude-worker/subagent-timeline-recorder.mjs +75 -8
- package/server/claude-worker/task-output-discovery.mjs +28 -2
- package/server/copilot-worker/copilot-ask-user-tool.mjs +1 -1
- package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
- package/server/copilot-worker/copilot-prompt-context.mjs +3 -1
- package/server/copilot-worker/copilot-question-bridge.mjs +11 -6
- package/server/copilot-worker/copilot-sdk-adapter.mjs +11 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +24 -1
- package/server/cursor-worker/cursor-ask-user-tool.mjs +1 -1
- package/server/cursor-worker/cursor-sdk-adapter.mjs +22 -7
- package/server/cursor-worker/cursor-session-worker.mjs +9 -2
- package/server/cursor-worker/cursor-turn-runner.mjs +18 -3
- package/server/db-schema.mjs +10 -0
- package/server/grok-worker/acp-host-services.mjs +6 -3
- package/server/grok-worker/grok-background-tasks.mjs +7 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +14 -0
- package/server/grok-worker/grok-turn-runner.mjs +13 -3
- package/server/migrations/0018-background-task-rows.mjs +101 -0
- package/server/public/app/action-confirmations.js +12 -15
- package/server/public/app/api-client.js +60 -4
- package/server/public/app/ask-user-view.js +29 -4
- package/server/public/app/background-tasks-view.mjs +56 -7
- package/server/public/app/bootstrap.js +7 -0
- package/server/public/app/conversation-preferences.mjs +10 -4
- package/server/public/app/conversation-provider-indicator.mjs +18 -8
- package/server/public/app/conversation-view.js +33 -9
- package/server/public/app/host-suspend-ui.js +45 -44
- package/server/public/app/journal-view.js +9 -3
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/question-answer-rule.mjs +15 -0
- package/server/public/app/question-multi-select.mjs +9 -2
- package/server/public/app/relay-event-view.mjs +5 -1
- package/server/public/app/relay-prompt-sanitizer.mjs +9 -2
- package/server/public/app/relay-restart-note-view.mjs +36 -0
- package/server/public/app/relay-restart-ui.js +568 -0
- package/server/public/app/settings-modal.js +52 -0
- package/server/public/app/socket-handlers.js +1 -1
- package/server/public/app/task-console-render.mjs +171 -7
- package/server/public/app/task-console.mjs +111 -11
- package/server/public/app/task-output-text.mjs +15 -6
- package/server/public/app/transcript-jump-bottom-view.mjs +5 -1
- package/server/public/app/transcript-jump-bottom.mjs +20 -0
- package/server/public/index.html +76 -5
- package/server/relay-exit-codes.mjs +6 -0
- package/server/relay-self-restart.mjs +6 -1
- package/server/relay-tools.md +17 -9
- package/server/repositories/message-repository.mjs +32 -28
- package/server/repositories/remote-relay-repository.mjs +4 -2
- package/server/repositories/subagent-timeline-statements.mjs +2 -0
- package/server/routes/ask-user-routes.mjs +41 -7
- package/server/routes/messages-routes.mjs +220 -37
- package/server/routes/preview-routes.mjs +73 -7
- package/server/routes/relay-board-routes.mjs +19 -2
- package/server/routes/relay-restart-routes.mjs +73 -0
- package/server/routes/sessions-routes.mjs +79 -5
- package/server/server-runtime.mjs +335 -218
- package/server/services/about-service.mjs +13 -12
- package/server/services/background-task-persistence.mjs +133 -0
- package/server/services/background-task-store.mjs +240 -13
- package/server/services/conversation-model-fallback.mjs +57 -0
- package/server/services/cursor-session-root-service.mjs +70 -6
- package/server/services/data-dir-resolution.mjs +208 -19
- package/server/services/earlier-database-service.mjs +631 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/oar-cli-helpers.mjs +6 -1
- package/server/services/oar-state-layout.mjs +107 -0
- package/server/services/oar-state-migration-service.mjs +387 -96
- package/server/services/package-data-rescue-preinstall.mjs +22 -0
- package/server/services/package-data-rescue.mjs +137 -0
- package/server/services/preview-proxy-server.mjs +14 -1
- package/server/services/preview-registry-service.mjs +3 -2
- package/server/services/process-list-service.mjs +2 -1
- package/server/services/provider-model-selection.mjs +195 -0
- package/server/services/push-dispatch-service.mjs +42 -8
- package/server/services/relay-event-service.mjs +210 -14
- package/server/services/relay-prompt-sanitizer.mjs +7 -0
- package/server/services/relay-restart-picture.mjs +209 -0
- package/server/services/relay-restart-queue.mjs +387 -0
- package/server/services/relay-shutdown-gate.mjs +4 -1
- package/server/services/relay-singleton-guard.mjs +300 -36
- package/server/services/remote-relay-caller-context.mjs +7 -2
- package/server/services/remote-relay-dispatcher.mjs +217 -28
- package/server/services/restart-resume-service.mjs +303 -0
- package/server/services/session-activity-tracker.mjs +52 -15
- package/server/services/session-worker-launch-service.mjs +31 -1
- package/server/services/session-worker-process-service.mjs +6 -4
- package/server/services/stale-turn-recovery.mjs +99 -0
- package/server/services/subagent-timeline-service.mjs +4 -1
- package/server/services/task-output-service.mjs +217 -18
- package/server/services/windows-autostart-service.mjs +329 -31
- package/server/services/windows-boot-autostart-service.mjs +188 -60
- package/shared/preview-instructions.mjs +17 -12
- package/shared/preview-tool-core.mjs +32 -14
- package/shared/question-answer-rule.mjs +63 -0
- package/shared/relay-events.mjs +106 -3
- package/shared/remote-relay-contract.mjs +32 -15
- package/shared/subagent-timeline.mjs +4 -0
- package/shared/task-output-text.mjs +196 -83
- package/shared/windows-system-tools.mjs +25 -0
- package/shared/worker-runtime/config-loader.mjs +54 -8
- package/shared/worker-runtime/prompt-context.mjs +3 -1
- package/shared/worker-runtime/worker-websocket-link.mjs +96 -20
- package/shared/worker-runtime/working-directory.mjs +65 -0
- package/server/uploads/image_input.md +0 -540
|
@@ -4,6 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import { createAskUserBridge } from '../../shared/ask-user-bridge.mjs';
|
|
5
5
|
import { createRelayQuestion } from '../../shared/question-wait.mjs';
|
|
6
6
|
import { DEFAULT_QUESTION_TIMEOUT_MS } from '../../shared/question-timeout.mjs';
|
|
7
|
+
import { readApprovalAnswer } from '../../shared/question-answer-rule.mjs';
|
|
7
8
|
import { EMPTY_TURN_COMPLETION_NOTE } from '../../shared/empty-turn-completion.mjs';
|
|
8
9
|
import { stripModelTierSuffix } from '../../shared/claude-cloud/repo-url.mjs';
|
|
9
10
|
import { resolveDeliveredAttribution, sameClaudeAttribution } from '../../shared/claude-attribution.mjs';
|
|
@@ -27,6 +28,7 @@ const PERMISSION_ALLOW = 'Allow';
|
|
|
27
28
|
const PERMISSION_DENY = 'Deny';
|
|
28
29
|
const QUESTION_UNANSWERED_MESSAGE = 'The user did not answer. Continue without this, or end the turn and say what you need.';
|
|
29
30
|
const PERMISSION_DENIED_MESSAGE = 'The user denied this tool use.';
|
|
31
|
+
const PERMISSION_UNREADABLE_MESSAGE = 'The permission card was answered with neither Allow nor Deny, so the tool use is not allowed.';
|
|
30
32
|
// A catch-up after a worker restart reads the events of one unfinished turn;
|
|
31
33
|
// the cap only stops a cursor that never advances.
|
|
32
34
|
const MAX_CATCH_UP_PAGES = 200;
|
|
@@ -253,8 +255,8 @@ export function createClaudeCloudSessionRunner({
|
|
|
253
255
|
const requested = String(message?.model || '').trim();
|
|
254
256
|
const perTurn = requested && requested.toLowerCase() !== 'auto' ? requested : '';
|
|
255
257
|
// The delivered message names the conversation's current model; the launch
|
|
256
|
-
// model is what it was when the worker started
|
|
257
|
-
//
|
|
258
|
+
// model is what it was when the worker started. A change after the first
|
|
259
|
+
// message is applied before the next one (applyModel).
|
|
258
260
|
return stripModelTierSuffix(String(message?.providerModel || '').trim() || perTurn || defaultModel);
|
|
259
261
|
}
|
|
260
262
|
|
|
@@ -723,8 +725,12 @@ export function createClaudeCloudSessionRunner({
|
|
|
723
725
|
});
|
|
724
726
|
const result = await askUserBridge.waitForRelayQuestionAnswer(questionId, { signal });
|
|
725
727
|
if (result.timedOut || signal.aborted) return { behavior: 'deny', message: QUESTION_UNANSWERED_MESSAGE };
|
|
726
|
-
|
|
727
|
-
return { behavior: '
|
|
728
|
+
const decision = readApprovalAnswer(result.answer, { approve: PERMISSION_ALLOW, deny: PERMISSION_DENY });
|
|
729
|
+
if (decision === 'approve') return { behavior: 'allow', updatedInput: request.input };
|
|
730
|
+
if (decision === 'deny') return { behavior: 'deny', message: PERMISSION_DENIED_MESSAGE };
|
|
731
|
+
// The relay takes only the choices on this card; anything else is no
|
|
732
|
+
// decision of the user's, and the agent is told so.
|
|
733
|
+
return { behavior: 'deny', message: PERMISSION_UNREADABLE_MESSAGE };
|
|
728
734
|
}
|
|
729
735
|
|
|
730
736
|
async function askUser(t, request, signal) {
|
|
@@ -39,11 +39,13 @@ import {
|
|
|
39
39
|
isStaleAttemptError,
|
|
40
40
|
} from './claude-turn-publisher.mjs';
|
|
41
41
|
import { createSubagentTimelineRecorder } from './subagent-timeline-recorder.mjs';
|
|
42
|
+
import { SUBAGENT_TASK_TYPES } from '../../shared/subagent-timeline.mjs';
|
|
42
43
|
import { createAskUserBridge } from '../../shared/ask-user-bridge.mjs';
|
|
43
44
|
import { createRemoteRelayToolGate } from '../../shared/remote-relay-tool-core.mjs';
|
|
44
45
|
import { buildRelayStopFailure } from '../../shared/relay-stop-failure.mjs';
|
|
45
46
|
import { createTurnLiveness, resolveTurnStallWindows } from '../../shared/worker-runtime/turn-liveness.mjs';
|
|
46
47
|
import { createKeptPosts } from '../../shared/worker-runtime/kept-posts.mjs';
|
|
48
|
+
import { explainLaunchFailure, missingWorkingDirectoryError } from '../../shared/worker-runtime/working-directory.mjs';
|
|
47
49
|
import {
|
|
48
50
|
ANSWERED_ELSEWHERE_KIND,
|
|
49
51
|
DELIVERY_ANSWERED_ELSEWHERE_TEXT,
|
|
@@ -60,6 +62,7 @@ import {
|
|
|
60
62
|
import { createUsageLimitTracker } from '../../shared/claude-usage-limit.mjs';
|
|
61
63
|
import {
|
|
62
64
|
discoverTaskOutputPaths,
|
|
65
|
+
shellCommandsOf,
|
|
63
66
|
exitCodeFromSummary,
|
|
64
67
|
isShellTaskType,
|
|
65
68
|
outputDirOf,
|
|
@@ -81,8 +84,6 @@ function modeAppendClass(relayMode) {
|
|
|
81
84
|
|
|
82
85
|
export { STEER_FOLDED_TEXT, STEER_STOPPED_TEXT };
|
|
83
86
|
|
|
84
|
-
/** Task types that run a model of their own (vs. bash/monitor/workflow). */
|
|
85
|
-
const AGENT_TASK_TYPES = new Set(['local_agent', 'agent', 'subagent']);
|
|
86
87
|
const MAX_TASK_TOOL_CALL_CHARS = 300;
|
|
87
88
|
/** Output paths remembered per process (one per backgrounded shell). */
|
|
88
89
|
const MAX_TASK_OUTPUT_PATHS = 200;
|
|
@@ -458,6 +459,9 @@ export function createClaudeSessionRunner({
|
|
|
458
459
|
readContextUsageImpl = readContextUsage,
|
|
459
460
|
readPlanUsageImpl = readPlanUsage,
|
|
460
461
|
relocateTranscriptImpl = relocateClaudeTranscriptForCwd,
|
|
462
|
+
// The error a spawn in `cwd` fails with up front, or null
|
|
463
|
+
// (shared/worker-runtime/working-directory.mjs).
|
|
464
|
+
checkWorkingDirectoryImpl = missingWorkingDirectoryError,
|
|
461
465
|
idleShutdownMs = 10 * 60_000,
|
|
462
466
|
getBackgroundTaskTimeoutMs = () => 0,
|
|
463
467
|
// The per-conversation auto-compact window (token count, null = Auto). Read
|
|
@@ -1762,7 +1766,7 @@ export function createClaudeSessionRunner({
|
|
|
1762
1766
|
// agent-like task runs on the session model. Resolved at publish
|
|
1763
1767
|
// time so the spawn block landing after task_started still heals on
|
|
1764
1768
|
// the next publish. Bash & co. get no model at all.
|
|
1765
|
-
const agentLike =
|
|
1769
|
+
const agentLike = SUBAGENT_TASK_TYPES.has(String(task.taskType || ''));
|
|
1766
1770
|
const pinnedModel = (task.toolUseId && processRef.subagentSpawns.get(task.toolUseId)) || '';
|
|
1767
1771
|
const activity = (task.toolUseId && processRef.subagentActivity.get(task.toolUseId)) || {};
|
|
1768
1772
|
const model = agentLike ? (activity.model || pinnedModel || processRef.initModel || null) : null;
|
|
@@ -1776,6 +1780,7 @@ export function createClaudeSessionRunner({
|
|
|
1776
1780
|
lastToolCall: (agentLike && activity.lastToolCall) || null,
|
|
1777
1781
|
totalTokens: task.totalTokens ?? null,
|
|
1778
1782
|
toolUseId: task.toolUseId || null,
|
|
1783
|
+
...(task.taskRunId ? { taskRunId: task.taskRunId } : {}),
|
|
1779
1784
|
subagentType: task.subagentType || null,
|
|
1780
1785
|
model,
|
|
1781
1786
|
modelInherited: Boolean(model && !pinnedModel && !activity.model),
|
|
@@ -1783,8 +1788,10 @@ export function createClaudeSessionRunner({
|
|
|
1783
1788
|
// sanitizer whitelists and re-clamps it before storing.
|
|
1784
1789
|
...(task.workflowProgress ? { workflowProgress: task.workflowProgress } : {}),
|
|
1785
1790
|
...(agentLike && Number.isInteger(task.steps) ? { steps: task.steps } : {}),
|
|
1786
|
-
// Where the relay reads the task's console from
|
|
1791
|
+
// Where the relay reads the task's console from, and the command
|
|
1792
|
+
// line the console reads a `| tail` off.
|
|
1787
1793
|
...(isShellTaskType(task.taskType) ? taskOutputFields(processRef, taskId) : {}),
|
|
1794
|
+
...(isShellTaskType(task.taskType) && taskCommand(processRef, taskId, task) ? { command: taskCommand(processRef, taskId, task) } : {}),
|
|
1788
1795
|
};
|
|
1789
1796
|
});
|
|
1790
1797
|
return postTaskSet({ tasks, ...(resync ? { resync: true } : {}) }).catch((error) => {
|
|
@@ -1846,12 +1853,49 @@ export function createClaudeSessionRunner({
|
|
|
1846
1853
|
/** A backgrounded Bash/Monitor result: note its file, and show it on the live row. */
|
|
1847
1854
|
function recordTaskOutputPaths(sdkMessage) {
|
|
1848
1855
|
let liveChanged = false;
|
|
1849
|
-
for (const { taskId, outputPath } of discoverTaskOutputPaths(sdkMessage)) {
|
|
1856
|
+
for (const { taskId, outputPath, toolUseId } of discoverTaskOutputPaths(sdkMessage)) {
|
|
1857
|
+
if (toolUseId && !proc.taskToolUses.has(taskId)) {
|
|
1858
|
+
proc.taskToolUses.set(taskId, toolUseId);
|
|
1859
|
+
capMap(proc.taskToolUses, MAX_TASK_OUTPUT_PATHS);
|
|
1860
|
+
if (proc.liveTasks.has(taskId) && proc.shellCommands.has(toolUseId)) liveChanged = true;
|
|
1861
|
+
}
|
|
1850
1862
|
if (rememberTaskOutputPath(proc, taskId, outputPath) && proc.liveTasks.has(taskId)) liveChanged = true;
|
|
1851
1863
|
}
|
|
1852
1864
|
if (liveChanged) publishBackgroundTasks();
|
|
1853
1865
|
}
|
|
1854
1866
|
|
|
1867
|
+
function capMap(map, max) {
|
|
1868
|
+
while (map.size > max) map.delete(map.keys().next().value);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// The command line of each Bash or Monitor call, by its tool_use id: a
|
|
1872
|
+
// shell task's row names it (task_started's tool_use_id, or the call whose
|
|
1873
|
+
// result announced the task's file).
|
|
1874
|
+
function recordShellCommands(sdkMessage) {
|
|
1875
|
+
for (const { toolUseId, command } of shellCommandsOf(sdkMessage)) {
|
|
1876
|
+
proc.shellCommands.set(toolUseId, command);
|
|
1877
|
+
capMap(proc.shellCommands, MAX_TASK_OUTPUT_PATHS);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// A task's command, pinned to its id once found: the 200 newest calls
|
|
1882
|
+
// the map by tool_use id keeps may move past a long-running task's call.
|
|
1883
|
+
function taskCommand(processRef, taskId, task) {
|
|
1884
|
+
const pinned = processRef.taskCommands.get(taskId);
|
|
1885
|
+
if (pinned) return pinned;
|
|
1886
|
+
const toolUseId = task?.toolUseId || processRef.taskToolUses.get(taskId) || '';
|
|
1887
|
+
const command = (toolUseId && processRef.shellCommands.get(toolUseId)) || '';
|
|
1888
|
+
if (command && (processRef.liveTasks.has(taskId) || processRef.departedTasks.has(taskId))) processRef.taskCommands.set(taskId, command);
|
|
1889
|
+
return command;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// Pinned commands go with their task: live or departed until its settle.
|
|
1893
|
+
function prunePinnedCommands(processRef) {
|
|
1894
|
+
for (const taskId of [...processRef.taskCommands.keys()]) {
|
|
1895
|
+
if (!processRef.liveTasks.has(taskId) && !processRef.departedTasks.has(taskId)) processRef.taskCommands.delete(taskId);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1855
1899
|
/**
|
|
1856
1900
|
* The relay's fallback for a shell task with no file: the CLI's last 8 KiB
|
|
1857
1901
|
* (`Query.getTaskOutput`, implemented in sdk.mjs though sdk.d.ts does not
|
|
@@ -2175,7 +2219,7 @@ export function createClaudeSessionRunner({
|
|
|
2175
2219
|
// alias like "opus") and the full input of the tool it is calling now.
|
|
2176
2220
|
// Single-lined and capped: the panel crops it to its width.
|
|
2177
2221
|
function recordSubagentActivity(sdkMessage) {
|
|
2178
|
-
const parentId =
|
|
2222
|
+
const parentId = timelineRecorder.runIdFor(sdkMessage?.parent_tool_use_id);
|
|
2179
2223
|
if (!parentId) return;
|
|
2180
2224
|
const entry = proc.subagentActivity.get(parentId) || {};
|
|
2181
2225
|
const before = `${entry.model || ''}\n${entry.lastToolCall || ''}`;
|
|
@@ -2321,6 +2365,7 @@ export function createClaudeSessionRunner({
|
|
|
2321
2365
|
function observeProcessLevel(sdkMessage) {
|
|
2322
2366
|
const type = String(sdkMessage?.type || '');
|
|
2323
2367
|
if (type === 'assistant') {
|
|
2368
|
+
recordShellCommands(sdkMessage);
|
|
2324
2369
|
recordSubagentSpawns(sdkMessage);
|
|
2325
2370
|
recordSubagentActivity(sdkMessage);
|
|
2326
2371
|
return;
|
|
@@ -2550,11 +2595,15 @@ export function createClaudeSessionRunner({
|
|
|
2550
2595
|
.filter((task) => String(task?.task_id || '').trim())
|
|
2551
2596
|
.map((task) => {
|
|
2552
2597
|
const taskId = String(task.task_id).trim();
|
|
2553
|
-
const
|
|
2598
|
+
const runId = String(task?.run_id || '').trim();
|
|
2599
|
+
// Another run of a task still listed (resumed at once): its own row.
|
|
2600
|
+
const listed = previous.get(taskId) || {};
|
|
2601
|
+
const known = runId && listed.taskRunId && listed.taskRunId !== runId ? {} : listed;
|
|
2554
2602
|
return [taskId, {
|
|
2555
2603
|
...known,
|
|
2556
2604
|
taskType: String(task?.task_type || '').trim() || known.taskType || '',
|
|
2557
2605
|
description: String(task?.description || '').trim() || known.description || '',
|
|
2606
|
+
taskRunId: runId || known.taskRunId || '',
|
|
2558
2607
|
startedAt: known.startedAt || Date.now(),
|
|
2559
2608
|
}];
|
|
2560
2609
|
}));
|
|
@@ -2596,6 +2645,7 @@ export function createClaudeSessionRunner({
|
|
|
2596
2645
|
}
|
|
2597
2646
|
syncWorkflowPoller(proc);
|
|
2598
2647
|
publishBackgroundTasks();
|
|
2648
|
+
prunePinnedCommands(proc);
|
|
2599
2649
|
evaluateLifecycle();
|
|
2600
2650
|
return;
|
|
2601
2651
|
}
|
|
@@ -2609,8 +2659,14 @@ export function createClaudeSessionRunner({
|
|
|
2609
2659
|
known.subagentType = String(sdkMessage.subagent_type || '').trim() || known.subagentType || '';
|
|
2610
2660
|
// The task's spawning tool_use id is the same identity the subagent
|
|
2611
2661
|
// bubbles key on — recorded so the panel and the bubbles can be
|
|
2612
|
-
// correlated (and a targeted stop can find its task).
|
|
2613
|
-
|
|
2662
|
+
// correlated (and a targeted stop can find its task). A resumed agent
|
|
2663
|
+
// names the call that resumed it (SendMessage); the recorder leads
|
|
2664
|
+
// that back to the agent's run.
|
|
2665
|
+
const toolUseId = String(sdkMessage.tool_use_id || '').trim();
|
|
2666
|
+
known.toolUseId = timelineRecorder.runIdFor(toolUseId) || known.toolUseId || '';
|
|
2667
|
+
// Which run of the task this is: the SDK's run id, else (an older
|
|
2668
|
+
// CLI) the tool use that started it, which differs per run too.
|
|
2669
|
+
known.taskRunId = String(sdkMessage.run_id || '').trim() || known.taskRunId || toolUseId;
|
|
2614
2670
|
// task_started can be the first event carrying the task_type; a
|
|
2615
2671
|
// workflow revealed here must start its poller too.
|
|
2616
2672
|
syncWorkflowPoller(proc);
|
|
@@ -2625,7 +2681,7 @@ export function createClaudeSessionRunner({
|
|
|
2625
2681
|
known.summary = String(sdkMessage.summary || '').trim() || known.summary;
|
|
2626
2682
|
known.lastToolName = String(sdkMessage.last_tool_name || '').trim() || known.lastToolName;
|
|
2627
2683
|
known.subagentType = String(sdkMessage.subagent_type || '').trim() || known.subagentType || '';
|
|
2628
|
-
known.toolUseId =
|
|
2684
|
+
known.toolUseId = timelineRecorder.runIdFor(sdkMessage.tool_use_id) || known.toolUseId || '';
|
|
2629
2685
|
const totalTokens = Number(sdkMessage.usage?.total_tokens);
|
|
2630
2686
|
if (Number.isFinite(totalTokens)) known.totalTokens = totalTokens;
|
|
2631
2687
|
// The sub-agent's tool calls so far: the panel row's step count.
|
|
@@ -2655,15 +2711,19 @@ export function createClaudeSessionRunner({
|
|
|
2655
2711
|
rememberTaskOutputPath(proc, taskId, sdkMessage.output_file);
|
|
2656
2712
|
const shellLike = isShellTaskType(published?.taskType);
|
|
2657
2713
|
const outputPath = shellLike ? (proc.taskOutputPaths.get(taskId) || '') : '';
|
|
2714
|
+
const command = shellLike ? taskCommand(proc, taskId, { toolUseId: String(sdkMessage.tool_use_id || published?.toolUseId || '').trim() }) : '';
|
|
2715
|
+
proc.taskCommands.delete(taskId);
|
|
2658
2716
|
const exitCode = shellLike ? exitCodeFromSummary(sdkMessage.summary) : null;
|
|
2659
2717
|
const toolUses = Number(sdkMessage.usage?.tool_uses);
|
|
2660
|
-
const steps =
|
|
2718
|
+
const steps = SUBAGENT_TASK_TYPES.has(String(published?.taskType || '')) && Number.isFinite(toolUses) && toolUses >= 0
|
|
2661
2719
|
? Math.floor(toolUses)
|
|
2662
2720
|
: null;
|
|
2721
|
+
const settledRunId = String(sdkMessage.run_id || published?.taskRunId || '').trim();
|
|
2663
2722
|
void keptTaskSettles.post('/api/background-task-settled', {
|
|
2664
2723
|
conversationId: sdkSessionId,
|
|
2665
2724
|
taskId,
|
|
2666
|
-
toolUseId:
|
|
2725
|
+
toolUseId: timelineRecorder.runIdFor(sdkMessage.tool_use_id) || published?.toolUseId || null,
|
|
2726
|
+
...(settledRunId ? { taskRunId: settledRunId } : {}),
|
|
2667
2727
|
status: String(sdkMessage.status || '').trim() || 'completed',
|
|
2668
2728
|
summary: String(sdkMessage.summary || '').trim().slice(0, 500) || null,
|
|
2669
2729
|
...(sdkMessage.reason ? { reason: String(sdkMessage.reason).slice(0, 40) } : {}),
|
|
@@ -2673,6 +2733,7 @@ export function createClaudeSessionRunner({
|
|
|
2673
2733
|
startedAt: published.startedAt || null,
|
|
2674
2734
|
} : {}),
|
|
2675
2735
|
...(outputPath ? { outputPath } : {}),
|
|
2736
|
+
...(command ? { command } : {}),
|
|
2676
2737
|
...(exitCode !== null ? { exitCode } : {}),
|
|
2677
2738
|
...(steps !== null ? { steps } : {}),
|
|
2678
2739
|
});
|
|
@@ -3456,6 +3517,7 @@ export function createClaudeSessionRunner({
|
|
|
3456
3517
|
message: ctx.message,
|
|
3457
3518
|
errorText: String(streamError?.message || streamError || 'unknown error'),
|
|
3458
3519
|
usageLimit: usageLimit.classifyException(streamError),
|
|
3520
|
+
error: streamError,
|
|
3459
3521
|
}).catch(() => {});
|
|
3460
3522
|
}
|
|
3461
3523
|
continue;
|
|
@@ -3477,6 +3539,11 @@ export function createClaudeSessionRunner({
|
|
|
3477
3539
|
// Spawn / adapt
|
|
3478
3540
|
|
|
3479
3541
|
function spawnProcess(message, { resume: resumeOverride = '' } = {}) {
|
|
3542
|
+
// A spawn in a folder that is gone fails with ENOENT, which the SDK
|
|
3543
|
+
// reports as a native binary that does not match the libc. Thrown here,
|
|
3544
|
+
// the delivery fails with the folder's name instead (handlePendingPayload).
|
|
3545
|
+
const missingFolder = checkWorkingDirectoryImpl(cwd);
|
|
3546
|
+
if (missingFolder) throw missingFolder;
|
|
3480
3547
|
const relayMode = message.relayMode || 'agent';
|
|
3481
3548
|
const model = resolvePerTurnModel(message);
|
|
3482
3549
|
const effort = normalizeClaudeEffort(message.reasoningEffort);
|
|
@@ -3517,6 +3584,12 @@ export function createClaudeSessionRunner({
|
|
|
3517
3584
|
// next to it.
|
|
3518
3585
|
taskOutputPaths: new Map(),
|
|
3519
3586
|
taskOutputDir: '',
|
|
3587
|
+
// tool_use id → the command line of a Bash or Monitor call, and taskId
|
|
3588
|
+
// → the call whose result announced the task: a shell row's command.
|
|
3589
|
+
shellCommands: new Map(),
|
|
3590
|
+
taskToolUses: new Map(),
|
|
3591
|
+
// taskId → its command, pinned while the task is live or departed.
|
|
3592
|
+
taskCommands: new Map(),
|
|
3520
3593
|
// taskId → { runId, labels, digestJson } for live local_workflow tasks;
|
|
3521
3594
|
// pruned by syncWorkflowPoller when a task leaves the live set.
|
|
3522
3595
|
workflowStates: new Map(),
|
|
@@ -3691,7 +3764,11 @@ export function createClaudeSessionRunner({
|
|
|
3691
3764
|
syncDeliveryReadiness();
|
|
3692
3765
|
}
|
|
3693
3766
|
} catch (error) {
|
|
3694
|
-
|
|
3767
|
+
// A launch that failed because the folder went away after the check
|
|
3768
|
+
// above is told as that, too.
|
|
3769
|
+
if (!processRef.aborted && !abortController.signal.aborted) {
|
|
3770
|
+
streamError = explainLaunchFailure(error, cwd, { check: checkWorkingDirectoryImpl });
|
|
3771
|
+
}
|
|
3695
3772
|
} finally {
|
|
3696
3773
|
try {
|
|
3697
3774
|
processRef.turn.endInput?.();
|
|
@@ -4170,7 +4247,7 @@ export function createClaudeSessionRunner({
|
|
|
4170
4247
|
settingsWindowOpen = false;
|
|
4171
4248
|
const errorText = String(error?.message || error || 'unknown error');
|
|
4172
4249
|
dbg('claude turn failed', message.id, errorText);
|
|
4173
|
-
await publisher.publishTurnException({ message, errorText, usageLimit: usageLimit.classifyException(error) });
|
|
4250
|
+
await publisher.publishTurnException({ message, errorText, usageLimit: usageLimit.classifyException(error), error });
|
|
4174
4251
|
return true;
|
|
4175
4252
|
} finally {
|
|
4176
4253
|
endAdmission();
|
|
@@ -4211,7 +4288,7 @@ export function createClaudeSessionRunner({
|
|
|
4211
4288
|
* subagents have no task id and remain full-turn-Stop only.
|
|
4212
4289
|
*/
|
|
4213
4290
|
async function stopBackgroundTaskByToolUseId(toolUseId) {
|
|
4214
|
-
const normalized =
|
|
4291
|
+
const normalized = timelineRecorder.runIdFor(toolUseId);
|
|
4215
4292
|
if (!normalized || !proc) return false;
|
|
4216
4293
|
for (const [taskId, task] of proc.liveTasks.entries()) {
|
|
4217
4294
|
if (String(task?.toolUseId || '').trim() === normalized) {
|
|
@@ -4,6 +4,7 @@ import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
|
|
|
4
4
|
import { EMPTY_TURN_COMPLETION_NOTE } from '../../shared/empty-turn-completion.mjs';
|
|
5
5
|
import { buildSteerSettleFailure, steerSettleFailureText } from '../../shared/steer-settle-failure.mjs';
|
|
6
6
|
import { buildUsageLimitFailure } from '../../shared/claude-usage-limit.mjs';
|
|
7
|
+
import { WORKING_DIRECTORY_MISSING_CODE, isWorkingDirectoryMissingError } from '../../shared/worker-runtime/working-directory.mjs';
|
|
7
8
|
|
|
8
9
|
const PLAN_BOARD_ACTIONS = [
|
|
9
10
|
{ id: 'autopilot', label: 'Implement in autopilot', mode: 'autopilot' },
|
|
@@ -390,7 +391,7 @@ export function createClaudeTurnPublisher({ api, dbg = () => {}, takeWorkflowRun
|
|
|
390
391
|
});
|
|
391
392
|
}
|
|
392
393
|
|
|
393
|
-
async function publishTurnException({ message, errorText, usageLimit = null }) {
|
|
394
|
+
async function publishTurnException({ message, errorText, usageLimit = null, error = null }) {
|
|
394
395
|
if (usageLimit) {
|
|
395
396
|
await publishResponse(message, {
|
|
396
397
|
text: usageLimit.text || errorText,
|
|
@@ -399,6 +400,24 @@ export function createClaudeTurnPublisher({ api, dbg = () => {}, takeWorkflowRun
|
|
|
399
400
|
});
|
|
400
401
|
return;
|
|
401
402
|
}
|
|
403
|
+
// The conversation's folder is gone: the error names it and says what to
|
|
404
|
+
// do, which neither a retry nor a sign-in changes.
|
|
405
|
+
if (isWorkingDirectoryMissingError(error)) {
|
|
406
|
+
await publishResponse(message, {
|
|
407
|
+
text: `System note: ${error.message} ${error.guidance}`,
|
|
408
|
+
model: null,
|
|
409
|
+
terminalError: {
|
|
410
|
+
kind: 'claude-turn-failed',
|
|
411
|
+
code: WORKING_DIRECTORY_MISSING_CODE,
|
|
412
|
+
stableCode: `claude.${WORKING_DIRECTORY_MISSING_CODE}`,
|
|
413
|
+
message: error.message,
|
|
414
|
+
guidance: error.guidance,
|
|
415
|
+
failedAt: new Date().toISOString(),
|
|
416
|
+
queueMessageId: String(message.id || '') || null,
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
402
421
|
const isAuthError = /authentication|logged in|login|credential|api key/i.test(errorText);
|
|
403
422
|
await publishResponse(message, {
|
|
404
423
|
text: isAuthError
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { displayToolName, isBackgroundLaunch, isSubagentToolName } from './sdk-message-normalizer.mjs';
|
|
3
3
|
import {
|
|
4
|
+
SUBAGENT_TASK_TYPES,
|
|
4
5
|
SUBAGENT_TIMELINE_ENTRY_MAX_CHARS,
|
|
5
6
|
SUBAGENT_TIMELINE_MAX_ENTRIES,
|
|
6
7
|
cutSubagentTimelineText,
|
|
@@ -8,6 +9,8 @@ import {
|
|
|
8
9
|
} from '../../shared/subagent-timeline.mjs';
|
|
9
10
|
|
|
10
11
|
const MAX_TRACKED_RUNS = 200;
|
|
12
|
+
// Task ids and resume ids remembered to find a resumed agent's run.
|
|
13
|
+
const MAX_TRACKED_TASKS = 500;
|
|
11
14
|
|
|
12
15
|
export function toolResultText(content) {
|
|
13
16
|
if (typeof content === 'string') return content;
|
|
@@ -53,6 +56,16 @@ function capText(text) {
|
|
|
53
56
|
*
|
|
54
57
|
* It also answers which turn owns a run's live lanes (`ownsRun`): only the
|
|
55
58
|
* turn that spawned an agent shows its frames in its in-turn lanes.
|
|
59
|
+
*
|
|
60
|
+
* A finished background agent that the session sends a new message
|
|
61
|
+
* (SendMessage) runs again as the same task: task_started names its task id
|
|
62
|
+
* again, with the SendMessage call as its tool_use_id. That id leads to the
|
|
63
|
+
* run of the task's first start (`runIdFor`), so the record goes on under the
|
|
64
|
+
* agent's run, running again, with a note where it resumed. A start the
|
|
65
|
+
* recorder already saw (same run id) adds nothing, and an event of an
|
|
66
|
+
* earlier run (a lower run id) is late and left out. Every post names
|
|
67
|
+
* the run's task id too: a relay that knows the run by the task alone (this
|
|
68
|
+
* recorder did not see the first start) places it there.
|
|
56
69
|
*/
|
|
57
70
|
export function createSubagentTimelineRecorder({
|
|
58
71
|
api,
|
|
@@ -66,6 +79,9 @@ export function createSubagentTimelineRecorder({
|
|
|
66
79
|
writerId = crypto.randomBytes(4).toString('hex'),
|
|
67
80
|
} = {}) {
|
|
68
81
|
const runs = new Map(); // runId -> run state, in registration order
|
|
82
|
+
const taskRuns = new Map(); // taskId -> the run its first start named
|
|
83
|
+
const resumeRuns = new Map(); // tool_use_id of a later start -> that run
|
|
84
|
+
const latestRuns = new Map(); // taskId -> the SDK run id of its latest start
|
|
69
85
|
let flushTimer = null;
|
|
70
86
|
let flushing = null;
|
|
71
87
|
let localCounter = 0;
|
|
@@ -75,6 +91,26 @@ export function createSubagentTimelineRecorder({
|
|
|
75
91
|
return `${writerId}:${localCounter}`;
|
|
76
92
|
}
|
|
77
93
|
|
|
94
|
+
/** The run an id belongs to: a resumed agent's later tool use leads to its first run. */
|
|
95
|
+
function runIdFor(id) {
|
|
96
|
+
const key = String(id || '').trim();
|
|
97
|
+
return resumeRuns.get(key) || key;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// An event of an earlier run of a task than its latest start: late (the
|
|
101
|
+
// run ids of one task sort in the order the runs opened). Without run ids,
|
|
102
|
+
// every event counts.
|
|
103
|
+
function isEarlierRun(taskId, sdkRunId) {
|
|
104
|
+
const latest = taskId ? latestRuns.get(taskId) : '';
|
|
105
|
+
return !!sdkRunId && !!latest && sdkRunId < latest;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function remember(map, key, value) {
|
|
109
|
+
map.delete(key);
|
|
110
|
+
map.set(key, value);
|
|
111
|
+
while (map.size > MAX_TRACKED_TASKS) map.delete(map.keys().next().value);
|
|
112
|
+
}
|
|
113
|
+
|
|
78
114
|
function ensureRun(runId, { parentId = null, displayName = '', subagentType = '' } = {}) {
|
|
79
115
|
const id = String(runId || '').trim();
|
|
80
116
|
if (!id) return null;
|
|
@@ -221,7 +257,7 @@ export function createSubagentTimelineRecorder({
|
|
|
221
257
|
}
|
|
222
258
|
|
|
223
259
|
function observeAssistant(sdkMessage) {
|
|
224
|
-
const parentId =
|
|
260
|
+
const parentId = runIdFor(sdkMessage?.parent_tool_use_id);
|
|
225
261
|
const content = Array.isArray(sdkMessage?.message?.content) ? sdkMessage.message.content : [];
|
|
226
262
|
if (!parentId) {
|
|
227
263
|
// The main thread: only the agents it spawns matter here.
|
|
@@ -261,7 +297,7 @@ export function createSubagentTimelineRecorder({
|
|
|
261
297
|
}
|
|
262
298
|
|
|
263
299
|
function observeStreamEvent(sdkMessage) {
|
|
264
|
-
const parentId =
|
|
300
|
+
const parentId = runIdFor(sdkMessage?.parent_tool_use_id);
|
|
265
301
|
if (!parentId) return;
|
|
266
302
|
const run = ensureRun(parentId);
|
|
267
303
|
const event = sdkMessage?.event || {};
|
|
@@ -309,7 +345,7 @@ export function createSubagentTimelineRecorder({
|
|
|
309
345
|
}
|
|
310
346
|
|
|
311
347
|
function observeUser(sdkMessage) {
|
|
312
|
-
const parentId =
|
|
348
|
+
const parentId = runIdFor(sdkMessage?.parent_tool_use_id);
|
|
313
349
|
const content = Array.isArray(sdkMessage?.message?.content) ? sdkMessage.message.content : [];
|
|
314
350
|
for (const block of content) {
|
|
315
351
|
if (block?.type !== 'tool_result') continue;
|
|
@@ -346,17 +382,46 @@ export function createSubagentTimelineRecorder({
|
|
|
346
382
|
const subtype = String(sdkMessage?.subtype || '');
|
|
347
383
|
const toolUseId = String(sdkMessage?.tool_use_id || '').trim();
|
|
348
384
|
if (!toolUseId) return;
|
|
385
|
+
const taskId = String(sdkMessage?.task_id || '').trim();
|
|
386
|
+
const sdkRunId = String(sdkMessage?.run_id || '').trim();
|
|
349
387
|
if (subtype === 'task_started') {
|
|
350
|
-
|
|
388
|
+
// Only sub-agents are resumed; shells and the rest are not remembered.
|
|
389
|
+
const taskType = String(sdkMessage.task_type || '').trim();
|
|
390
|
+
const agent = !!taskId && (!taskType || SUBAGENT_TASK_TYPES.has(taskType));
|
|
391
|
+
if (agent && sdkRunId) {
|
|
392
|
+
// The same start again, or an earlier run's start arriving late.
|
|
393
|
+
const latest = latestRuns.get(taskId);
|
|
394
|
+
if (latest && sdkRunId <= latest) return;
|
|
395
|
+
remember(latestRuns, taskId, sdkRunId);
|
|
396
|
+
}
|
|
397
|
+
const firstRun = agent ? taskRuns.get(taskId) : '';
|
|
398
|
+
if (firstRun && firstRun !== toolUseId) remember(resumeRuns, toolUseId, firstRun);
|
|
399
|
+
else if (agent && !firstRun) remember(taskRuns, taskId, toolUseId);
|
|
400
|
+
const run = ensureRun(runIdFor(toolUseId), { subagentType: String(sdkMessage.subagent_type || '').trim() });
|
|
401
|
+
// The same task started again (under another tool use, or after it
|
|
402
|
+
// ended): resumed.
|
|
403
|
+
const resumed = !!firstRun && (firstRun !== toolUseId || run.settled);
|
|
404
|
+
run.settled = false;
|
|
351
405
|
if (sdkMessage.is_backgrounded === true) run.background = true;
|
|
406
|
+
if (resumed) {
|
|
407
|
+
const prompt = String(sdkMessage.prompt || '').trim();
|
|
408
|
+
addEntry(run, {
|
|
409
|
+
kind: 'note',
|
|
410
|
+
text: capText(prompt ? `Resumed with a new message: ${prompt}` : 'Resumed with a new message'),
|
|
411
|
+
metadata: { resumed: true },
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
// Running: a start, and for a resumed agent its return from the end.
|
|
352
415
|
patchHeader(run, {
|
|
353
|
-
|
|
416
|
+
status: 'running',
|
|
417
|
+
taskId: taskId || null,
|
|
354
418
|
subagentType: String(sdkMessage.subagent_type || '').trim() || null,
|
|
355
419
|
background: sdkMessage.is_backgrounded === true ? true : null,
|
|
356
420
|
});
|
|
357
421
|
return;
|
|
358
422
|
}
|
|
359
|
-
|
|
423
|
+
if (isEarlierRun(taskId, sdkRunId)) return;
|
|
424
|
+
const run = runs.get(runIdFor(toolUseId));
|
|
360
425
|
if (!run) return;
|
|
361
426
|
if (subtype === 'task_updated') {
|
|
362
427
|
if (sdkMessage?.patch?.is_backgrounded === true) {
|
|
@@ -413,7 +478,7 @@ export function createSubagentTimelineRecorder({
|
|
|
413
478
|
}
|
|
414
479
|
|
|
415
480
|
function ownsRun(runId, owner) {
|
|
416
|
-
const run = runs.get(
|
|
481
|
+
const run = runs.get(runIdFor(runId));
|
|
417
482
|
if (!run || !owner) return false;
|
|
418
483
|
if (!run.owner && run.parentId) run.owner = runs.get(run.parentId)?.owner || null;
|
|
419
484
|
return run.owner === owner;
|
|
@@ -426,7 +491,7 @@ export function createSubagentTimelineRecorder({
|
|
|
426
491
|
* carries it now; an unknown run keeps the old behaviour.
|
|
427
492
|
*/
|
|
428
493
|
function isForeignRun(runId, owner) {
|
|
429
|
-
let run = runs.get(
|
|
494
|
+
let run = runs.get(runIdFor(runId));
|
|
430
495
|
if (!run) return false;
|
|
431
496
|
const spawnOwner = run.owner || null;
|
|
432
497
|
let background = false;
|
|
@@ -469,6 +534,7 @@ export function createSubagentTimelineRecorder({
|
|
|
469
534
|
await api('POST', '/api/subagent-timeline', {
|
|
470
535
|
conversationId,
|
|
471
536
|
subagentRunId: run.id,
|
|
537
|
+
...(run.header.taskId ? { taskId: run.header.taskId } : {}),
|
|
472
538
|
entries,
|
|
473
539
|
...(header ? { header } : {}),
|
|
474
540
|
});
|
|
@@ -566,6 +632,7 @@ export function createSubagentTimelineRecorder({
|
|
|
566
632
|
flush,
|
|
567
633
|
processEnded,
|
|
568
634
|
hasPending,
|
|
635
|
+
runIdFor,
|
|
569
636
|
// For tests and diagnostics.
|
|
570
637
|
runState: (runId) => runs.get(String(runId || '').trim()) || null,
|
|
571
638
|
};
|
|
@@ -13,6 +13,13 @@ import nodePath from 'node:path';
|
|
|
13
13
|
/** Task types whose output is a shell's stdout/stderr (getTaskOutput serves these too). */
|
|
14
14
|
export const SHELL_TASK_TYPES = new Set(['local_bash', 'monitor', 'monitor_mcp', 'monitor_ws']);
|
|
15
15
|
export const MAX_OUTPUT_PATH_CHARS = 1024;
|
|
16
|
+
// The command line a shell row carries. The relay scrubs it, then cuts it
|
|
17
|
+
// to 2,000 characters: a cut here first could split a credential it would
|
|
18
|
+
// no longer recognise, so this one is only a bound on the post.
|
|
19
|
+
export const MAX_TASK_COMMAND_CHARS = 8000;
|
|
20
|
+
// The tools whose call starts a shell task: their input's `command` is the
|
|
21
|
+
// task's command line (the console marks a `| tail` from it).
|
|
22
|
+
const SHELL_TOOL_NAMES = new Set(['Bash', 'Monitor']);
|
|
16
23
|
|
|
17
24
|
const OUTPUT_MARKER = 'Output is being written to';
|
|
18
25
|
// The CLI's own file name: a path with spaces (a Windows profile dir) still
|
|
@@ -89,9 +96,28 @@ function isBackgroundNote(text) {
|
|
|
89
96
|
return Boolean(mcp && taskIdFromOutputFile(mcp[1]));
|
|
90
97
|
}
|
|
91
98
|
|
|
99
|
+
/**
|
|
100
|
+
* `{ toolUseId, command }` of each Bash or Monitor call in an `assistant` SDK
|
|
101
|
+
* message: the command line of the task such a call starts in the
|
|
102
|
+
* background, cut at 8,000 characters (the relay cuts after scrubbing).
|
|
103
|
+
*/
|
|
104
|
+
export function shellCommandsOf(sdkMessage) {
|
|
105
|
+
if (String(sdkMessage?.type || '') !== 'assistant') return [];
|
|
106
|
+
const blocks = Array.isArray(sdkMessage?.message?.content) ? sdkMessage.message.content : [];
|
|
107
|
+
const found = [];
|
|
108
|
+
for (const block of blocks) {
|
|
109
|
+
if (block?.type !== 'tool_use' || !SHELL_TOOL_NAMES.has(String(block.name || ''))) continue;
|
|
110
|
+
const toolUseId = String(block.id || '').trim();
|
|
111
|
+
const command = typeof block.input?.command === 'string' ? block.input.command.trim().slice(0, MAX_TASK_COMMAND_CHARS) : '';
|
|
112
|
+
if (toolUseId && command) found.push({ toolUseId, command });
|
|
113
|
+
}
|
|
114
|
+
return found;
|
|
115
|
+
}
|
|
116
|
+
|
|
92
117
|
/**
|
|
93
118
|
* Every task output file a `user` SDK message announces: the tool_result
|
|
94
|
-
* blocks of backgrounded Bash (and Monitor) calls
|
|
119
|
+
* blocks of backgrounded Bash (and Monitor) calls, with the id of the call
|
|
120
|
+
* (`toolUseId`, '' when the block names none). The structured result's
|
|
95
121
|
* `backgroundTaskId` names the task when the message answers a single call.
|
|
96
122
|
*/
|
|
97
123
|
export function discoverTaskOutputPaths(sdkMessage) {
|
|
@@ -108,7 +134,7 @@ export function discoverTaskOutputPaths(sdkMessage) {
|
|
|
108
134
|
const text = toolResultText(block);
|
|
109
135
|
if (!structuredTaskId && !isBackgroundNote(text)) continue;
|
|
110
136
|
const announced = parseOutputAnnouncement(text, structuredTaskId);
|
|
111
|
-
if (announced) found.push(announced);
|
|
137
|
+
if (announced) found.push({ ...announced, toolUseId: String(block.tool_use_id || '').trim() });
|
|
112
138
|
}
|
|
113
139
|
return found;
|
|
114
140
|
}
|
|
@@ -40,7 +40,7 @@ export const RELAY_ASK_USER_TOOL_PARAMETERS = Object.freeze({
|
|
|
40
40
|
choices: {
|
|
41
41
|
type: 'array',
|
|
42
42
|
items: { type: 'string' },
|
|
43
|
-
description: 'Optional list of choices for a multiple choice question. Prefer providing choices when possible.',
|
|
43
|
+
description: 'Optional list of choices for a multiple choice question. Prefer providing choices when possible. The relay keeps the first 8 choices and drops the rest.',
|
|
44
44
|
},
|
|
45
45
|
multi_select: {
|
|
46
46
|
type: 'boolean',
|
|
@@ -249,9 +249,13 @@ export function createReplayGate() {
|
|
|
249
249
|
* `observe(event)` returns what the event changed, so the caller can pin the
|
|
250
250
|
* runtime's lifecycle and note that a continuation is due without re-parsing.
|
|
251
251
|
*/
|
|
252
|
+
// A shell row's command line: a bound on the post. The relay scrubs it, then
|
|
253
|
+
// cuts it to 2,000 characters.
|
|
254
|
+
const SHELL_COMMAND_CHARS_MAX = 8000;
|
|
255
|
+
|
|
252
256
|
export function createBackgroundShellTracker({ now = () => Date.now() } = {}) {
|
|
253
|
-
const shells = new Map(); // shellId -> { shellId, description, startedAt }
|
|
254
|
-
const toolCalls = new Map(); // toolCallId -> { toolName, shellId, description }
|
|
257
|
+
const shells = new Map(); // shellId -> { shellId, description, command, startedAt }
|
|
258
|
+
const toolCalls = new Map(); // toolCallId -> { toolName, shellId, description, command }
|
|
255
259
|
|
|
256
260
|
function rememberToolCall(toolCallId, entry) {
|
|
257
261
|
if (!toolCallId) return;
|
|
@@ -264,10 +268,10 @@ export function createBackgroundShellTracker({ now = () => Date.now() } = {}) {
|
|
|
264
268
|
}
|
|
265
269
|
}
|
|
266
270
|
|
|
267
|
-
function open(shellId, description) {
|
|
271
|
+
function open(shellId, description, command = '') {
|
|
268
272
|
const id = normalizeShellId(shellId);
|
|
269
273
|
if (!id || shells.has(id)) return null;
|
|
270
|
-
const shell = { shellId: id, description: String(description || '').trim(), startedAt: now() };
|
|
274
|
+
const shell = { shellId: id, description: String(description || '').trim(), command: String(command || '').trim(), startedAt: now() };
|
|
271
275
|
shells.set(id, shell);
|
|
272
276
|
return shell;
|
|
273
277
|
}
|
|
@@ -308,6 +312,8 @@ export function createBackgroundShellTracker({ now = () => Date.now() } = {}) {
|
|
|
308
312
|
toolName,
|
|
309
313
|
shellId: normalizeShellId(args.shellId),
|
|
310
314
|
description: String(args.description || '').trim(),
|
|
315
|
+
// The shell tool's command line, for the row.
|
|
316
|
+
command: typeof args.command === 'string' ? args.command.trim().slice(0, SHELL_COMMAND_CHARS_MAX) : '',
|
|
311
317
|
});
|
|
312
318
|
return { opened, settled, heralded };
|
|
313
319
|
}
|
|
@@ -337,7 +343,7 @@ export function createBackgroundShellTracker({ now = () => Date.now() } = {}) {
|
|
|
337
343
|
|
|
338
344
|
const started = SHELL_OPENED_RE.exec(text);
|
|
339
345
|
if (started && data.success !== false) {
|
|
340
|
-
const shell = open(started[1], call?.description);
|
|
346
|
+
const shell = open(started[1], call?.description, call?.command);
|
|
341
347
|
if (shell) opened.push(shell);
|
|
342
348
|
return { opened, settled, heralded };
|
|
343
349
|
}
|