@oar-sh/oar 0.9.10 → 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.
Files changed (135) hide show
  1. package/CHANGELOG.md +440 -1
  2. package/README.md +156 -972
  3. package/package.json +13 -13
  4. package/server/README.md +112 -25
  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 +217 -11
  8. package/server/claude-worker/claude-session-worker.mjs +26 -1
  9. package/server/claude-worker/task-output-discovery.mjs +126 -0
  10. package/server/copilot-worker/copilot-sdk-session-process.mjs +235 -14
  11. package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -0
  12. package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
  13. package/server/copilot-worker/copilot-task-settle.mjs +96 -0
  14. package/server/db-schema.mjs +68 -2
  15. package/server/grok-worker/acp-client.mjs +2 -1
  16. package/server/grok-worker/acp-host-services.mjs +46 -6
  17. package/server/grok-worker/grok-background-tasks.mjs +241 -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/0013-previews.mjs +130 -0
  22. package/server/migrations/0014-conversation-groups.mjs +89 -0
  23. package/server/migrations/0015-conversation-group-position.mjs +76 -0
  24. package/server/migrations/0016-share-access-log.mjs +110 -0
  25. package/server/migrations/0017-conversation-history-indexes.mjs +89 -0
  26. package/server/public/app/about-ui.js +73 -0
  27. package/server/public/app/about-view.mjs +234 -0
  28. package/server/public/app/action-confirmations.js +31 -14
  29. package/server/public/app/agent-exchange-fold-view.mjs +40 -101
  30. package/server/public/app/api-client.js +264 -16
  31. package/server/public/app/ask-user-view.js +17 -15
  32. package/server/public/app/background-tasks-view.mjs +285 -16
  33. package/server/public/app/bootstrap.js +253 -37
  34. package/server/public/app/chat-actions-menu-state.mjs +20 -10
  35. package/server/public/app/claude-background-command-limit-options.mjs +21 -0
  36. package/server/public/app/context-menu.mjs +90 -6
  37. package/server/public/app/conversation-group-drag.mjs +191 -0
  38. package/server/public/app/conversation-group-order.mjs +265 -0
  39. package/server/public/app/conversation-group-state.mjs +100 -0
  40. package/server/public/app/conversation-list-drag.mjs +449 -0
  41. package/server/public/app/conversation-payload-cache.mjs +357 -0
  42. package/server/public/app/conversation-turn-state.mjs +81 -0
  43. package/server/public/app/conversation-view.js +567 -94
  44. package/server/public/app/conversation-window-retention.mjs +96 -0
  45. package/server/public/app/cwd-picker.js +1 -1
  46. package/server/public/app/hidden-messages-view.mjs +312 -0
  47. package/server/public/app/host-suspend-ui.js +7 -1
  48. package/server/public/app/journal-view.js +1033 -82
  49. package/server/public/app/preview-cards.mjs +117 -31
  50. package/server/public/app/preview-command.mjs +10 -1
  51. package/server/public/app/relay-board-view.js +19 -6
  52. package/server/public/app/relay-prompt-sanitizer.mjs +11 -4
  53. package/server/public/app/relay-restart-error.mjs +16 -0
  54. package/server/public/app/settings-modal.js +150 -10
  55. package/server/public/app/settings-tabs.js +1 -1
  56. package/server/public/app/share-format.mjs +49 -0
  57. package/server/public/app/share-preview-marks.mjs +167 -0
  58. package/server/public/app/share-state.js +109 -0
  59. package/server/public/app/shared-conversations-settings.js +480 -0
  60. package/server/public/app/socket-handlers.js +138 -20
  61. package/server/public/app/store.js +121 -30
  62. package/server/public/app/subagent-row.mjs +3 -3
  63. package/server/public/app/subagent-viewer.mjs +37 -7
  64. package/server/public/app/task-console-render.mjs +237 -0
  65. package/server/public/app/task-console-watch.mjs +42 -0
  66. package/server/public/app/task-console.mjs +630 -0
  67. package/server/public/app/task-output-text.mjs +234 -0
  68. package/server/public/app/thinking-follow.js +0 -4
  69. package/server/public/app/transcript-island.mjs +73 -0
  70. package/server/public/app/transcript-jump-bottom-view.mjs +230 -26
  71. package/server/public/app/transcript-jump-bottom.mjs +170 -22
  72. package/server/public/app/transcript-reading-anchor.mjs +256 -0
  73. package/server/public/app/transcript-reload-freshness.mjs +17 -1
  74. package/server/public/app/transcript-viewport-anchor.mjs +159 -0
  75. package/server/public/index.html +625 -21
  76. package/server/relay-tools.md +3 -1
  77. package/server/relay.mjs +2 -1
  78. package/server/repositories/conversation-group-repository.mjs +444 -0
  79. package/server/repositories/message-repository.mjs +9 -0
  80. package/server/repositories/question-repository.mjs +4 -0
  81. package/server/repositories/session-repository.mjs +12 -37
  82. package/server/repositories/share-repository.mjs +322 -0
  83. package/server/repositories/share-visibility-statements.mjs +10 -0
  84. package/server/routes/about-routes.mjs +15 -0
  85. package/server/routes/cache-routes.mjs +4 -0
  86. package/server/routes/conversation-group-routes.mjs +171 -0
  87. package/server/routes/messages-routes.mjs +433 -68
  88. package/server/routes/preview-routes.mjs +9 -1
  89. package/server/routes/sessions-routes.mjs +901 -253
  90. package/server/server-runtime.mjs +392 -99
  91. package/server/services/about-service.mjs +461 -0
  92. package/server/services/background-task-store.mjs +365 -0
  93. package/server/services/background-work-view.mjs +228 -0
  94. package/server/services/cache-rebuild-service.mjs +30 -0
  95. package/server/services/cli-process-runner.mjs +4 -21
  96. package/server/services/conversation-history-page.mjs +199 -0
  97. package/server/services/delete-archive-service.mjs +4 -1
  98. package/server/services/detached-worker-watch-service.mjs +150 -0
  99. package/server/services/hidden-message-list.mjs +21 -0
  100. package/server/services/host-suspend-command.mjs +2 -1
  101. package/server/services/message-pin-list.mjs +1 -1
  102. package/server/services/preview-health-probe.mjs +8 -1
  103. package/server/services/preview-proxy-server.mjs +8 -4
  104. package/server/services/preview-registry-service.mjs +676 -26
  105. package/server/services/preview-static-handler.mjs +40 -7
  106. package/server/services/preview-store.mjs +274 -0
  107. package/server/services/rate-limit-service.mjs +12 -1
  108. package/server/services/relay-auth.mjs +183 -0
  109. package/server/services/relay-cli-launcher-service.mjs +3 -1
  110. package/server/services/relay-event-service.mjs +17 -0
  111. package/server/services/relay-prompt-sanitizer.mjs +11 -4
  112. package/server/services/remote-relay-dispatcher.mjs +187 -7
  113. package/server/services/request-boundary.mjs +183 -0
  114. package/server/services/session-activity-tracker.mjs +231 -19
  115. package/server/services/session-worker-launch-service.mjs +190 -6
  116. package/server/services/session-worker-process-service.mjs +10 -6
  117. package/server/services/share-access-log-service.mjs +123 -0
  118. package/server/services/share-path-redaction.mjs +424 -0
  119. package/server/services/status-event-service.mjs +7 -3
  120. package/server/services/task-output-rooms.mjs +259 -0
  121. package/server/services/task-output-service.mjs +562 -0
  122. package/server/services/tunnel-worker-path-guard.mjs +24 -41
  123. package/server/services/windows-autostart-service.mjs +6 -1
  124. package/server/services/windows-boot-autostart-service.mjs +207 -23
  125. package/shared/claude-background-command-limit.mjs +83 -0
  126. package/shared/preview-tool-core.mjs +15 -5
  127. package/shared/remote-relay-contract.mjs +35 -3
  128. package/shared/share-path-privacy.mjs +30 -0
  129. package/shared/task-output-text.mjs +737 -0
  130. package/shared/windows-system-tools.mjs +106 -0
  131. package/shared/worker-runtime/background-task-digest.mjs +30 -0
  132. package/shared/worker-runtime/heartbeat.mjs +24 -0
  133. package/shared/worker-runtime/kept-posts.mjs +83 -0
  134. package/shared/worker-runtime/task-output-spool.mjs +181 -0
  135. package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migration: persisted previews.
