@oar-sh/oar 0.9.11 → 0.9.13

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 (90) hide show
  1. package/CHANGELOG.md +332 -0
  2. package/README.md +10 -10
  3. package/package.json +1 -1
  4. package/server/README.md +201 -12
  5. package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
  6. package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
  7. package/server/claude-worker/claude-session-process.mjs +179 -0
  8. package/server/claude-worker/claude-session-worker.mjs +17 -0
  9. package/server/claude-worker/task-output-discovery.mjs +152 -0
  10. package/server/copilot-worker/copilot-continuation-signals.mjs +11 -5
  11. package/server/copilot-worker/copilot-sdk-session-process.mjs +169 -3
  12. package/server/copilot-worker/copilot-sdk-session-worker.mjs +2 -0
  13. package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
  14. package/server/copilot-worker/copilot-task-settle.mjs +96 -0
  15. package/server/db-schema.mjs +19 -0
  16. package/server/grok-worker/acp-host-services.mjs +48 -6
  17. package/server/grok-worker/grok-background-tasks.mjs +248 -0
  18. package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
  19. package/server/grok-worker/grok-session-worker.mjs +33 -1
  20. package/server/grok-worker/grok-turn-runner.mjs +11 -0
  21. package/server/migrations/0017-conversation-history-indexes.mjs +89 -0
  22. package/server/migrations/0018-background-task-rows.mjs +101 -0
  23. package/server/public/app/action-confirmations.js +12 -15
  24. package/server/public/app/api-client.js +70 -3
  25. package/server/public/app/background-tasks-view.mjs +334 -16
  26. package/server/public/app/bootstrap.js +102 -5
  27. package/server/public/app/claude-background-command-limit-options.mjs +21 -0
  28. package/server/public/app/conversation-payload-cache.mjs +357 -0
  29. package/server/public/app/conversation-preferences.mjs +10 -4
  30. package/server/public/app/conversation-turn-state.mjs +1 -1
  31. package/server/public/app/conversation-view.js +462 -8
  32. package/server/public/app/conversation-window-retention.mjs +96 -0
  33. package/server/public/app/host-suspend-ui.js +45 -44
  34. package/server/public/app/journal-view.js +259 -67
  35. package/server/public/app/push-settings.js +1 -0
  36. package/server/public/app/relay-prompt-sanitizer.mjs +20 -6
  37. package/server/public/app/relay-restart-note-view.mjs +36 -0
  38. package/server/public/app/relay-restart-ui.js +568 -0
  39. package/server/public/app/settings-modal.js +162 -2
  40. package/server/public/app/socket-handlers.js +59 -7
  41. package/server/public/app/store.js +25 -0
  42. package/server/public/app/subagent-viewer.mjs +7 -1
  43. package/server/public/app/task-console-render.mjs +401 -0
  44. package/server/public/app/task-console-watch.mjs +42 -0
  45. package/server/public/app/task-console.mjs +730 -0
  46. package/server/public/app/task-output-text.mjs +243 -0
  47. package/server/public/app/transcript-island.mjs +73 -0
  48. package/server/public/app/transcript-jump-bottom-view.mjs +42 -8
  49. package/server/public/app/transcript-jump-bottom.mjs +68 -11
  50. package/server/public/app/transcript-reading-anchor.mjs +256 -0
  51. package/server/public/app/transcript-reload-freshness.mjs +17 -1
  52. package/server/public/app/transcript-viewport-anchor.mjs +20 -1
  53. package/server/public/index.html +306 -6
  54. package/server/relay-tools.md +6 -0
  55. package/server/repositories/message-repository.mjs +11 -0
  56. package/server/repositories/question-repository.mjs +4 -0
  57. package/server/repositories/remote-relay-repository.mjs +4 -2
  58. package/server/routes/messages-routes.mjs +397 -23
  59. package/server/routes/relay-board-routes.mjs +19 -2
  60. package/server/routes/relay-restart-routes.mjs +73 -0
  61. package/server/routes/sessions-routes.mjs +208 -66
  62. package/server/server-runtime.mjs +269 -137
  63. package/server/services/about-service.mjs +13 -12
  64. package/server/services/background-task-persistence.mjs +133 -0
  65. package/server/services/background-task-store.mjs +470 -6
  66. package/server/services/cli-process-runner.mjs +4 -21
  67. package/server/services/conversation-history-page.mjs +199 -0
  68. package/server/services/conversation-model-fallback.mjs +57 -0
  69. package/server/services/host-suspend-command.mjs +2 -1
  70. package/server/services/preview-proxy-server.mjs +14 -1
  71. package/server/services/process-list-service.mjs +2 -1
  72. package/server/services/provider-model-selection.mjs +195 -0
  73. package/server/services/push-dispatch-service.mjs +9 -2
  74. package/server/services/relay-event-service.mjs +27 -9
  75. package/server/services/relay-prompt-sanitizer.mjs +18 -4
  76. package/server/services/relay-restart-picture.mjs +209 -0
  77. package/server/services/relay-restart-queue.mjs +380 -0
  78. package/server/services/relay-shutdown-gate.mjs +4 -1
  79. package/server/services/remote-relay-dispatcher.mjs +316 -20
  80. package/server/services/restart-resume-service.mjs +303 -0
  81. package/server/services/session-activity-tracker.mjs +6 -2
  82. package/server/services/session-worker-process-service.mjs +6 -4
  83. package/server/services/task-output-rooms.mjs +259 -0
  84. package/server/services/task-output-service.mjs +761 -0
  85. package/shared/claude-background-command-limit.mjs +83 -0
  86. package/shared/relay-events.mjs +55 -2
  87. package/shared/remote-relay-contract.mjs +47 -10
  88. package/shared/task-output-text.mjs +850 -0
  89. package/shared/worker-runtime/task-output-spool.mjs +181 -0
  90. package/shared/worker-runtime/worker-websocket-link.mjs +96 -20
