@oar-sh/oar 0.9.11 → 0.9.12
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 +126 -0
- package/README.md +10 -10
- package/package.json +1 -1
- package/server/README.md +17 -3
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
- package/server/claude-worker/claude-session-process.mjs +128 -0
- package/server/claude-worker/claude-session-worker.mjs +17 -0
- package/server/claude-worker/task-output-discovery.mjs +126 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +159 -3
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +2 -0
- package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
- package/server/copilot-worker/copilot-task-settle.mjs +96 -0
- package/server/db-schema.mjs +9 -0
- package/server/grok-worker/acp-host-services.mjs +42 -3
- package/server/grok-worker/grok-background-tasks.mjs +241 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
- package/server/grok-worker/grok-session-worker.mjs +33 -1
- package/server/grok-worker/grok-turn-runner.mjs +11 -0
- package/server/migrations/0017-conversation-history-indexes.mjs +89 -0
- package/server/public/app/api-client.js +13 -0
- package/server/public/app/background-tasks-view.mjs +285 -16
- package/server/public/app/bootstrap.js +95 -5
- package/server/public/app/claude-background-command-limit-options.mjs +21 -0
- package/server/public/app/conversation-payload-cache.mjs +357 -0
- package/server/public/app/conversation-turn-state.mjs +1 -1
- package/server/public/app/conversation-view.js +433 -3
- package/server/public/app/conversation-window-retention.mjs +96 -0
- package/server/public/app/journal-view.js +252 -66
- package/server/public/app/relay-prompt-sanitizer.mjs +11 -4
- package/server/public/app/settings-modal.js +110 -2
- package/server/public/app/socket-handlers.js +59 -7
- package/server/public/app/store.js +25 -0
- package/server/public/app/subagent-viewer.mjs +7 -1
- package/server/public/app/task-console-render.mjs +237 -0
- package/server/public/app/task-console-watch.mjs +42 -0
- package/server/public/app/task-console.mjs +630 -0
- package/server/public/app/task-output-text.mjs +234 -0
- package/server/public/app/transcript-island.mjs +73 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +38 -8
- package/server/public/app/transcript-jump-bottom.mjs +48 -11
- package/server/public/app/transcript-reading-anchor.mjs +256 -0
- package/server/public/app/transcript-reload-freshness.mjs +17 -1
- package/server/public/app/transcript-viewport-anchor.mjs +20 -1
- package/server/public/index.html +239 -6
- package/server/relay-tools.md +1 -0
- package/server/repositories/message-repository.mjs +8 -0
- package/server/repositories/question-repository.mjs +4 -0
- package/server/routes/messages-routes.mjs +249 -3
- package/server/routes/sessions-routes.mjs +133 -63
- package/server/server-runtime.mjs +77 -0
- package/server/services/background-task-store.mjs +240 -3
- package/server/services/cli-process-runner.mjs +4 -21
- package/server/services/conversation-history-page.mjs +199 -0
- package/server/services/relay-prompt-sanitizer.mjs +11 -4
- package/server/services/remote-relay-dispatcher.mjs +130 -2
- package/server/services/session-activity-tracker.mjs +6 -2
- package/server/services/task-output-rooms.mjs +259 -0
- package/server/services/task-output-service.mjs +562 -0
- package/shared/claude-background-command-limit.mjs +83 -0
- package/shared/remote-relay-contract.mjs +32 -2
- package/shared/task-output-text.mjs +737 -0
- package/shared/worker-runtime/task-output-spool.mjs +181 -0
|
@@ -174,6 +174,10 @@ export function createQuestionRepository(db) {
|
|
|
174
174
|
WHERE response_message_id = ?
|
|
175
175
|
ORDER BY created_at ASC, id ASC
|
|
176
176
|
`),
|
|
177
|
+
// Every card of a conversation, for the shared view's link redaction:
|
|
178
|
+
// a paged transcript hydrates only its page's cards, yet a reply on
|
|
179
|
+
// the page may quote the link of a card outside it.
|
|
180
|
+
listPreviewCardJsonByConversation: db.prepare(`SELECT preview_json FROM preview_cards WHERE conversation_id = ?`),
|
|
177
181
|
deleteConvPreviewCards: db.prepare(`DELETE FROM preview_cards WHERE conversation_id = ?`),
|
|
178
182
|
|
|
179
183
|
// relay boards
|
|
@@ -42,6 +42,14 @@ import { DEFAULT_CLAUDE_REASONING_EFFORTS } from '../services/provider-reasoning
|
|
|
42
42
|
import { claudeBaseModelId, claudeLongContextModelId } from '../../shared/model-id.mjs';
|
|
43
43
|
import { sanitizeSubagentRunId } from '../../shared/subagent-run-id.mjs';
|
|
44
44
|
import { normalizeBackgroundTaskDigest } from '../../shared/worker-runtime/background-task-digest.mjs';
|
|
45
|
+
import {
|
|
46
|
+
DEFAULT_TASK_STALL_MINUTES,
|
|
47
|
+
TASK_OUTPUT_LIMITS,
|
|
48
|
+
normalizeTaskOutputKind,
|
|
49
|
+
normalizeTaskOutputPath,
|
|
50
|
+
} from '../../shared/task-output-text.mjs';
|
|
51
|
+
import { readBefore, readTail, shapeSnapshot, taskOutputSource } from '../services/task-output-service.mjs';
|
|
52
|
+
import { taskKindOf } from '../services/session-activity-tracker.mjs';
|
|
45
53
|
import { ANSWERED_ELSEWHERE_KIND, STEER_FOLDED_TEXT } from '../../shared/steer-settle-markers.mjs';
|
|
46
54
|
import { isLiveResend } from '../services/steer-resend.mjs';
|
|
47
55
|
import { resolveProviderModelSelection } from '../services/provider-model-selection.mjs';
|
|
@@ -1779,6 +1787,12 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
1779
1787
|
buildRelayReadyBannerData,
|
|
1780
1788
|
backgroundTaskStore,
|
|
1781
1789
|
backgroundWorkView = null,
|
|
1790
|
+
// The 8 KiB a worker posts for a task without an output file
|
|
1791
|
+
// (services/task-output-service.mjs createTaskOutputSnapshots).
|
|
1792
|
+
taskOutputSnapshots = null,
|
|
1793
|
+
// Has the health poller read soon (a task with a console ended).
|
|
1794
|
+
pokeTaskHealth = null,
|
|
1795
|
+
getTaskStallMinutes = () => DEFAULT_TASK_STALL_MINUTES,
|
|
1782
1796
|
sendWorkerControl,
|
|
1783
1797
|
ensureSessionId,
|
|
1784
1798
|
touchCli,
|
|
@@ -5468,6 +5482,7 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5468
5482
|
io.emit('background_tasks', {
|
|
5469
5483
|
conversationId: requesterSessionId,
|
|
5470
5484
|
tasks: backgroundWorkView.panelTasks(requesterSessionId, requesterConversationId),
|
|
5485
|
+
endedTasks: backgroundTaskStore?.ended?.(requesterSessionId) || [],
|
|
5471
5486
|
});
|
|
5472
5487
|
}
|
|
5473
5488
|
}
|
|
@@ -7629,6 +7644,34 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7629
7644
|
res.json({ ok: true, messageId, conversationId, attemptId });
|
|
7630
7645
|
});
|
|
7631
7646
|
|
|
7647
|
+
// An integer from a worker's post, or undefined (dropped from the row).
|
|
7648
|
+
function wholeNumber(value, { min = Number.MIN_SAFE_INTEGER } = {}) {
|
|
7649
|
+
if (value === null || value === undefined || value === '' || typeof value === 'boolean') return undefined;
|
|
7650
|
+
const number = Number(value);
|
|
7651
|
+
return Number.isSafeInteger(number) && number >= min ? number : undefined;
|
|
7652
|
+
}
|
|
7653
|
+
|
|
7654
|
+
// A task row's console fields as the publish route keeps them.
|
|
7655
|
+
function taskOutputFields(task) {
|
|
7656
|
+
const outputPath = normalizeTaskOutputPath(task?.outputPath);
|
|
7657
|
+
const requestedKind = String(task?.outputKind || '').trim();
|
|
7658
|
+
if (!outputPath && !requestedKind) return {};
|
|
7659
|
+
const outputKind = normalizeTaskOutputKind(requestedKind, outputPath);
|
|
7660
|
+
// A file kind without a readable path has nothing to show.
|
|
7661
|
+
if ((outputKind === 'file' || outputKind === 'spool') && !outputPath) return { outputKind: 'none' };
|
|
7662
|
+
return { ...(outputPath ? { outputPath } : {}), outputKind };
|
|
7663
|
+
}
|
|
7664
|
+
|
|
7665
|
+
// `background_tasks` with the health readings on the live rows and the
|
|
7666
|
+
// ended rows beside them, for the key the worker posts under.
|
|
7667
|
+
function emitBackgroundTasks(key, tasks = backgroundTaskStore?.get?.(key) || []) {
|
|
7668
|
+
io.emit('background_tasks', {
|
|
7669
|
+
conversationId: key,
|
|
7670
|
+
tasks: backgroundTaskStore?.withHealth ? backgroundTaskStore.withHealth(tasks, key) : tasks,
|
|
7671
|
+
endedTasks: backgroundTaskStore?.ended?.(key) || [],
|
|
7672
|
+
});
|
|
7673
|
+
}
|
|
7674
|
+
|
|
7632
7675
|
function resolveTaskConversationId(id) {
|
|
7633
7676
|
const key = String(id || '').trim();
|
|
7634
7677
|
if (!key) return '';
|
|
@@ -7648,14 +7691,34 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7648
7691
|
const conversationId = resolveTaskConversationId(req.body?.conversationId);
|
|
7649
7692
|
const taskId = String(req.body?.taskId || '').trim();
|
|
7650
7693
|
if (!conversationId || !taskId) return res.status(400).json({ error: 'Missing conversationId or taskId' });
|
|
7651
|
-
|
|
7694
|
+
// Only a number the worker sent: a missing or null exit code stays null
|
|
7695
|
+
// (Number(null) would read as a clean exit).
|
|
7696
|
+
const exitCode = wholeNumber(req.body?.exitCode);
|
|
7652
7697
|
const settledDescription = String(req.body?.description || '').trim().slice(0, 500);
|
|
7698
|
+
// The store is keyed as the worker posts. A task with a console stays
|
|
7699
|
+
// viewable as an ended row; one that ended while this runtime was away
|
|
7700
|
+
// becomes one when the post names its output.
|
|
7701
|
+
const settledOutputPath = normalizeTaskOutputPath(req.body?.outputPath);
|
|
7702
|
+
const endedRow = backgroundTaskStore?.settle?.(String(req.body?.conversationId || '').trim(), taskId, {
|
|
7703
|
+
status: req.body?.status,
|
|
7704
|
+
exitCode: exitCode ?? null,
|
|
7705
|
+
steps: wholeNumber(req.body?.steps, { min: 0 }),
|
|
7706
|
+
outputPath: settledOutputPath || undefined,
|
|
7707
|
+
outputKind: settledOutputPath ? normalizeTaskOutputKind(req.body?.outputKind, settledOutputPath) : undefined,
|
|
7708
|
+
description: settledDescription,
|
|
7709
|
+
taskType: String(req.body?.taskType || '').trim().slice(0, 80),
|
|
7710
|
+
startedAt: Number(req.body?.startedAt) || null,
|
|
7711
|
+
}) || null;
|
|
7712
|
+
if (endedRow) {
|
|
7713
|
+
emitBackgroundTasks(String(req.body?.conversationId || '').trim());
|
|
7714
|
+
pokeTaskHealth?.();
|
|
7715
|
+
}
|
|
7653
7716
|
sessionActivityTracker?.noteTaskSettled?.(conversationId, {
|
|
7654
7717
|
taskId,
|
|
7655
7718
|
toolUseId: sanitizeSubagentRunId(req.body?.toolUseId) || null,
|
|
7656
7719
|
status: String(req.body?.status || '').trim().slice(0, 40) || 'completed',
|
|
7657
7720
|
summary: String(req.body?.summary || '').trim().slice(0, 500) || null,
|
|
7658
|
-
exitCode:
|
|
7721
|
+
exitCode: exitCode ?? null,
|
|
7659
7722
|
// What the task was, from a worker that sends it: the end of a task
|
|
7660
7723
|
// this runtime never knew (it ended while the relay was away) is
|
|
7661
7724
|
// still an event then.
|
|
@@ -7702,8 +7765,23 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7702
7765
|
// The spawning tool_use id: the sub-agent run this task is. The panel
|
|
7703
7766
|
// opens the run's viewer from it.
|
|
7704
7767
|
toolUseId: sanitizeSubagentRunId(task?.toolUseId) || undefined,
|
|
7768
|
+
// The console (shell and Monitor tasks): the file holding the
|
|
7769
|
+
// output, how it is obtained ('none': the provider has none to give).
|
|
7770
|
+
...taskOutputFields(task),
|
|
7771
|
+
// Tool calls a sub-agent made so far.
|
|
7772
|
+
steps: wholeNumber(task?.steps, { min: 0 }),
|
|
7773
|
+
exitCode: wholeNumber(task?.exitCode),
|
|
7705
7774
|
}))
|
|
7706
7775
|
.filter((task) => task.taskId);
|
|
7776
|
+
// The first path a task names wins: a later post cannot point a live
|
|
7777
|
+
// task's console at another file.
|
|
7778
|
+
for (const task of tasks) {
|
|
7779
|
+
const held = (backgroundTaskStore?.get?.(conversationId) || []).find((entry) => entry?.taskId === task.taskId);
|
|
7780
|
+
if (held?.outputPath && task.outputPath !== held.outputPath) {
|
|
7781
|
+
task.outputPath = held.outputPath;
|
|
7782
|
+
task.outputKind = held.outputKind;
|
|
7783
|
+
}
|
|
7784
|
+
}
|
|
7707
7785
|
const resync = req.body?.resync === true;
|
|
7708
7786
|
// Posts can overtake each other on the way (an ordinary publish and the
|
|
7709
7787
|
// answer to a resync, two publishes): a worker numbers its sets
|
|
@@ -7720,9 +7798,15 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7720
7798
|
console.log(`[${typeof ts === 'function' ? ts() : new Date().toISOString()}] BG-TASKS set dropped (${setOrder.reason}) session=${conversationId.slice(0, 8)} epoch=${Number(req.body?.order?.epoch) || 0} seq=${Number(req.body?.order?.seq) || 0} tasks=${tasks.length}`);
|
|
7721
7799
|
return res.json({ ok: true, count: tasks.length, stale: true, reason: setOrder.reason });
|
|
7722
7800
|
}
|
|
7801
|
+
// A task with a console that leaves the set ended: its file is read once
|
|
7802
|
+
// more soon (its last output often lands after the poller's last look).
|
|
7803
|
+
const nextIds = new Set(tasks.map((task) => task.taskId));
|
|
7804
|
+
const endedWithConsole = (backgroundTaskStore?.get?.(conversationId) || [])
|
|
7805
|
+
.some((task) => task?.outputPath && !nextIds.has(task.taskId));
|
|
7723
7806
|
backgroundTaskStore?.replace?.(conversationId, tasks);
|
|
7724
7807
|
backgroundTaskStore?.announce?.(conversationId, false);
|
|
7725
|
-
|
|
7808
|
+
if (endedWithConsole) pokeTaskHealth?.();
|
|
7809
|
+
emitBackgroundTasks(conversationId, tasks);
|
|
7726
7810
|
// The store is keyed as the worker posts (its SDK session id); the
|
|
7727
7811
|
// tracker by the conversation.
|
|
7728
7812
|
try {
|
|
@@ -7769,6 +7853,168 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
7769
7853
|
res.json({ ok: true });
|
|
7770
7854
|
});
|
|
7771
7855
|
|
|
7856
|
+
// ─── Task console ──────────────────────────────────────────────────────────
|
|
7857
|
+
|
|
7858
|
+
// The task a conversation's console asks for: in the live or the ended set
|
|
7859
|
+
// of the conversation, under its own id or its worker's session id (the
|
|
7860
|
+
// key the worker posts under), as the stop route resolves it.
|
|
7861
|
+
function findConversationTask(conversationId, taskId) {
|
|
7862
|
+
const conversation = stmts.getConvAnyStatus?.get?.(conversationId) || null;
|
|
7863
|
+
if (!conversation) return { conversation: null, found: null, sessionId: '' };
|
|
7864
|
+
const sessionId = String(conversation.sdk_session_id || conversationId);
|
|
7865
|
+
const found = backgroundTaskStore?.findTask?.([conversationId, sessionId], taskId) || null;
|
|
7866
|
+
return { conversation, found, sessionId };
|
|
7867
|
+
}
|
|
7868
|
+
|
|
7869
|
+
// The providers whose workers name a shell task's output (a file, a spool
|
|
7870
|
+
// or a snapshot). A task of theirs without one comes from a worker that
|
|
7871
|
+
// started before the console existed.
|
|
7872
|
+
const TASK_OUTPUT_PROVIDERS = new Set(['claude', 'github', 'copilot', 'grok']);
|
|
7873
|
+
const SUBAGENT_OUTPUT_MESSAGE = 'Sub-agents have no shell output; open the sub-agent view.';
|
|
7874
|
+
|
|
7875
|
+
// Why a task has no output to show, for the console's `none` answer.
|
|
7876
|
+
function noOutputMessage(conversationId, task) {
|
|
7877
|
+
if (taskKindOf(task?.taskType) === 'subagent') return SUBAGENT_OUTPUT_MESSAGE;
|
|
7878
|
+
let provider = '';
|
|
7879
|
+
try {
|
|
7880
|
+
provider = String(stmts.getRuntimeSessionByConversation?.get?.(conversationId)?.provider_type || '').trim().toLowerCase();
|
|
7881
|
+
} catch {}
|
|
7882
|
+
if (!provider || TASK_OUTPUT_PROVIDERS.has(provider)) {
|
|
7883
|
+
return 'This session did not report an output file for this task. Sessions started before the task console need a restart to show output.';
|
|
7884
|
+
}
|
|
7885
|
+
return 'Output is not available for this provider.';
|
|
7886
|
+
}
|
|
7887
|
+
|
|
7888
|
+
// A task the session's task list shows that the task store does not hold:
|
|
7889
|
+
// a sub-agent (live or ended, its run is its record) or a task that ended
|
|
7890
|
+
// without a console. The activity tracker knows it by its task or run id.
|
|
7891
|
+
function trackedTask(conversationId, taskId) {
|
|
7892
|
+
let view = null;
|
|
7893
|
+
try { view = sessionActivityTracker?.describe?.(conversationId) || null; } catch {}
|
|
7894
|
+
const tasks = Array.isArray(view?.tasks) ? view.tasks : [];
|
|
7895
|
+
const found = tasks.find((task) => !task?.placeholder && (String(task?.id || '') === taskId || String(task?.runId || '') === taskId));
|
|
7896
|
+
if (found) return found;
|
|
7897
|
+
const run = stmts.getSubagentRun?.get?.(taskId) || null;
|
|
7898
|
+
if (run && String(run.conversation_id || '') === conversationId) {
|
|
7899
|
+
return { id: taskId, kind: 'subagent', type: run.subagent_type || null, description: run.display_name || '', status: run.status || null };
|
|
7900
|
+
}
|
|
7901
|
+
return null;
|
|
7902
|
+
}
|
|
7903
|
+
|
|
7904
|
+
function taskStallMinutes() {
|
|
7905
|
+
try {
|
|
7906
|
+
return getTaskStallMinutes();
|
|
7907
|
+
} catch {
|
|
7908
|
+
return DEFAULT_TASK_STALL_MINUTES;
|
|
7909
|
+
}
|
|
7910
|
+
}
|
|
7911
|
+
|
|
7912
|
+
// GET /api/conversation/:conversationId/background-task/:taskId/output —
|
|
7913
|
+
// the console's read of a shell or Monitor task's output. Without
|
|
7914
|
+
// `before`: the last `lines` lines (default 2,000, at most 5,000) within
|
|
7915
|
+
// 256 KB; with `before` (a startOffset of an earlier answer): the lines
|
|
7916
|
+
// ending there. Everything is cleaned (controls out, colours kept,
|
|
7917
|
+
// credentials scrubbed) for every reader. Another relay's agent reads it
|
|
7918
|
+
// too (remote_relay read_task_output, read permission): no inbound switch,
|
|
7919
|
+
// nothing it reads changes anything.
|
|
7920
|
+
app.get('/api/conversation/:conversationId/background-task/:taskId/output', auth, async (req, res) => {
|
|
7921
|
+
const conversationId = String(req.params.conversationId || '').trim();
|
|
7922
|
+
const taskId = String(req.params.taskId || '').trim();
|
|
7923
|
+
if (!conversationId || !taskId) return res.status(400).json({ error: 'Missing conversationId or taskId' });
|
|
7924
|
+
const { conversation, found, sessionId } = findConversationTask(conversationId, taskId);
|
|
7925
|
+
if (!conversation) return res.status(404).json({ error: 'Conversation not found' });
|
|
7926
|
+
if (!found) {
|
|
7927
|
+
const tracked = trackedTask(conversationId, taskId);
|
|
7928
|
+
if (!tracked) return res.status(404).json({ error: 'Task not found', code: 'task-not-found' });
|
|
7929
|
+
const running = String(tracked.status || 'running') === 'running';
|
|
7930
|
+
const subagent = tracked.kind !== 'shell';
|
|
7931
|
+
return res.json({
|
|
7932
|
+
ok: true,
|
|
7933
|
+
taskId,
|
|
7934
|
+
kind: 'none',
|
|
7935
|
+
ended: !running,
|
|
7936
|
+
status: running ? 'running' : String(tracked.status || 'unknown'),
|
|
7937
|
+
exitCode: running ? null : (Number.isInteger(tracked.exitCode) ? tracked.exitCode : null),
|
|
7938
|
+
health: null,
|
|
7939
|
+
taskStallMinutes: taskStallMinutes(),
|
|
7940
|
+
task: {
|
|
7941
|
+
taskId,
|
|
7942
|
+
taskType: tracked.type || (subagent ? 'local_agent' : 'local_bash'),
|
|
7943
|
+
description: String(tracked.description || ''),
|
|
7944
|
+
startedAt: Date.parse(tracked.startedAt) || null,
|
|
7945
|
+
endedAt: Date.parse(tracked.endedAt) || null,
|
|
7946
|
+
stoppable: false,
|
|
7947
|
+
},
|
|
7948
|
+
message: subagent ? SUBAGENT_OUTPUT_MESSAGE : noOutputMessage(conversationId, { taskType: 'local_bash' }),
|
|
7949
|
+
});
|
|
7950
|
+
}
|
|
7951
|
+
const { task, ended, key } = found;
|
|
7952
|
+
const { kind, path } = taskOutputSource(task);
|
|
7953
|
+
const head = {
|
|
7954
|
+
ok: true,
|
|
7955
|
+
taskId,
|
|
7956
|
+
kind,
|
|
7957
|
+
ended,
|
|
7958
|
+
status: ended ? task.status : 'running',
|
|
7959
|
+
exitCode: ended ? (task.exitCode ?? null) : null,
|
|
7960
|
+
health: task.health || null,
|
|
7961
|
+
taskStallMinutes: taskStallMinutes(),
|
|
7962
|
+
task: {
|
|
7963
|
+
taskId,
|
|
7964
|
+
taskType: task.taskType || null,
|
|
7965
|
+
description: task.description || '',
|
|
7966
|
+
startedAt: task.startedAt || null,
|
|
7967
|
+
endedAt: task.endedAt || null,
|
|
7968
|
+
stoppable: !ended && task.stoppable !== false,
|
|
7969
|
+
},
|
|
7970
|
+
};
|
|
7971
|
+
if (kind === 'none') {
|
|
7972
|
+
return res.json({ ...head, message: noOutputMessage(conversationId, task) });
|
|
7973
|
+
}
|
|
7974
|
+
if (kind === 'snapshot') {
|
|
7975
|
+
// The worker answers a request with a post of the task's last 8 KiB;
|
|
7976
|
+
// the console asks again every few seconds and gets the newest copy.
|
|
7977
|
+
const cached = taskOutputSnapshots?.get?.([key, sessionId, conversationId], taskId) || null;
|
|
7978
|
+
if (!ended) {
|
|
7979
|
+
const sent = sendWorkerControl?.(sessionId, { type: 'task_output_snapshot', taskId });
|
|
7980
|
+
if (!sent && !cached) return res.status(409).json({ error: 'Session worker is not connected', code: 'worker-not-connected' });
|
|
7981
|
+
}
|
|
7982
|
+
if (!cached) return res.json({ ...head, pending: true, text: '', lines: [], startOffset: 0, endOffset: 0, size: 0, mtime: null, hasMoreBefore: false });
|
|
7983
|
+
return res.json({ ...head, ...shapeSnapshot(cached) });
|
|
7984
|
+
}
|
|
7985
|
+
if (!path) return res.json({ ...head, kind: 'none', message: 'Output is not available for this task.' });
|
|
7986
|
+
const lines = req.query?.lines;
|
|
7987
|
+
const before = req.query?.before;
|
|
7988
|
+
const read = before !== undefined && before !== ''
|
|
7989
|
+
? await readBefore(path, { before, lines })
|
|
7990
|
+
: await readTail(path, { lines });
|
|
7991
|
+
if (!read.ok) {
|
|
7992
|
+
// No file yet (a spool is named before its first byte) reads as no
|
|
7993
|
+
// output yet; one the relay does not read (a symlink, a folder) is
|
|
7994
|
+
// said so instead of failing.
|
|
7995
|
+
const empty = { text: '', lines: [], startOffset: 0, endOffset: 0, size: 0, mtime: null, hasMoreBefore: false };
|
|
7996
|
+
return res.json({ ...head, ...empty, ...(read.code === 'missing' ? {} : { unavailable: read.code }) });
|
|
7997
|
+
}
|
|
7998
|
+
return res.json({ ...head, ...read });
|
|
7999
|
+
});
|
|
8000
|
+
|
|
8001
|
+
// POST /api/background-tasks/:taskId/output-snapshot — a worker's answer
|
|
8002
|
+
// to `task_output_snapshot`: the task's latest output where it has no
|
|
8003
|
+
// file (Claude's getTaskOutput, its last 8 KiB).
|
|
8004
|
+
app.post('/api/background-tasks/:taskId/output-snapshot', auth, (req, res) => {
|
|
8005
|
+
touchCli();
|
|
8006
|
+
const key = String(req.body?.conversationId || '').trim();
|
|
8007
|
+
const taskId = String(req.params.taskId || '').trim();
|
|
8008
|
+
if (!key || !taskId) return res.status(400).json({ error: 'Missing conversationId or taskId' });
|
|
8009
|
+
if (typeof req.body?.output !== 'string') return res.status(400).json({ error: 'output must be a string' });
|
|
8010
|
+
taskOutputSnapshots?.set?.(key, taskId, {
|
|
8011
|
+
output: req.body.output.slice(0, TASK_OUTPUT_LIMITS.chunkBytes),
|
|
8012
|
+
totalBytes: req.body?.totalBytes,
|
|
8013
|
+
truncated: req.body?.truncated === true,
|
|
8014
|
+
});
|
|
8015
|
+
res.json({ ok: true });
|
|
8016
|
+
});
|
|
8017
|
+
|
|
7772
8018
|
// POST /api/activity — relay sends in-flight activity updates (tool/search sections)
|
|
7773
8019
|
// Optional attempt fence for the live lanes (stream/thought/activity/
|
|
7774
8020
|
// subagent): a writer that names its processing attempt is rejected once
|