5
+ *
6
+ * `previews` holds every published preview (a public link to a dev-server port
7
+ * or to a folder the relay serves), so a restarted runtime can restore them and
8
+ * a link handed out before the restart keeps working. One row per token:
9
+ *
10
+ * - `mode` is 'port' or 'static'. A port row has `target_host` and
11
+ * `target_port`, a static row `root_dir` (the folder's realpath).
12
+ * - `conversation_id` is NULL for a preview registered without one.
13
+ * - `created_at` and `last_hit_at` are epoch milliseconds.
14
+ * - `host_boot_id` names the host boot the preview was registered in. A port
15
+ * preview is restored only in the same boot (a dev server does not survive a
16
+ * reboot, and the port may later belong to something else).
17
+ *
18
+ * No foreign key on `conversation_id`: deleting a conversation closes its
19
+ * previews through the registry, and a restore drops rows whose conversation
20
+ * is gone.
21
+ *
22
+ * Additive and idempotent. Safe to run on every boot, and from the command line
23
+ * against a database a running relay has open (one CREATE TABLE in WAL mode).
24
+ * An older build never reads the table.
25
+ *
26
+ * A `previews` table this migration did not create (other columns) is left
27
+ * alone and reported as not compatible; the relay then keeps previews in
28
+ * memory only, it never refuses to start over it.
29
+ *
30
+ * Usage: node server/migrations/0013-previews.mjs [path/to/copilot.db]
31
+ * The file must exist: a wrong path is refused rather than creating an empty
32
+ * database.
33
+ */
34
+
35
+ import fs from 'fs';
36
+ import { fileURLToPath } from 'url';
37
+ import path from 'path';
38
+ import process from 'process';
39
+ import Database from 'better-sqlite3';
40
+
41
+ function tableExists(db, name) {
42
+ return !!db
43
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
44
+ .get(name);
45
+ }
46
+
47
+ export const PREVIEWS_COLUMNS = Object.freeze([
48
+ 'token', 'mode', 'conversation_id', 'label', 'target_host', 'target_port', 'root_dir',
49
+ 'created_at', 'last_hit_at', 'host_boot_id',
50
+ ]);
51
+
52
+ /**
53
+ * Why the `previews` table cannot be used, or '' when it can: every column
54
+ * this build reads and writes is there, and any other column takes a NULL or
55
+ * has a default (an insert would fail otherwise). '' too when the table is
56
+ * missing; the caller checks that separately.
57
+ */
58
+ export function previewsTableProblem(db) {
59
+ const info = db.prepare(`PRAGMA table_info(previews)`).all();
60
+ if (info.length === 0) return '';
61
+ const names = new Set(info.map((column) => column.name));
62
+ const missing = PREVIEWS_COLUMNS.filter((name) => !names.has(name));
63
+ if (missing.length) return `the previews table lacks column(s) ${missing.join(', ')}`;
64
+ const blocking = info.filter((column) => !PREVIEWS_COLUMNS.includes(column.name)
65
+ && column.notnull && column.dflt_value === null && !column.pk);
66
+ if (blocking.length) {
67
+ return `the previews table has required column(s) this build does not write: ${blocking.map((column) => column.name).join(', ')}`;
68
+ }
69
+ return '';
70
+ }
71
+
72
+ export function migratePreviews(db) {
73
+ let tablesAdded = 0;
74
+ if (!tableExists(db, 'previews')) {
75
+ db.exec(`
76
+ CREATE TABLE previews (
77
+ token TEXT PRIMARY KEY,
78
+ mode TEXT NOT NULL,
79
+ conversation_id TEXT,
80
+ label TEXT NOT NULL,
81
+ target_host TEXT,
82
+ target_port INTEGER,
83
+ root_dir TEXT,
84
+ created_at INTEGER NOT NULL,
85
+ last_hit_at INTEGER,
86
+ host_boot_id TEXT
87
+ )
88
+ `);
89
+ tablesAdded += 1;
90
+ }
91
+ const problem = previewsTableProblem(db);
92
+ if (problem) return { applied: false, tablesAdded, compatible: false, problem };
93
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_previews_conversation ON previews(conversation_id)`);
94
+ return { applied: tablesAdded > 0, tablesAdded, compatible: true };
95
+ }
96
+
97
+ function defaultDbPath() {
98
+ const here = path.dirname(fileURLToPath(import.meta.url));
99
+ return path.resolve(here, '..', 'data', 'copilot.db');
100
+ }
101
+
102
+ export function migrate(dbPath, options = {}) {
103
+ if (!fs.existsSync(dbPath)) throw new Error(`no database at ${dbPath}`);
104
+ const db = new Database(dbPath, { fileMustExist: true, ...options });
105
+ try {
106
+ return migratePreviews(db);
107
+ } finally {
108
+ db.close();
109
+ }
110
+ }
111
+
112
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
113
+ if (isMain) {
114
+ const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
115
+ try {
116
+ const result = migrate(dbPath);
117
+ console.log(`Migration 0013 on ${dbPath}`);
118
+ if (!result.compatible) {
119
+ console.error(` not compatible: ${result.problem}; the relay will keep previews in memory only`);
120
+ process.exit(1);
121
+ }
122
+ console.log(result.applied
123
+ ? ` previews: tables added ${result.tablesAdded}`
124
+ : ' skipped: nothing to change');
125
+ process.exit(0);
126
+ } catch (err) {
127
+ console.error(`Migration 0013 failed: ${err?.message || err}`);
128
+ process.exit(1);
129
+ }
130
+ }
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migration: session groups in the sidebar.
5
+ *
6
+ * A group is the set of conversations that share a `group_id`. The table
7
+ * `conversation_groups` holds what the set itself needs: a name given by hand
8
+ * (NULL means "shown under the head's title") and the head, the session that
9
+ * created the others with the remote_relay tool (NULL for a group made by
10
+ * hand, or once the head is deleted). A conversation an agent creates on this
11
+ * relay joins its creator's group at bootstrap; everything else is done by
12
+ * hand in the sidebar. Nothing is backfilled.
13
+ *
14
+ * Additive and idempotent. Safe to run on every boot.
15
+ *
16
+ * Usage: node server/migrations/0014-conversation-groups.mjs [path/to/copilot.db]
17
+ */
18
+
19
+ import { fileURLToPath } from 'url';
20
+ import path from 'path';
21
+ import process from 'process';
22
+ import Database from 'better-sqlite3';
23
+
24
+ function tableExists(db, name) {
25
+ return !!db
26
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
27
+ .get(name);
28
+ }
29
+
30
+ export function migrateConversationGroups(db) {
31
+ if (!tableExists(db, 'conversations')) {
32
+ throw new Error('conversations is missing (the base schema has not run)');
33
+ }
34
+ let tablesAdded = 0;
35
+ if (!tableExists(db, 'conversation_groups')) {
36
+ db.exec(`
37
+ CREATE TABLE conversation_groups (
38
+ id TEXT PRIMARY KEY,
39
+ name TEXT,
40
+ parent_conversation_id TEXT,
41
+ created_at TEXT NOT NULL,
42
+ updated_at TEXT NOT NULL
43
+ )
44
+ `);
45
+ tablesAdded += 1;
46
+ }
47
+ const present = new Set(db.prepare(`PRAGMA table_info(conversations)`).all().map((column) => column.name));
48
+ let columnsAdded = 0;
49
+ if (!present.has('group_id')) {
50
+ db.exec(`ALTER TABLE conversations ADD COLUMN group_id TEXT`);
51
+ columnsAdded += 1;
52
+ }
53
+ db.exec(`
54
+ CREATE INDEX IF NOT EXISTS idx_conversations_group
55
+ ON conversations(group_id)
56
+ WHERE group_id IS NOT NULL
57
+ `);
58
+ return { applied: columnsAdded > 0 || tablesAdded > 0, columnsAdded, tablesAdded };
59
+ }
60
+
61
+ function defaultDbPath() {
62
+ const here = path.dirname(fileURLToPath(import.meta.url));
63
+ return path.resolve(here, '..', 'data', 'copilot.db');
64
+ }
65
+
66
+ export function migrate(dbPath, options = {}) {
67
+ const db = new Database(dbPath, options);
68
+ try {
69
+ return migrateConversationGroups(db);
70
+ } finally {
71
+ db.close();
72
+ }
73
+ }
74
+
75
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
76
+ if (isMain) {
77
+ const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
78
+ try {
79
+ const result = migrate(dbPath);
80
+ console.log(`Migration 0014 on ${dbPath}`);
81
+ console.log(result.applied
82
+ ? ` conversation groups: columns added ${result.columnsAdded}, tables added ${result.tablesAdded}`
83
+ : ' skipped: nothing to change');
84
+ process.exit(0);
85
+ } catch (err) {
86
+ console.error(`Migration 0014 failed: ${err?.message || err}`);
87
+ process.exit(1);
88
+ }
89
+ }
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migration: a hand order inside a session group.
5
+ *
6
+ * `group_position` is a member's place in its group once the owner has
7
+ * dragged a row there: the members with one come first, by position, the
8
+ * others after them, newest first. The head keeps NULL (it always leads),
9
+ * and so does every member of a group nobody reordered. A row that leaves
10
+ * its group loses its position.
11
+ *
12
+ * Additive and idempotent. Safe to run on every boot. Rolling back needs no
13
+ * data change: older code ignores the column; a row it moves out of a group
14
+ * keeps a stale place, which the next run here clears.
15
+ *
16
+ * Usage: node server/migrations/0015-conversation-group-position.mjs [path/to/copilot.db]
17
+ */
18
+
19
+ import { fileURLToPath } from 'url';
20
+ import path from 'path';
21
+ import process from 'process';
22
+ import Database from 'better-sqlite3';
23
+
24
+ function tableExists(db, name) {
25
+ return !!db
26
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
27
+ .get(name);
28
+ }
29
+
30
+ export function migrateConversationGroupPosition(db) {
31
+ if (!tableExists(db, 'conversations')) {
32
+ throw new Error('conversations is missing (the base schema has not run)');
33
+ }
34
+ const present = new Set(db.prepare(`PRAGMA table_info(conversations)`).all().map((column) => column.name));
35
+ let columnsAdded = 0;
36
+ if (!present.has('group_position')) {
37
+ db.exec(`ALTER TABLE conversations ADD COLUMN group_position INTEGER`);
38
+ columnsAdded += 1;
39
+ }
40
+ // A place without a group: an older release (which ignores the column)
41
+ // moved the row out of its group after a roll back. Cleared on every run.
42
+ const placesCleared = present.has('group_id')
43
+ ? db.prepare(`UPDATE conversations SET group_position = NULL WHERE group_id IS NULL AND group_position IS NOT NULL`).run().changes
44
+ : 0;
45
+ return { applied: columnsAdded > 0 || placesCleared > 0, columnsAdded, placesCleared };
46
+ }
47
+
48
+ function defaultDbPath() {
49
+ const here = path.dirname(fileURLToPath(import.meta.url));
50
+ return path.resolve(here, '..', 'data', 'copilot.db');
51
+ }
52
+
53
+ export function migrate(dbPath, options = {}) {
54
+ const db = new Database(dbPath, options);
55
+ try {
56
+ return migrateConversationGroupPosition(db);
57
+ } finally {
58
+ db.close();
59
+ }
60
+ }
61
+
62
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
63
+ if (isMain) {
64
+ const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
65
+ try {
66
+ const result = migrate(dbPath);
67
+ console.log(`Migration 0015 on ${dbPath}`);
68
+ console.log(result.applied
69
+ ? ` group position: columns added ${result.columnsAdded}, stale places cleared ${result.placesCleared}`
70
+ : ' skipped: nothing to change');
71
+ process.exit(0);
72
+ } catch (err) {
73
+ console.error(`Migration 0015 failed: ${err?.message || err}`);
74
+ process.exit(1);
75
+ }
76
+ }
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migration: share lineages and the share access log.
5
+ *
6
+ * A share link can be renewed: the conversation gets a new token and the old
7
+ * one stops at once. What the link has been through (its opens) belongs to the
8
+ * share, not to one token, so every row of `conversation_shares` gets a
9
+ * `lineage_id`: the token the share started with, carried over to every
10
+ * renewed token. Existing rows start their own lineage (`lineage_id = token`).
11
+ *
12
+ * `conversation_share_opens` holds one row per open of a shared page:
13
+ *
14
+ * - `lineage_id` names the share; the log survives a renew.
15
+ * - `opened_at` is an ISO timestamp.
16
+ * - `ip` is NULL when the relay does not store viewer addresses (a setting).
17
+ * - `country` is the edge's two-letter country, NULL when unknown.
18
+ * - `viewer_key` is a keyed hash of address and browser, so distinct viewers
19
+ * can be counted when the address itself is not stored.
20
+ *
21
+ * Stopping a share deletes its log; deleting the conversation does too (the
22
+ * foreign key, and the delete route by hand). Nothing else prunes it.
23
+ *
24
+ * Additive and idempotent. Safe to run on every boot. An older build never
25
+ * reads the new table, and a share it creates has no lineage, which this build
26
+ * reads as the share's own token.
27
+ *
28
+ * Usage: node server/migrations/0016-share-access-log.mjs [path/to/copilot.db]
29
+ */
30
+
31
+ import { fileURLToPath } from 'url';
32
+ import path from 'path';
33
+ import process from 'process';
34
+ import Database from 'better-sqlite3';
35
+
36
+ function tableExists(db, name) {
37
+ return !!db
38
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
39
+ .get(name);
40
+ }
41
+
42
+ export function migrateShareAccessLog(db) {
43
+ if (!tableExists(db, 'conversation_shares')) {
44
+ throw new Error('conversation_shares is missing (the base schema has not run)');
45
+ }
46
+ let columnsAdded = 0;
47
+ let tablesAdded = 0;
48
+ const present = new Set(db.prepare(`PRAGMA table_info(conversation_shares)`).all().map((column) => column.name));
49
+ if (!present.has('lineage_id')) {
50
+ db.exec(`ALTER TABLE conversation_shares ADD COLUMN lineage_id TEXT`);
51
+ columnsAdded += 1;
52
+ }
53
+ // Every run: a share an older build created after this migration has none.
54
+ const lineagesSet = db.prepare(`UPDATE conversation_shares SET lineage_id = token WHERE lineage_id IS NULL OR lineage_id = ''`).run().changes;
55
+ if (!tableExists(db, 'conversation_share_opens')) {
56
+ db.exec(`
57
+ CREATE TABLE conversation_share_opens (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ lineage_id TEXT NOT NULL,
60
+ conversation_id TEXT NOT NULL,
61
+ opened_at TEXT NOT NULL,
62
+ ip TEXT,
63
+ user_agent TEXT,
64
+ country TEXT,
65
+ viewer_key TEXT NOT NULL,
66
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
67
+ )
68
+ `);
69
+ tablesAdded += 1;
70
+ }
71
+ db.exec(`
72
+ CREATE INDEX IF NOT EXISTS idx_conversation_share_opens_lineage
73
+ ON conversation_share_opens(lineage_id, opened_at DESC)
74
+ `);
75
+ db.exec(`
76
+ CREATE INDEX IF NOT EXISTS idx_conversation_share_opens_conversation
77
+ ON conversation_share_opens(conversation_id)
78
+ `);
79
+ return { applied: columnsAdded > 0 || tablesAdded > 0 || lineagesSet > 0, columnsAdded, tablesAdded, lineagesSet };
80
+ }
81
+
82
+ function defaultDbPath() {
83
+ const here = path.dirname(fileURLToPath(import.meta.url));
84
+ return path.resolve(here, '..', 'data', 'copilot.db');
85
+ }
86
+
87
+ export function migrate(dbPath, options = {}) {
88
+ const db = new Database(dbPath, options);
89
+ try {
90
+ return migrateShareAccessLog(db);
91
+ } finally {
92
+ db.close();
93
+ }
94
+ }
95
+
96
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
97
+ if (isMain) {
98
+ const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
99
+ try {
100
+ const result = migrate(dbPath);
101
+ console.log(`Migration 0016 on ${dbPath}`);
102
+ console.log(result.applied
103
+ ? ` share access log: columns added ${result.columnsAdded}, tables added ${result.tablesAdded}, lineages set ${result.lineagesSet}`
104
+ : ' skipped: nothing to change');
105
+ process.exit(0);
106
+ } catch (err) {
107
+ console.error(`Migration 0016 failed: ${err?.message || err}`);
108
+ process.exit(1);
109
+ }
110
+ }
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migration: two indexes for loading the history of long conversations.
5
+ *
6
+ * - `idx_queue_response_message` on `queue(response_message_id)`: the
7
+ * sub-agent records of a reply are found through the queue row that
8
+ * produced it, one lookup per assistant message on every history page.
9
+ * Without the index each lookup scanned the whole queue.
10
+ * - `idx_messages_conv_order` on `messages(conversation_id, timestamp, id)`:
11
+ * a history page is selected in SQL in conversation order (timestamp, then
12
+ * id); the index serves that order and the check that a conversation's
13
+ * rows can be paged that way, without reading the message rows.
14
+ *
15
+ * Additive and idempotent. Safe to run on every boot. An older build ignores
16
+ * both indexes.
17
+ *
18
+ * Usage: node server/migrations/0017-conversation-history-indexes.mjs [path/to/copilot.db]
19
+ */
20
+
21
+ import { fileURLToPath } from 'url';
22
+ import path from 'path';
23
+ import process from 'process';
24
+ import Database from 'better-sqlite3';
25
+
26
+ function tableExists(db, name) {
27
+ return !!db
28
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
29
+ .get(name);
30
+ }
31
+
32
+ function indexExists(db, name) {
33
+ return !!db
34
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?`)
35
+ .get(name);
36
+ }
37
+
38
+ function columnNames(db, table) {
39
+ return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
40
+ }
41
+
42
+ export function migrateConversationHistoryIndexes(db) {
43
+ if (!tableExists(db, 'messages') || !tableExists(db, 'queue')) {
44
+ throw new Error('messages or queue is missing (the base schema has not run)');
45
+ }
46
+ if (!columnNames(db, 'queue').has('response_message_id')) {
47
+ throw new Error('queue.response_message_id is missing (the base schema has not run)');
48
+ }
49
+ let indexesAdded = 0;
50
+ if (!indexExists(db, 'idx_queue_response_message')) {
51
+ db.exec(`CREATE INDEX idx_queue_response_message ON queue(response_message_id)`);
52
+ indexesAdded += 1;
53
+ }
54
+ if (!indexExists(db, 'idx_messages_conv_order')) {
55
+ db.exec(`CREATE INDEX idx_messages_conv_order ON messages(conversation_id, timestamp, id)`);
56
+ indexesAdded += 1;
57
+ }
58
+ return { applied: indexesAdded > 0, indexesAdded };
59
+ }
60
+
61
+ function defaultDbPath() {
62
+ const here = path.dirname(fileURLToPath(import.meta.url));
63
+ return path.resolve(here, '..', 'data', 'copilot.db');
64
+ }
65
+
66
+ export function migrate(dbPath, options = {}) {
67
+ const db = new Database(dbPath, options);
68
+ try {
69
+ return migrateConversationHistoryIndexes(db);
70
+ } finally {
71
+ db.close();
72
+ }
73
+ }
74
+
75
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
76
+ if (isMain) {
77
+ const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
78
+ try {
79
+ const result = migrate(dbPath);
80
+ console.log(`Migration 0017 on ${dbPath}`);
81
+ console.log(result.applied
82
+ ? ` conversation history indexes: added ${result.indexesAdded}`
83
+ : ' skipped: nothing to change');
84
+ process.exit(0);
85
+ } catch (err) {
86
+ console.error(`Migration 0017 failed: ${err?.message || err}`);
87
+ process.exit(1);
88
+ }
89
+ }
@@ -0,0 +1,73 @@
1
+ // ⋯ → About OAR: the menu entry's label and the modal (about-view.mjs builds
2
+ // its HTML from GET /api/about).
3
+
4
+ import {
5
+ closeSummaryModal,
6
+ escHtml,
7
+ getUpdateState,
8
+ openSummaryModal,
9
+ renderSummaryModalContent,
10
+ setSummaryModalLoading,
11
+ } from './store.js';
12
+ import { apiFetchOrThrow } from './api-client.js';
13
+ import { openSettingsModal } from './settings-modal.js';
14
+ import { aboutMenuLabel, buildAboutModalHtml } from './about-view.mjs';
15
+
16
+ const ABOUT_KIND = 'about';
17
+ const ABOUT_TITLE = 'About OAR';
18
+
19
+ /** The entry carries the running version; /api/status brings it on load. */
20
+ export function syncAboutMenuLabel(version = getUpdateState()?.runningVersion) {
21
+ const button = document.getElementById('chat-menu-about');
22
+ if (button) button.textContent = aboutMenuLabel(version);
23
+ }
24
+
25
+ function bindAboutModalActions() {
26
+ const body = document.getElementById('summary-modal-body');
27
+ const button = body?.querySelector('[data-about-open-settings]');
28
+ if (!button) return;
29
+ button.addEventListener('click', (event) => {
30
+ event.preventDefault();
31
+ closeSummaryModal();
32
+ openSettingsModal('general');
33
+ document.getElementById('update-auto-check-toggle')?.scrollIntoView?.({ block: 'center' });
34
+ });
35
+ }
36
+
37
+ async function loadAboutAndRender() {
38
+ const about = await apiFetchOrThrow('/api/about');
39
+ if (!about) throw new Error('The relay is not reachable right now.');
40
+ renderSummaryModalContent({
41
+ title: ABOUT_TITLE,
42
+ subtitle: about.version ? `Version ${about.version}` : '',
43
+ bodyHtml: buildAboutModalHtml(about),
44
+ refresh: loadAboutAndRender,
45
+ kind: ABOUT_KIND,
46
+ });
47
+ bindAboutModalActions();
48
+ syncAboutMenuLabel(about.version);
49
+ }
50
+
51
+ export async function showAbout() {
52
+ openSummaryModal({
53
+ title: ABOUT_TITLE,
54
+ subtitle: 'Loading…',
55
+ bodyHtml: '<div class="summary-loading">Reading the relay…</div>',
56
+ refresh: loadAboutAndRender,
57
+ kind: ABOUT_KIND,
58
+ });
59
+ setSummaryModalLoading(true);
60
+ try {
61
+ await loadAboutAndRender();
62
+ } catch (error) {
63
+ renderSummaryModalContent({
64
+ title: ABOUT_TITLE,
65
+ subtitle: 'Unable to load',
66
+ bodyHtml: `<div class="summary-error">${escHtml(error?.message || 'Unknown error')}</div>`,
67
+ refresh: loadAboutAndRender,
68
+ kind: ABOUT_KIND,
69
+ });
70
+ } finally {
71
+ setSummaryModalLoading(false);
72
+ }
73
+ }