@@ -68,32 +68,33 @@ const COMMIT_PATTERN = /^[0-9a-f]{40}$/i;
68
68
  * The commit a git checkout has checked out, read from its .git files rather
69
69
  * than from a `git` process: HEAD, then the loose ref, then packed-refs. A
70
70
  * linked worktree's .git is a file naming its git dir, whose refs live in the
71
- * common dir. Anything unexpected reads as null.
71
+ * common dir. Anything unexpected reads as null. `pathImpl` is the host's path
72
+ * module; tests pass path.posix and path.win32.
72
73
  */
73
- export function readGitCommit(root, { fsImpl = fs } = {}) {
74
+ export function readGitCommit(root, { fsImpl = fs, pathImpl = path } = {}) {
74
75
  if (!root) return null;
75
- const dotGit = path.join(root, '.git');
76
+ const dotGit = pathImpl.join(root, '.git');
76
77
  let gitDir = dotGit;
77
78
  const pointer = readFileText(fsImpl, dotGit);
78
79
  if (pointer !== null) {
79
80
  const match = pointer.match(/^gitdir:\s*(.+)$/m);
80
81
  if (!match) return null;
81
- gitDir = path.resolve(root, match[1].trim());
82
+ gitDir = pathImpl.resolve(root, match[1].trim());
82
83
  }
83
- const head = readFileText(fsImpl, path.join(gitDir, 'HEAD'));
84
+ const head = readFileText(fsImpl, pathImpl.join(gitDir, 'HEAD'));
84
85
  if (head === null) return null;
85
86
  const headText = head.trim();
86
87
  if (COMMIT_PATTERN.test(headText)) return headText.toLowerCase();
87
88
  const refMatch = headText.match(/^ref:\s*(\S+)$/);
88
89
  if (!refMatch) return null;
89
90
  const ref = refMatch[1];
90
- const commonDirText = readFileText(fsImpl, path.join(gitDir, 'commondir'));
91
- const commonDir = commonDirText ? path.resolve(gitDir, commonDirText.trim()) : gitDir;
91
+ const commonDirText = readFileText(fsImpl, pathImpl.join(gitDir, 'commondir'));
92
+ const commonDir = commonDirText ? pathImpl.resolve(gitDir, commonDirText.trim()) : gitDir;
92
93
  for (const dir of new Set([gitDir, commonDir])) {
93
- const loose = readFileText(fsImpl, path.join(dir, ...ref.split('/')));
94
+ const loose = readFileText(fsImpl, pathImpl.join(dir, ...ref.split('/')));
94
95
  if (loose && COMMIT_PATTERN.test(loose.trim())) return loose.trim().toLowerCase();
95
96
  }
96
- const packed = readFileText(fsImpl, path.join(commonDir, 'packed-refs')) || '';
97
+ const packed = readFileText(fsImpl, pathImpl.join(commonDir, 'packed-refs')) || '';
97
98
  for (const line of packed.split(/\r?\n/)) {
98
99
  const [hash, name] = line.trim().split(/\s+/);
99
100
  if (name === ref && COMMIT_PATTERN.test(hash || '')) return hash.toLowerCase();
@@ -102,11 +103,11 @@ export function readGitCommit(root, { fsImpl = fs } = {}) {
102
103
  }
103
104
 
104
105
  /** The commit this build came from: the checkout's HEAD, else a `gitHead` stamp in package.json. */
105
- export function resolveBuildCommit(packageRoot, { fsImpl = fs } = {}) {
106
- const fromGit = readGitCommit(packageRoot, { fsImpl });
106
+ export function resolveBuildCommit(packageRoot, { fsImpl = fs, pathImpl = path } = {}) {
107
+ const fromGit = readGitCommit(packageRoot, { fsImpl, pathImpl });
107
108
  if (fromGit) return fromGit;
108
109
  try {
109
- const stamp = String(JSON.parse(readFileText(fsImpl, path.join(packageRoot, 'package.json')) || '{}')?.gitHead || '').trim();
110
+ const stamp = String(JSON.parse(readFileText(fsImpl, pathImpl.join(packageRoot, 'package.json')) || '{}')?.gitHead || '').trim();
110
111
  return COMMIT_PATTERN.test(stamp) ? stamp.toLowerCase() : null;
111
112
  } catch {
112
113
  return null;
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ import { backgroundTaskRowsTableProblem } from '../migrations/0018-background-task-rows.mjs';
4
+
5
+ // Persistence for the background-task store's console rows: the
6
+ // `background_task_rows` table (migration 0018) behind a small adapter, so
7
+ // the store restores its rows after a restart and stays testable without a
8
+ // database. Without a usable table every call is a no-op and the store keeps
9
+ // its rows in memory only, as before; a problem is logged once and never
10
+ // stops the relay.
11
+
12
+ function unavailable(reason) {
13
+ return {
14
+ available: false,
15
+ reason,
16
+ load: () => [],
17
+ saveLive: () => false,
18
+ saveEnded: () => false,
19
+ remove: () => false,
20
+ forget: () => false,
21
+ pruneEnded: () => 0,
22
+ touchLive: () => false,
23
+ };
24
+ }
25
+
26
+ function parseRow(text) {
27
+ try {
28
+ const row = JSON.parse(String(text || ''));
29
+ return row && typeof row === 'object' && !Array.isArray(row) ? row : null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /** A row as stored: no health reading (the poller reads the file again). */
36
+ function storedRowJson(row) {
37
+ const { health: _health, ...rest } = row || {};
38
+ return JSON.stringify(rest);
39
+ }
40
+
41
+ export function createSqliteBackgroundTaskPersistence(db, { logger = console, now = () => Date.now() } = {}) {
42
+ let stmts;
43
+ try {
44
+ const problem = backgroundTaskRowsTableProblem(db);
45
+ if (problem) return unavailable(problem);
46
+ stmts = {
47
+ list: db.prepare(`SELECT store_key, task_id, state, row_json, ended_at, updated_at FROM background_task_rows`),
48
+ touchLive: db.prepare(`UPDATE background_task_rows SET updated_at = ? WHERE store_key = ? AND state = 'live'`),
49
+ upsert: db.prepare(`
50
+ INSERT INTO background_task_rows (store_key, task_id, state, row_json, ended_at, updated_at)
51
+ VALUES (?, ?, ?, ?, ?, ?)
52
+ ON CONFLICT(store_key, task_id) DO UPDATE SET
53
+ state = excluded.state, row_json = excluded.row_json, ended_at = excluded.ended_at, updated_at = excluded.updated_at
54
+ `),
55
+ liveOfKey: db.prepare(`SELECT task_id FROM background_task_rows WHERE store_key = ? AND state = 'live'`),
56
+ remove: db.prepare(`DELETE FROM background_task_rows WHERE store_key = ? AND task_id = ?`),
57
+ removeLive: db.prepare(`DELETE FROM background_task_rows WHERE store_key = ? AND task_id = ? AND state = 'live'`),
58
+ forget: db.prepare(`DELETE FROM background_task_rows WHERE store_key = ?`),
59
+ prune: db.prepare(`DELETE FROM background_task_rows WHERE state = 'ended' AND ended_at < ?`),
60
+ };
61
+ } catch (error) {
62
+ const reason = error?.message || String(error);
63
+ logger?.warn?.(`[background-tasks] console rows are not persisted: ${reason}`);
64
+ return unavailable(reason);
65
+ }
66
+
67
+ // One warning per kind of failure: a write that fails keeps failing, and
68
+ // the store goes on in memory either way.
69
+ const warned = new Set();
70
+ function guard(what, run, fallback) {
71
+ try {
72
+ return run();
73
+ } catch (error) {
74
+ if (!warned.has(what)) {
75
+ warned.add(what);
76
+ logger?.warn?.(`[background-tasks] could not ${what} a console row: ${error?.message || error}`);
77
+ }
78
+ return fallback;
79
+ }
80
+ }
81
+
82
+ const saveLiveTx = db.transaction((key, rows) => {
83
+ const keep = new Set(rows.map((row) => String(row.taskId)));
84
+ for (const { task_id: taskId } of stmts.liveOfKey.all(key)) {
85
+ if (!keep.has(taskId)) stmts.removeLive.run(key, taskId);
86
+ }
87
+ const at = now();
88
+ for (const row of rows) stmts.upsert.run(key, String(row.taskId), 'live', storedRowJson(row), null, at);
89
+ });
90
+
91
+ return {
92
+ available: true,
93
+ reason: '',
94
+ /** Every stored row: `{ key, taskId, state, row, endedAt, updatedAt }`. */
95
+ load() {
96
+ return guard('read', () => stmts.list.all().map((entry) => ({
97
+ key: String(entry.store_key),
98
+ taskId: String(entry.task_id),
99
+ state: String(entry.state),
100
+ row: parseRow(entry.row_json),
101
+ endedAt: Number.isFinite(Number(entry.ended_at)) && entry.ended_at !== null ? Number(entry.ended_at) : null,
102
+ updatedAt: Number.isFinite(Number(entry.updated_at)) ? Number(entry.updated_at) : null,
103
+ })).filter((entry) => entry.row), []);
104
+ },
105
+ /** The key's live console rows are exactly these (an ended row of the same task is replaced). */
106
+ saveLive(key, rows) {
107
+ return guard('write', () => { saveLiveTx(String(key), Array.isArray(rows) ? rows : []); return true; }, false);
108
+ },
109
+ saveEnded(key, row) {
110
+ return guard('write', () => {
111
+ stmts.upsert.run(String(key), String(row.taskId), 'ended', storedRowJson(row), Number(row.endedAt) || now(), now());
112
+ return true;
113
+ }, false);
114
+ },
115
+ remove(key, taskId) {
116
+ return guard('delete', () => stmts.remove.run(String(key), String(taskId)).changes > 0, false);
117
+ },
118
+ forget(keys) {
119
+ return guard('delete', () => {
120
+ for (const key of keys) stmts.forget.run(String(key));
121
+ return true;
122
+ }, false);
123
+ },
124
+ /** The key's live rows were confirmed running now (its worker's digest agreed). */
125
+ touchLive(key) {
126
+ return guard('write', () => stmts.touchLive.run(now(), String(key)).changes > 0, false);
127
+ },
128
+ /** Drops the ended rows that ended before `cutoff` (epoch ms). */
129
+ pruneEnded(cutoff) {
130
+ return guard('prune', () => stmts.prune.run(Number(cutoff) || 0).changes, 0);
131
+ },
132
+ };
133
+ }