@oar-sh/oar 0.9.9 → 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.
Files changed (85) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/README.md +78 -9
  3. package/bin/bat/oar.bat +46 -46
  4. package/package.json +2 -2
  5. package/server/README.md +286 -3
  6. package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
  7. package/server/claude-worker/claude-session-process.mjs +35 -1
  8. package/server/claude-worker/claude-turn-publisher.mjs +3 -0
  9. package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
  10. package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
  11. package/server/copilot-worker/copilot-sdk-session-process.mjs +3 -1
  12. package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
  13. package/server/db-schema.mjs +36 -0
  14. package/server/migrations/0009-subagent-timeline.mjs +135 -0
  15. package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
  16. package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
  17. package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
  18. package/server/public/app/agent-exchange-fold-view.mjs +248 -0
  19. package/server/public/app/api-client.js +61 -0
  20. package/server/public/app/archive-confirm.mjs +28 -0
  21. package/server/public/app/ask-user-view.js +3 -0
  22. package/server/public/app/attachments-view.js +516 -6
  23. package/server/public/app/background-tasks-view.mjs +18 -26
  24. package/server/public/app/bootstrap.js +186 -35
  25. package/server/public/app/chat-actions-menu-state.mjs +116 -0
  26. package/server/public/app/claude-cloud-conversation-ui.js +10 -22
  27. package/server/public/app/composer-control-state.mjs +16 -0
  28. package/server/public/app/composer-keyboard.mjs +54 -0
  29. package/server/public/app/conversation-view.js +361 -85
  30. package/server/public/app/explorer-edit-state.mjs +68 -0
  31. package/server/public/app/host-suspend-ui.js +6 -3
  32. package/server/public/app/journal-view.js +146 -26
  33. package/server/public/app/no-conversation-state.mjs +58 -0
  34. package/server/public/app/provider-readiness.mjs +120 -0
  35. package/server/public/app/relay-board-view.js +3 -0
  36. package/server/public/app/relay-event-subscriptions-ui.js +184 -0
  37. package/server/public/app/relay-event-view.mjs +234 -0
  38. package/server/public/app/settings-modal.js +53 -1
  39. package/server/public/app/socket-handlers.js +59 -28
  40. package/server/public/app/store.js +19 -2
  41. package/server/public/app/subagent-row.mjs +138 -0
  42. package/server/public/app/subagent-timeline-view.mjs +186 -0
  43. package/server/public/app/subagent-viewer-watch.mjs +50 -0
  44. package/server/public/app/subagent-viewer.mjs +534 -0
  45. package/server/public/app/task-row-format.mjs +29 -0
  46. package/server/public/app/thinking-follow.js +4 -0
  47. package/server/public/app/transcript-jump-bottom-view.mjs +290 -0
  48. package/server/public/app/transcript-jump-bottom.mjs +99 -0
  49. package/server/public/app/transcript-reload-freshness.mjs +25 -0
  50. package/server/public/app/update-ui.js +1 -1
  51. package/server/public/app/viewer-history.mjs +5 -3
  52. package/server/public/index.html +396 -5
  53. package/server/repositories/message-repository.mjs +29 -3
  54. package/server/repositories/question-repository.mjs +9 -3
  55. package/server/repositories/remote-relay-repository.mjs +6 -2
  56. package/server/repositories/subagent-timeline-statements.mjs +170 -0
  57. package/server/routes/ask-user-routes.mjs +3 -0
  58. package/server/routes/messages-routes.mjs +515 -50
  59. package/server/routes/relay-event-routes.mjs +157 -0
  60. package/server/routes/remote-relay-routes.mjs +15 -0
  61. package/server/routes/sessions-routes.mjs +72 -0
  62. package/server/server-runtime.mjs +245 -62
  63. package/server/services/explorer-entry-name.mjs +26 -0
  64. package/server/services/explorer-mutations.mjs +122 -0
  65. package/server/services/preview-proxy-server.mjs +41 -23
  66. package/server/services/push-dispatch-service.mjs +18 -0
  67. package/server/services/relay-event-link-service.mjs +289 -0
  68. package/server/services/relay-event-service.mjs +1935 -0
  69. package/server/services/relay-shutdown-gate.mjs +31 -0
  70. package/server/services/remote-relay-caller-context.mjs +52 -7
  71. package/server/services/remote-relay-dispatcher.mjs +494 -2
  72. package/server/services/remote-relay-registry-service.mjs +11 -1
  73. package/server/services/session-activity-tracker.mjs +487 -0
  74. package/server/services/session-history-refresh-service.mjs +7 -0
  75. package/server/services/stale-attempt-log-limiter.mjs +96 -0
  76. package/server/services/stale-recovery-clock-guard.mjs +57 -0
  77. package/server/services/stale-subagent-run-sweep.mjs +109 -0
  78. package/server/services/subagent-record-retention.mjs +164 -0
  79. package/server/services/subagent-timeline-service.mjs +328 -0
  80. package/server/services/subagent-viewer-rooms.mjs +119 -0
  81. package/server/services/upgrade-socket-guard.mjs +16 -0
  82. package/shared/relay-events.mjs +371 -0
  83. package/shared/remote-relay-contract.mjs +157 -5
  84. package/shared/subagent-record-retention.mjs +31 -0
  85. package/shared/subagent-timeline.mjs +79 -0
@@ -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