@oar-sh/oar 0.9.8 → 0.9.10
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 +478 -0
- package/README.md +129 -39
- package/bin/bat/oar.bat +46 -46
- package/bin/oar.js +814 -228
- package/package.json +5 -6
- package/server/README.md +306 -4
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +35 -1
- package/server/claude-worker/claude-turn-publisher.mjs +3 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
- package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +3 -1
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +44 -0
- package/server/migrations/0008-message-pins.mjs +85 -0
- package/server/migrations/0009-subagent-timeline.mjs +135 -0
- package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
- package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
- package/server/public/app/agent-exchange-fold-view.mjs +248 -0
- package/server/public/app/api-client.js +91 -0
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +3 -0
- package/server/public/app/attachments-view.js +1024 -131
- package/server/public/app/back-guard.mjs +58 -0
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +230 -35
- package/server/public/app/chat-actions-menu-state.mjs +116 -0
- package/server/public/app/claude-cloud-conversation-ui.js +10 -22
- package/server/public/app/composer-control-state.mjs +16 -0
- package/server/public/app/composer-keyboard.mjs +54 -0
- package/server/public/app/conversation-view.js +471 -110
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/external-link-policy.mjs +4 -1
- package/server/public/app/file-version.mjs +58 -0
- package/server/public/app/host-suspend-ui.js +6 -3
- package/server/public/app/journal-view.js +161 -27
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/pinned-messages-view.mjs +253 -0
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/pwa-install.js +1 -1
- package/server/public/app/relay-board-view.js +3 -0
- package/server/public/app/relay-dot-state.mjs +8 -1
- package/server/public/app/relay-event-subscriptions-ui.js +184 -0
- package/server/public/app/relay-event-view.mjs +234 -0
- package/server/public/app/router.js +8 -2
- package/server/public/app/settings-modal.js +53 -1
- package/server/public/app/socket-handlers.js +65 -28
- package/server/public/app/store.js +25 -2
- package/server/public/app/subagent-row.mjs +138 -0
- package/server/public/app/subagent-timeline-view.mjs +186 -0
- package/server/public/app/subagent-viewer-watch.mjs +50 -0
- package/server/public/app/subagent-viewer.mjs +534 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/thinking-follow.js +4 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +290 -0
- package/server/public/app/transcript-jump-bottom.mjs +99 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-chat-gallery.mjs +59 -0
- package/server/public/app/viewer-controls-state.mjs +59 -0
- package/server/public/app/viewer-gallery.mjs +46 -0
- package/server/public/app/viewer-gestures.mjs +105 -0
- package/server/public/app/viewer-history.mjs +45 -0
- package/server/public/app/viewer-tap.mjs +112 -0
- package/server/public/index.html +700 -59
- package/server/repositories/message-pin-statements.mjs +39 -0
- package/server/repositories/message-repository.mjs +34 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- package/server/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/messages-routes.mjs +548 -111
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +164 -24
- package/server/server-runtime.mjs +274 -62
- package/server/services/cloudflared-tunnel-service.mjs +139 -41
- package/server/services/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -0
- package/server/services/file-serving.mjs +155 -0
- package/server/services/message-pin-list.mjs +52 -0
- package/server/services/oar-cli-helpers.mjs +139 -4
- package/server/services/oar-cli-menu.mjs +240 -0
- package/server/services/oar-cli-wsl.mjs +129 -0
- package/server/services/preview-proxy-server.mjs +41 -23
- package/server/services/push-dispatch-service.mjs +18 -0
- package/server/services/relay-event-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1935 -0
- package/server/services/relay-shutdown-gate.mjs +31 -0
- package/server/services/remote-relay-caller-context.mjs +52 -7
- package/server/services/remote-relay-dispatcher.mjs +494 -2
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/safe-served-content.mjs +61 -11
- package/server/services/session-activity-tracker.mjs +487 -0
- package/server/services/session-history-refresh-service.mjs +31 -0
- package/server/services/stale-attempt-log-limiter.mjs +96 -0
- package/server/services/stale-recovery-clock-guard.mjs +57 -0
- package/server/services/stale-subagent-run-sweep.mjs +109 -0
- package/server/services/subagent-record-retention.mjs +164 -0
- package/server/services/subagent-timeline-service.mjs +328 -0
- package/server/services/subagent-viewer-rooms.mjs +119 -0
- package/server/services/upgrade-socket-guard.mjs +16 -0
- package/shared/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +157 -5
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Pinned messages: the pinned_at column detection and its SQL in one place.
|
|
4
|
+
// The statements are null on a database the 0008 migration has not reached
|
|
5
|
+
// yet; every caller treats that as "no pins".
|
|
6
|
+
export function createMessagePinStatements(db) {
|
|
7
|
+
const messageColumns = new Set(
|
|
8
|
+
db.prepare(`PRAGMA table_info(messages)`).all().map((column) => String(column?.name || '').trim()),
|
|
9
|
+
);
|
|
10
|
+
const messagesSupportPins = messageColumns.has('pinned_at');
|
|
11
|
+
if (!messagesSupportPins) {
|
|
12
|
+
return {
|
|
13
|
+
messagesSupportPins,
|
|
14
|
+
setMessagePinnedAt: null,
|
|
15
|
+
listPinnedMessages: null,
|
|
16
|
+
countPinnedMessages: null,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
messagesSupportPins,
|
|
21
|
+
setMessagePinnedAt: db.prepare(`
|
|
22
|
+
UPDATE messages
|
|
23
|
+
SET pinned_at = ?
|
|
24
|
+
WHERE id = ? AND conversation_id = ?
|
|
25
|
+
`),
|
|
26
|
+
// Conversation order, not pin order: the list reads like a table of contents.
|
|
27
|
+
listPinnedMessages: db.prepare(`
|
|
28
|
+
SELECT id, role, text, mode, attachments, timestamp, pinned_at, hidden_from_shares
|
|
29
|
+
FROM messages
|
|
30
|
+
WHERE conversation_id = ? AND pinned_at IS NOT NULL
|
|
31
|
+
ORDER BY timestamp ASC
|
|
32
|
+
`),
|
|
33
|
+
countPinnedMessages: db.prepare(`
|
|
34
|
+
SELECT COUNT(*) AS cnt
|
|
35
|
+
FROM messages
|
|
36
|
+
WHERE conversation_id = ? AND pinned_at IS NOT NULL
|
|
37
|
+
`),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
import { createShareVisibilityStatements } from './share-visibility-statements.mjs';
|
|
4
|
+
import { createMessagePinStatements } from './message-pin-statements.mjs';
|
|
4
5
|
import { sessionWorkerProviderSqlList } from '../../shared/provider-routing.mjs';
|
|
5
6
|
|
|
6
7
|
export function createMessageRepository(db) {
|
|
7
8
|
const shareVisibility = createShareVisibilityStatements(db);
|
|
9
|
+
const messagePins = createMessagePinStatements(db);
|
|
8
10
|
const queueHasImageOperationId = db.prepare(`PRAGMA table_info(queue)`).all()
|
|
9
11
|
.some((column) => column.name === 'image_operation_id');
|
|
10
12
|
// A turn paused at the Claude usage limit (usage-limit-pause-service): a
|
|
@@ -27,6 +29,9 @@ export function createMessageRepository(db) {
|
|
|
27
29
|
getSharedMessages: shareVisibility.getSharedMessages,
|
|
28
30
|
getMessageByConversation: db.prepare(`SELECT * FROM messages WHERE id = ? AND conversation_id = ? LIMIT 1`),
|
|
29
31
|
setMessageShareVisibility: shareVisibility.setMessageShareVisibility,
|
|
32
|
+
setMessagePinnedAt: messagePins.setMessagePinnedAt,
|
|
33
|
+
listPinnedMessages: messagePins.listPinnedMessages,
|
|
34
|
+
countPinnedMessages: messagePins.countPinnedMessages,
|
|
30
35
|
getLatestConversationModel: db.prepare(`SELECT model FROM messages WHERE conversation_id = ? AND model IS NOT NULL AND model != '' ORDER BY timestamp DESC LIMIT 1`),
|
|
31
36
|
getRecentMessagesDesc: db.prepare(`SELECT id, role, text, timestamp FROM messages WHERE conversation_id = ? ORDER BY timestamp DESC LIMIT ?`),
|
|
32
37
|
insertMsg: db.prepare(`INSERT INTO messages (id, conversation_id, role, text, model, mode, attachments, timestamp, model_requested, model_actual, model_origin) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
|
|
@@ -341,6 +346,12 @@ export function createMessageRepository(db) {
|
|
|
341
346
|
//
|
|
342
347
|
// A turn blocked on an unanswered AskUserQuestion is never stale — it is
|
|
343
348
|
// waiting on the human, and relay_questions carries its own expiry.
|
|
349
|
+
//
|
|
350
|
+
// The ceiling counts from the last human interaction with the turn, not
|
|
351
|
+
// from its start: answering a question restarts the clock. Otherwise an
|
|
352
|
+
// interview that waited hours for its answers would be cut off by the
|
|
353
|
+
// ceiling the moment the last answer arrived (live 2026-10-10: a
|
|
354
|
+
// five-question interview lost its reply that way).
|
|
344
355
|
listRecoverableProcessing: db.prepare(`
|
|
345
356
|
SELECT id, conversation_id, kind, relay_mode, model, retry_count, consumed_at
|
|
346
357
|
FROM queue
|
|
@@ -348,7 +359,13 @@ export function createMessageRepository(db) {
|
|
|
348
359
|
AND COALESCE(kind, '') != 'continuation'
|
|
349
360
|
AND (
|
|
350
361
|
COALESCE(owner_last_claimed_at, processing_at, timestamp) < @inactiveBefore
|
|
351
|
-
OR (
|
|
362
|
+
OR (
|
|
363
|
+
@ceilingBefore IS NOT NULL
|
|
364
|
+
AND MAX(
|
|
365
|
+
COALESCE(processing_at, timestamp),
|
|
366
|
+
COALESCE((SELECT MAX(answered_at) FROM relay_questions WHERE queue_id = queue.id), '')
|
|
367
|
+
) < @ceilingBefore
|
|
368
|
+
)
|
|
352
369
|
)
|
|
353
370
|
AND id NOT IN (SELECT queue_id FROM relay_questions WHERE status = 'pending')
|
|
354
371
|
`),
|
|
@@ -426,15 +443,29 @@ export function createMessageRepository(db) {
|
|
|
426
443
|
// A continuation with no live worker can only be torn down — replaying
|
|
427
444
|
// it as deliverable work would send the CLI's own bookkeeping text back
|
|
428
445
|
// to the CLI as a user prompt.
|
|
446
|
+
//
|
|
447
|
+
// Same staleness rules as listRecoverableProcessing: a continuation
|
|
448
|
+
// waiting on an unanswered question is never stale, and an answer
|
|
449
|
+
// restarts the ceiling clock. A background-agent interview hangs its
|
|
450
|
+
// question cards on the continuation row, so without this a long
|
|
451
|
+
// interview was torn down by the ceiling while the human was still
|
|
452
|
+
// answering (live 2026-10-10).
|
|
429
453
|
listStaleProcessingContinuations: db.prepare(`
|
|
430
|
-
SELECT id, conversation_id
|
|
454
|
+
SELECT id, conversation_id, timestamp, processing_at, owner_last_claimed_at
|
|
431
455
|
FROM queue
|
|
432
456
|
WHERE status = 'processing'
|
|
433
457
|
AND kind = 'continuation'
|
|
434
458
|
AND (
|
|
435
459
|
COALESCE(owner_last_claimed_at, processing_at, timestamp) < @inactiveBefore
|
|
436
|
-
OR (
|
|
460
|
+
OR (
|
|
461
|
+
@ceilingBefore IS NOT NULL
|
|
462
|
+
AND MAX(
|
|
463
|
+
COALESCE(processing_at, timestamp),
|
|
464
|
+
COALESCE((SELECT MAX(answered_at) FROM relay_questions WHERE queue_id = queue.id), '')
|
|
465
|
+
) < @ceilingBefore
|
|
466
|
+
)
|
|
437
467
|
)
|
|
468
|
+
AND id NOT IN (SELECT queue_id FROM relay_questions WHERE status = 'pending')
|
|
438
469
|
`),
|
|
439
470
|
listQueueForPauseDrop: db.prepare(`SELECT id, conversation_id FROM queue WHERE status IN ('pending', 'processing', 'parked')`),
|
|
440
471
|
// Authoritative "is a turn in flight" signal for the conversation list.
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
import { createSubagentTimelineStatements } from './subagent-timeline-statements.mjs';
|
|
4
|
+
|
|
3
5
|
export function createQuestionRepository(db) {
|
|
6
|
+
const subagentTimeline = createSubagentTimelineStatements(db);
|
|
7
|
+
const runColumns = subagentTimeline.subagentRunSelectColumns;
|
|
8
|
+
const runColumnsAs = (alias) => runColumns.split(', ').map((column) => `${alias}.${column}`).join(', ');
|
|
4
9
|
return {
|
|
10
|
+
...subagentTimeline,
|
|
5
11
|
// relay questions
|
|
6
12
|
insertQuestion: db.prepare(`INSERT INTO relay_questions (id, queue_id, conversation_id, message_id, relay_mode, prompt, choices, request, request_schema, status, answer, structured_answer, sdk_session_id, owner_worker_id, continuation_id, continuation_question_id, created_at, answered_at, expires_at, attempt_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, ?, ?, ?, ?, ?, NULL, ?, ?)`),
|
|
7
13
|
getQuestion: db.prepare(`SELECT * FROM relay_questions WHERE id = ?`),
|
|
@@ -99,7 +105,7 @@ export function createQuestionRepository(db) {
|
|
|
99
105
|
WHERE id = ?
|
|
100
106
|
`),
|
|
101
107
|
listSubagentRunsByQueueMessage: db.prepare(`
|
|
102
|
-
SELECT
|
|
108
|
+
SELECT ${runColumns}
|
|
103
109
|
FROM subagent_runs
|
|
104
110
|
WHERE queue_message_id = ?
|
|
105
111
|
ORDER BY started_at ASC, id ASC
|
|
@@ -108,7 +114,7 @@ export function createQuestionRepository(db) {
|
|
|
108
114
|
// abort) also ends its subagent runs — nothing else ever will, and an
|
|
109
115
|
// un-reconciled row renders as a bubble stuck "running" forever.
|
|
110
116
|
listRunningSubagentRunsByQueueMessage: db.prepare(`
|
|
111
|
-
SELECT
|
|
117
|
+
SELECT ${runColumns}
|
|
112
118
|
FROM subagent_runs
|
|
113
119
|
WHERE queue_message_id = ? AND status = 'running'
|
|
114
120
|
`),
|
|
@@ -121,7 +127,7 @@ export function createQuestionRepository(db) {
|
|
|
121
127
|
WHERE queue_message_id = ? AND status = 'running'
|
|
122
128
|
`),
|
|
123
129
|
listSubagentRunsByResponse: db.prepare(`
|
|
124
|
-
SELECT sr
|
|
130
|
+
SELECT ${runColumnsAs('sr')}
|
|
125
131
|
FROM subagent_runs sr
|
|
126
132
|
WHERE sr.queue_message_id IN (
|
|
127
133
|
SELECT id FROM queue WHERE response_message_id = ?
|
|
@@ -29,7 +29,10 @@ export function createRemoteRelayRepository(db) {
|
|
|
29
29
|
const hasUnlock = db.prepare(`SELECT 1 FROM conversation_remote_relay_unlocks WHERE conversation_id = ? AND remote_relay_id = ?`);
|
|
30
30
|
const listUnlocks = db.prepare(`SELECT remote_relay_id, message_id, created_at FROM conversation_remote_relay_unlocks WHERE conversation_id = ? ORDER BY created_at ASC`);
|
|
31
31
|
const insertUnlock = db.prepare(`INSERT OR IGNORE INTO conversation_remote_relay_unlocks (conversation_id, remote_relay_id, message_id, created_at) VALUES (?, ?, ?, ?)`);
|
|
32
|
-
|
|
32
|
+
// The relay's unlocks and the approvals stored under its id (`<id>:watch`).
|
|
33
|
+
const deleteUnlocksForRelay = db.prepare(`
|
|
34
|
+
DELETE FROM conversation_remote_relay_unlocks WHERE remote_relay_id = ? OR substr(remote_relay_id, 1, length(?) + 1) = ? || ':'
|
|
35
|
+
`);
|
|
33
36
|
const deleteUnlocksForConversation = db.prepare(`DELETE FROM conversation_remote_relay_unlocks WHERE conversation_id = ?`);
|
|
34
37
|
const setMessageOrigin = db.prepare(`UPDATE messages SET origin_json = ? WHERE id = ?`);
|
|
35
38
|
const getMessageOrigin = db.prepare(`SELECT origin_json FROM messages WHERE id = ?`);
|
|
@@ -101,7 +104,8 @@ export function createRemoteRelayRepository(db) {
|
|
|
101
104
|
.map((row) => String(row.id));
|
|
102
105
|
},
|
|
103
106
|
forgetRelay(remoteRelayId) {
|
|
104
|
-
|
|
107
|
+
const id = String(remoteRelayId || '');
|
|
108
|
+
return deleteUnlocksForRelay.run(id, id, id).changes;
|
|
105
109
|
},
|
|
106
110
|
forgetConversation(conversationId) {
|
|
107
111
|
return deleteUnlocksForConversation.run(String(conversationId || '')).changes;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Sub-agent viewer: the timeline table, the run header columns and their SQL
|
|
4
|
+
// in one place. On a database the 0009 migration has not reached yet the
|
|
5
|
+
// timeline statements are null and the header columns read as absent; every
|
|
6
|
+
// caller treats that as "no timeline" and falls back to the activity, thought
|
|
7
|
+
// and stream rows a run has always had.
|
|
8
|
+
|
|
9
|
+
const HEADER_COLUMNS = [
|
|
10
|
+
'model',
|
|
11
|
+
'model_inherited',
|
|
12
|
+
'subagent_type',
|
|
13
|
+
'task_id',
|
|
14
|
+
'total_tokens',
|
|
15
|
+
'background',
|
|
16
|
+
'truncated',
|
|
17
|
+
'timeline_owner',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const BASE_RUN_COLUMNS = [
|
|
21
|
+
'id',
|
|
22
|
+
'queue_message_id',
|
|
23
|
+
'conversation_id',
|
|
24
|
+
'parent_subagent_id',
|
|
25
|
+
'display_name',
|
|
26
|
+
'status',
|
|
27
|
+
'started_at',
|
|
28
|
+
'updated_at',
|
|
29
|
+
'completed_at',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function createSubagentTimelineStatements(db) {
|
|
33
|
+
const tableNames = new Set(
|
|
34
|
+
db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table'`).all().map((row) => String(row.name)),
|
|
35
|
+
);
|
|
36
|
+
if (!tableNames.has('subagent_runs')) {
|
|
37
|
+
return { subagentRunsSupportHeader: false, subagentTimelineSupported: false, subagentRunSelectColumns: BASE_RUN_COLUMNS.join(', ') };
|
|
38
|
+
}
|
|
39
|
+
const runColumns = new Set(db.prepare(`PRAGMA table_info(subagent_runs)`).all().map((column) => String(column.name)));
|
|
40
|
+
const subagentRunsSupportHeader = HEADER_COLUMNS.every((name) => runColumns.has(name));
|
|
41
|
+
const subagentTimelineSupported = subagentRunsSupportHeader && tableNames.has('subagent_timeline');
|
|
42
|
+
const subagentRunSelectColumns = [...BASE_RUN_COLUMNS, ...(subagentRunsSupportHeader ? HEADER_COLUMNS : [])].join(', ');
|
|
43
|
+
|
|
44
|
+
const statements = {
|
|
45
|
+
subagentRunsSupportHeader,
|
|
46
|
+
subagentTimelineSupported,
|
|
47
|
+
// The column list every run read uses, so the header travels with the
|
|
48
|
+
// run wherever runs are listed (conversation payload, in-flight snapshot).
|
|
49
|
+
subagentRunSelectColumns,
|
|
50
|
+
updateSubagentRunHeader: null,
|
|
51
|
+
setSubagentRunTruncated: null,
|
|
52
|
+
listChildSubagentRuns: null,
|
|
53
|
+
getSubagentRunByTaskId: null,
|
|
54
|
+
listRunningBackgroundSubagentRunsForSession: null,
|
|
55
|
+
getTimelineEntryByKey: null,
|
|
56
|
+
getMaxTimelineSeq: null,
|
|
57
|
+
countTimelineEntries: null,
|
|
58
|
+
insertTimelineEntry: null,
|
|
59
|
+
updateTimelineEntryByKey: null,
|
|
60
|
+
listTimelineEntries: null,
|
|
61
|
+
deleteConvSubagentTimeline: null,
|
|
62
|
+
listLegacyActivityBySubagentRun: null,
|
|
63
|
+
listLegacyThoughtsBySubagentRun: null,
|
|
64
|
+
listLegacyStreamBySubagentRun: null,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// What a run recorded before the timeline existed: its tagged activity
|
|
68
|
+
// lines, thoughts and reply text. The viewer reads these for an old run.
|
|
69
|
+
const hasRunColumn = (table) => tableNames.has(table)
|
|
70
|
+
&& db.prepare(`PRAGMA table_info(${table})`).all().some((column) => column.name === 'subagent_run_id');
|
|
71
|
+
if (hasRunColumn('relay_activity')) {
|
|
72
|
+
statements.listLegacyActivityBySubagentRun = db.prepare(`
|
|
73
|
+
SELECT id, text, metadata_json, created_at FROM relay_activity
|
|
74
|
+
WHERE subagent_run_id = ? ORDER BY id ASC LIMIT ?
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
if (hasRunColumn('relay_thought')) {
|
|
78
|
+
statements.listLegacyThoughtsBySubagentRun = db.prepare(`
|
|
79
|
+
SELECT reasoning_id, seq, text, done, created_at FROM relay_thought
|
|
80
|
+
WHERE subagent_run_id = ? ORDER BY seq ASC, id ASC LIMIT ?
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
if (hasRunColumn('relay_stream_events')) {
|
|
84
|
+
statements.listLegacyStreamBySubagentRun = db.prepare(`
|
|
85
|
+
SELECT text, done, created_at FROM relay_stream_events
|
|
86
|
+
WHERE subagent_run_id = ? ORDER BY seq ASC, id ASC LIMIT ?
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
if (!subagentRunsSupportHeader) return statements;
|
|
90
|
+
|
|
91
|
+
// A patch, not a replace: every field the writer did not name (NULL) keeps
|
|
92
|
+
// what the row has. `background` and `truncated` only ever turn on, and the
|
|
93
|
+
// timeline owner is claimed once.
|
|
94
|
+
statements.updateSubagentRunHeader = db.prepare(`
|
|
95
|
+
UPDATE subagent_runs
|
|
96
|
+
SET model = COALESCE(@model, model),
|
|
97
|
+
model_inherited = COALESCE(@modelInherited, model_inherited),
|
|
98
|
+
subagent_type = COALESCE(@subagentType, subagent_type),
|
|
99
|
+
task_id = COALESCE(@taskId, task_id),
|
|
100
|
+
total_tokens = COALESCE(@totalTokens, total_tokens),
|
|
101
|
+
background = MAX(background, COALESCE(@background, 0)),
|
|
102
|
+
truncated = MAX(truncated, COALESCE(@truncated, 0)),
|
|
103
|
+
timeline_owner = COALESCE(timeline_owner, @timelineOwner),
|
|
104
|
+
updated_at = @updatedAt
|
|
105
|
+
WHERE id = @id
|
|
106
|
+
`);
|
|
107
|
+
statements.setSubagentRunTruncated = db.prepare(`UPDATE subagent_runs SET truncated = 1, updated_at = ? WHERE id = ?`);
|
|
108
|
+
statements.listChildSubagentRuns = db.prepare(`
|
|
109
|
+
SELECT ${subagentRunSelectColumns}
|
|
110
|
+
FROM subagent_runs
|
|
111
|
+
WHERE parent_subagent_id = ?
|
|
112
|
+
ORDER BY started_at ASC, id ASC
|
|
113
|
+
`);
|
|
114
|
+
// A worker's live background agents: what dies with it.
|
|
115
|
+
if (tableNames.has('conversations')) {
|
|
116
|
+
statements.listRunningBackgroundSubagentRunsForSession = db.prepare(`
|
|
117
|
+
SELECT ${subagentRunSelectColumns}
|
|
118
|
+
FROM subagent_runs
|
|
119
|
+
WHERE status = 'running' AND background = 1
|
|
120
|
+
AND conversation_id IN (SELECT id FROM conversations WHERE sdk_session_id = ? OR id = ?)
|
|
121
|
+
`);
|
|
122
|
+
}
|
|
123
|
+
statements.getSubagentRunByTaskId = db.prepare(`
|
|
124
|
+
SELECT ${subagentRunSelectColumns}
|
|
125
|
+
FROM subagent_runs
|
|
126
|
+
WHERE conversation_id = ? AND task_id = ?
|
|
127
|
+
ORDER BY started_at DESC
|
|
128
|
+
LIMIT 1
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
if (!subagentTimelineSupported) return statements;
|
|
132
|
+
|
|
133
|
+
statements.getTimelineEntryByKey = db.prepare(`SELECT id, seq, done FROM subagent_timeline WHERE run_id = ? AND entry_key = ?`);
|
|
134
|
+
statements.getMaxTimelineSeq = db.prepare(`SELECT COALESCE(MAX(seq), 0) AS max_seq FROM subagent_timeline WHERE run_id = ?`);
|
|
135
|
+
statements.countTimelineEntries = db.prepare(`SELECT COUNT(*) AS cnt FROM subagent_timeline WHERE run_id = ?`);
|
|
136
|
+
statements.insertTimelineEntry = db.prepare(`
|
|
137
|
+
INSERT INTO subagent_timeline (
|
|
138
|
+
run_id, conversation_id, queue_message_id, entry_key, seq, kind, text, tool_name, tool_use_id,
|
|
139
|
+
metadata_json, done, created_at, updated_at
|
|
140
|
+
) VALUES (
|
|
141
|
+
@runId, @conversationId, @queueMessageId, @entryKey, @seq, @kind, @text, @toolName, @toolUseId,
|
|
142
|
+
@metadataJson, @done, @createdAt, @updatedAt
|
|
143
|
+
)
|
|
144
|
+
`);
|
|
145
|
+
// In place: the entry keeps its seq (its place in the order). A closed
|
|
146
|
+
// entry stays closed, though its text may still be corrected (the complete
|
|
147
|
+
// copy of a block that streamed).
|
|
148
|
+
statements.updateTimelineEntryByKey = db.prepare(`
|
|
149
|
+
UPDATE subagent_timeline
|
|
150
|
+
SET text = @text,
|
|
151
|
+
tool_name = COALESCE(@toolName, tool_name),
|
|
152
|
+
tool_use_id = COALESCE(@toolUseId, tool_use_id),
|
|
153
|
+
metadata_json = COALESCE(@metadataJson, metadata_json),
|
|
154
|
+
done = MAX(done, @done),
|
|
155
|
+
updated_at = @updatedAt
|
|
156
|
+
WHERE run_id = @runId AND entry_key = @entryKey
|
|
157
|
+
`);
|
|
158
|
+
// Everything after `afterSeq`, plus whatever is still open before it: an
|
|
159
|
+
// open entry changes in place, so a reader that already has its seq still
|
|
160
|
+
// needs its newer text.
|
|
161
|
+
statements.listTimelineEntries = db.prepare(`
|
|
162
|
+
SELECT seq, kind, text, tool_name, tool_use_id, metadata_json, done, created_at, updated_at
|
|
163
|
+
FROM subagent_timeline
|
|
164
|
+
WHERE run_id = @runId AND (seq > @afterSeq OR done = 0)
|
|
165
|
+
ORDER BY seq ASC
|
|
166
|
+
LIMIT @limit
|
|
167
|
+
`);
|
|
168
|
+
statements.deleteConvSubagentTimeline = db.prepare(`DELETE FROM subagent_timeline WHERE conversation_id = ?`);
|
|
169
|
+
return statements;
|
|
170
|
+
}
|
|
@@ -116,6 +116,8 @@ export function registerAskUserRoutes(app, deps) {
|
|
|
116
116
|
DEFAULT_RELAY_MODE,
|
|
117
117
|
sessionWorkerRegistry,
|
|
118
118
|
pushDispatchService,
|
|
119
|
+
// Relay event subscriptions: a question is an event for its watchers.
|
|
120
|
+
relayEventService = null,
|
|
119
121
|
// Remote relays: another relay's agent answering a question is refused
|
|
120
122
|
// while the inbound switch is off. Absent in older wiring (no-op).
|
|
121
123
|
remoteRelayInbound = null,
|
|
@@ -246,6 +248,7 @@ export function registerAskUserRoutes(app, deps) {
|
|
|
246
248
|
console.log(`[${ts()}] QUESTION ${questionId.slice(0,8)} conv=${question.conversationId.slice(0,8)} mode=${relayMode} fields=${fieldCount} prompt="${promptText.slice(0,60)}"`);
|
|
247
249
|
io.emit('relay_question', { question });
|
|
248
250
|
void pushDispatchService?.notifyQuestion?.(question);
|
|
251
|
+
try { relayEventService?.noteQuestion?.(question); } catch {}
|
|
249
252
|
res.json({ question });
|
|
250
253
|
});
|
|
251
254
|
|