@tt-a1i/hive 1.7.0 → 2.0.2

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 (251) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.en.md +73 -11
  3. package/README.md +41 -8
  4. package/dist/src/cli/hive-remote.d.ts +46 -0
  5. package/dist/src/cli/hive-remote.js +257 -0
  6. package/dist/src/cli/hive-update.js +7 -2
  7. package/dist/src/cli/hive.d.ts +6 -0
  8. package/dist/src/cli/hive.js +64 -0
  9. package/dist/src/cli/team.d.ts +22 -0
  10. package/dist/src/cli/team.js +255 -5
  11. package/dist/src/server/agent-command-resolver.js +10 -3
  12. package/dist/src/server/agent-exit-classification.d.ts +6 -0
  13. package/dist/src/server/agent-exit-classification.js +6 -0
  14. package/dist/src/server/agent-manager-support.d.ts +2 -1
  15. package/dist/src/server/agent-manager-support.js +59 -15
  16. package/dist/src/server/agent-manager.d.ts +3 -0
  17. package/dist/src/server/agent-manager.js +22 -7
  18. package/dist/src/server/agent-run-bootstrap.d.ts +14 -0
  19. package/dist/src/server/agent-run-bootstrap.js +11 -4
  20. package/dist/src/server/agent-run-exit-handler.js +14 -8
  21. package/dist/src/server/agent-run-starter.d.ts +3 -1
  22. package/dist/src/server/agent-run-starter.js +22 -5
  23. package/dist/src/server/agent-run-sync.js +13 -5
  24. package/dist/src/server/agent-runtime-types.d.ts +1 -0
  25. package/dist/src/server/agent-runtime.d.ts +2 -1
  26. package/dist/src/server/agent-runtime.js +9 -2
  27. package/dist/src/server/agent-startup-instructions.d.ts +2 -1
  28. package/dist/src/server/agent-startup-instructions.js +8 -4
  29. package/dist/src/server/agent-stdin-dispatcher.d.ts +4 -2
  30. package/dist/src/server/agent-stdin-dispatcher.js +35 -3
  31. package/dist/src/server/command-preset-defaults.d.ts +6 -1
  32. package/dist/src/server/command-preset-defaults.js +56 -0
  33. package/dist/src/server/fs-browse.d.ts +2 -0
  34. package/dist/src/server/fs-browse.js +165 -31
  35. package/dist/src/server/fs-pick-folder.js +6 -69
  36. package/dist/src/server/fs-sandbox.d.ts +5 -3
  37. package/dist/src/server/fs-sandbox.js +5 -3
  38. package/dist/src/server/hive-team-guidance.js +18 -6
  39. package/dist/src/server/machine-name.d.ts +2 -0
  40. package/dist/src/server/machine-name.js +13 -0
  41. package/dist/src/server/open-target-commands.d.ts +1 -0
  42. package/dist/src/server/open-target-commands.js +4 -1
  43. package/dist/src/server/orchestrator-autostart.js +1 -1
  44. package/dist/src/server/platform-path.d.ts +1 -0
  45. package/dist/src/server/platform-path.js +14 -1
  46. package/dist/src/server/post-start-input-writer.js +50 -13
  47. package/dist/src/server/preset-launch-support.js +1 -0
  48. package/dist/src/server/recovery-summary.d.ts +2 -1
  49. package/dist/src/server/recovery-summary.js +2 -1
  50. package/dist/src/server/remote-audit-store.d.ts +51 -0
  51. package/dist/src/server/remote-audit-store.js +108 -0
  52. package/dist/src/server/remote-config-keys.d.ts +17 -0
  53. package/dist/src/server/remote-config-keys.js +27 -0
  54. package/dist/src/server/remote-control-constants.d.ts +30 -0
  55. package/dist/src/server/remote-control-constants.js +29 -0
  56. package/dist/src/server/remote-device-session.d.ts +40 -0
  57. package/dist/src/server/remote-device-session.js +22 -0
  58. package/dist/src/server/remote-device-store.d.ts +36 -0
  59. package/dist/src/server/remote-device-store.js +67 -0
  60. package/dist/src/server/remote-frame-bridge.d.ts +102 -0
  61. package/dist/src/server/remote-frame-bridge.js +791 -0
  62. package/dist/src/server/remote-gateway-client.d.ts +14 -0
  63. package/dist/src/server/remote-gateway-client.js +36 -0
  64. package/dist/src/server/remote-loopback-auth.d.ts +6 -0
  65. package/dist/src/server/remote-loopback-auth.js +112 -0
  66. package/dist/src/server/remote-pairing-tunnel.d.ts +59 -0
  67. package/dist/src/server/remote-pairing-tunnel.js +146 -0
  68. package/dist/src/server/remote-pairing.d.ts +58 -0
  69. package/dist/src/server/remote-pairing.js +237 -0
  70. package/dist/src/server/remote-tunnel.d.ts +113 -0
  71. package/dist/src/server/remote-tunnel.js +514 -0
  72. package/dist/src/server/restart-policy-support.d.ts +4 -1
  73. package/dist/src/server/restart-policy-support.js +3 -1
  74. package/dist/src/server/restart-policy.d.ts +1 -1
  75. package/dist/src/server/restart-policy.js +19 -3
  76. package/dist/src/server/route-types.d.ts +1 -1
  77. package/dist/src/server/routes-dispatches.js +1 -1
  78. package/dist/src/server/routes-fs.js +3 -3
  79. package/dist/src/server/routes-marketplace.js +2 -2
  80. package/dist/src/server/routes-open-workspace.js +1 -1
  81. package/dist/src/server/routes-remote.d.ts +2 -0
  82. package/dist/src/server/routes-remote.js +166 -0
  83. package/dist/src/server/routes-runtime.js +6 -6
  84. package/dist/src/server/routes-settings.js +16 -16
  85. package/dist/src/server/routes-tasks.js +2 -2
  86. package/dist/src/server/routes-team-memory.d.ts +2 -0
  87. package/dist/src/server/routes-team-memory.js +154 -0
  88. package/dist/src/server/routes-team-recall.d.ts +2 -0
  89. package/dist/src/server/routes-team-recall.js +119 -0
  90. package/dist/src/server/routes-team.js +31 -9
  91. package/dist/src/server/routes-ui.js +11 -1
  92. package/dist/src/server/routes-workflow-schedules.js +3 -3
  93. package/dist/src/server/routes-workflows.js +5 -5
  94. package/dist/src/server/routes-workspace-memory-dreams.d.ts +2 -0
  95. package/dist/src/server/routes-workspace-memory-dreams.js +105 -0
  96. package/dist/src/server/routes-workspace-memory.d.ts +2 -0
  97. package/dist/src/server/routes-workspace-memory.js +215 -0
  98. package/dist/src/server/routes-workspaces.js +9 -9
  99. package/dist/src/server/routes.js +10 -0
  100. package/dist/src/server/runtime-database.d.ts +1 -0
  101. package/dist/src/server/runtime-database.js +27 -2
  102. package/dist/src/server/runtime-restart-policy.d.ts +3 -1
  103. package/dist/src/server/runtime-restart-policy.js +2 -1
  104. package/dist/src/server/runtime-store-contract.d.ts +37 -0
  105. package/dist/src/server/runtime-store-dream.d.ts +23 -0
  106. package/dist/src/server/runtime-store-dream.js +16 -0
  107. package/dist/src/server/runtime-store-helpers.d.ts +20 -0
  108. package/dist/src/server/runtime-store-helpers.js +81 -7
  109. package/dist/src/server/runtime-store-memory.d.ts +33 -0
  110. package/dist/src/server/runtime-store-memory.js +37 -0
  111. package/dist/src/server/runtime-store-remote.d.ts +5 -0
  112. package/dist/src/server/runtime-store-remote.js +45 -0
  113. package/dist/src/server/runtime-store-workflows.js +2 -0
  114. package/dist/src/server/runtime-store.js +14 -3
  115. package/dist/src/server/session-capture-claude.d.ts +1 -1
  116. package/dist/src/server/session-capture-claude.js +7 -4
  117. package/dist/src/server/session-capture-codex.js +4 -5
  118. package/dist/src/server/session-capture-gemini.js +4 -5
  119. package/dist/src/server/session-capture-opencode.d.ts +4 -4
  120. package/dist/src/server/session-capture-opencode.js +20 -12
  121. package/dist/src/server/session-capture-qwen.d.ts +5 -0
  122. package/dist/src/server/session-capture-qwen.js +104 -0
  123. package/dist/src/server/session-capture.d.ts +17 -0
  124. package/dist/src/server/session-capture.js +16 -0
  125. package/dist/src/server/sqlite-schema-v23.d.ts +2 -0
  126. package/dist/src/server/sqlite-schema-v23.js +43 -0
  127. package/dist/src/server/sqlite-schema-v24.d.ts +2 -0
  128. package/dist/src/server/sqlite-schema-v24.js +34 -0
  129. package/dist/src/server/sqlite-schema-v25.d.ts +2 -0
  130. package/dist/src/server/sqlite-schema-v25.js +127 -0
  131. package/dist/src/server/sqlite-schema-v26.d.ts +2 -0
  132. package/dist/src/server/sqlite-schema-v26.js +56 -0
  133. package/dist/src/server/sqlite-schema-v27.d.ts +6 -0
  134. package/dist/src/server/sqlite-schema-v27.js +92 -0
  135. package/dist/src/server/sqlite-schema-v28.d.ts +2 -0
  136. package/dist/src/server/sqlite-schema-v28.js +19 -0
  137. package/dist/src/server/sqlite-schema-v29.d.ts +2 -0
  138. package/dist/src/server/sqlite-schema-v29.js +27 -0
  139. package/dist/src/server/sqlite-schema-v30.d.ts +2 -0
  140. package/dist/src/server/sqlite-schema-v30.js +27 -0
  141. package/dist/src/server/sqlite-schema-v31.d.ts +2 -0
  142. package/dist/src/server/sqlite-schema-v31.js +30 -0
  143. package/dist/src/server/sqlite-schema.d.ts +1 -1
  144. package/dist/src/server/sqlite-schema.js +49 -1
  145. package/dist/src/server/startup-command-parser.js +5 -1
  146. package/dist/src/server/tasks-file-watcher.d.ts +2 -0
  147. package/dist/src/server/tasks-file-watcher.js +15 -6
  148. package/dist/src/server/tasks-file.js +30 -5
  149. package/dist/src/server/tasks-websocket-server.js +4 -0
  150. package/dist/src/server/team-authz.d.ts +1 -1
  151. package/dist/src/server/team-authz.js +13 -1
  152. package/dist/src/server/team-list-enrichment.js +3 -1
  153. package/dist/src/server/team-memory-digest.d.ts +52 -0
  154. package/dist/src/server/team-memory-digest.js +200 -0
  155. package/dist/src/server/team-memory-dream-applier.d.ts +5 -0
  156. package/dist/src/server/team-memory-dream-applier.js +234 -0
  157. package/dist/src/server/team-memory-dream-http-serializers.d.ts +13 -0
  158. package/dist/src/server/team-memory-dream-http-serializers.js +12 -0
  159. package/dist/src/server/team-memory-dream-ops.d.ts +40 -0
  160. package/dist/src/server/team-memory-dream-ops.js +153 -0
  161. package/dist/src/server/team-memory-dream-reverter.d.ts +22 -0
  162. package/dist/src/server/team-memory-dream-reverter.js +221 -0
  163. package/dist/src/server/team-memory-dream-run-store.d.ts +23 -0
  164. package/dist/src/server/team-memory-dream-run-store.js +211 -0
  165. package/dist/src/server/team-memory-dream-runner.d.ts +37 -0
  166. package/dist/src/server/team-memory-dream-runner.js +178 -0
  167. package/dist/src/server/team-memory-dream-scheduler.d.ts +32 -0
  168. package/dist/src/server/team-memory-dream-scheduler.js +115 -0
  169. package/dist/src/server/team-memory-dream-store.d.ts +19 -0
  170. package/dist/src/server/team-memory-dream-store.js +16 -0
  171. package/dist/src/server/team-memory-dream-types.d.ts +104 -0
  172. package/dist/src/server/team-memory-dream-types.js +23 -0
  173. package/dist/src/server/team-memory-export.d.ts +22 -0
  174. package/dist/src/server/team-memory-export.js +220 -0
  175. package/dist/src/server/team-memory-feature.d.ts +12 -0
  176. package/dist/src/server/team-memory-feature.js +12 -0
  177. package/dist/src/server/team-memory-http-serializers.d.ts +102 -0
  178. package/dist/src/server/team-memory-http-serializers.js +46 -0
  179. package/dist/src/server/team-memory-injection.d.ts +31 -0
  180. package/dist/src/server/team-memory-injection.js +49 -0
  181. package/dist/src/server/team-memory-store.d.ts +116 -0
  182. package/dist/src/server/team-memory-store.js +513 -0
  183. package/dist/src/server/team-operations.d.ts +5 -1
  184. package/dist/src/server/team-operations.js +46 -16
  185. package/dist/src/server/team-recall-store.d.ts +38 -0
  186. package/dist/src/server/team-recall-store.js +205 -0
  187. package/dist/src/server/terminal-input-profile.d.ts +1 -1
  188. package/dist/src/server/terminal-input-profile.js +18 -0
  189. package/dist/src/server/terminal-ws-server.js +6 -0
  190. package/dist/src/server/ui-auth-helpers.d.ts +1 -1
  191. package/dist/src/server/ui-auth-helpers.js +7 -1
  192. package/dist/src/server/ui-auth.d.ts +3 -0
  193. package/dist/src/server/ui-auth.js +21 -1
  194. package/dist/src/server/workflow-cli-policy.d.ts +2 -3
  195. package/dist/src/server/workflow-cli-policy.js +3 -3
  196. package/dist/src/server/workflow-runner.d.ts +1 -0
  197. package/dist/src/server/workflow-runner.js +9 -4
  198. package/dist/src/server/workspace-path-validation.js +6 -2
  199. package/dist/src/server/workspace-store.d.ts +1 -1
  200. package/dist/src/server/workspace-store.js +35 -9
  201. package/dist/src/shared/fs-browse.d.ts +1 -0
  202. package/dist/src/shared/fs-browse.js +1 -0
  203. package/dist/src/shared/path-input.d.ts +12 -0
  204. package/dist/src/shared/path-input.js +22 -0
  205. package/dist/src/shared/remote-bridge-routing.d.ts +19 -0
  206. package/dist/src/shared/remote-bridge-routing.js +141 -0
  207. package/dist/src/shared/remote-crypto.d.ts +138 -0
  208. package/dist/src/shared/remote-crypto.js +427 -0
  209. package/dist/src/shared/remote-pairing-code.d.ts +7 -0
  210. package/dist/src/shared/remote-pairing-code.js +47 -0
  211. package/dist/src/shared/remote-protocol.d.ts +160 -0
  212. package/dist/src/shared/remote-protocol.js +526 -0
  213. package/dist/src/shared/team-memory.d.ts +11 -0
  214. package/dist/src/shared/team-memory.js +10 -0
  215. package/dist/src/shared/team-recall.d.ts +1 -0
  216. package/dist/src/shared/team-recall.js +1 -0
  217. package/dist/src/shared/types.d.ts +4 -5
  218. package/package.json +12 -5
  219. package/scripts/postinstall-native-artifacts.mjs +113 -0
  220. package/web/dist/assets/AddWorkerDialog-CbV75qUX.js +2 -0
  221. package/web/dist/assets/AddWorkspaceFlow-CwV-7wPx.js +1 -0
  222. package/web/dist/assets/FirstRunWizard-a6PWIK3x.js +1 -0
  223. package/web/dist/assets/MarketplaceDrawer-Dd8WIA8T.js +67 -0
  224. package/web/dist/assets/TaskGraphDrawer-Bk5WFIk_.js +1 -0
  225. package/web/dist/assets/{WhatsNewDialog-CHkZeINH.js → WhatsNewDialog-C2VZaip0.js} +1 -1
  226. package/web/dist/assets/WorkerModal-DucW-9YT.js +1 -0
  227. package/web/dist/assets/WorkflowsDrawer-Bjf4olbR.js +1 -0
  228. package/web/dist/assets/WorkspaceMemoryDrawer-DglCy_5f.js +1 -0
  229. package/web/dist/assets/WorkspaceTaskDrawer-BIWwISvA.js +1 -0
  230. package/web/dist/assets/index-BAiLYajK.css +1 -0
  231. package/web/dist/assets/index-BV2k9Dts.js +73 -0
  232. package/web/dist/assets/search-Bk2HQvO7.js +1 -0
  233. package/web/dist/assets/square-terminal-D93m9hfY.js +1 -0
  234. package/web/dist/cli-icons/agy.png +0 -0
  235. package/web/dist/cli-icons/cursor.ico +0 -0
  236. package/web/dist/cli-icons/grok.ico +0 -0
  237. package/web/dist/cli-icons/qwen.png +0 -0
  238. package/web/dist/index.html +8 -3
  239. package/web/dist/sw.js +1 -1
  240. package/scripts/fix-runtime-artifacts.mjs +0 -33
  241. package/web/dist/assets/AddWorkerDialog-BRUxpa3f.js +0 -2
  242. package/web/dist/assets/AddWorkspaceDialog-D56x5JCb.js +0 -1
  243. package/web/dist/assets/FirstRunWizard-BFVaMIsE.js +0 -1
  244. package/web/dist/assets/MarketplaceDrawer-DeEZ35dN.js +0 -76
  245. package/web/dist/assets/WorkerModal-BBCuMLIa.js +0 -1
  246. package/web/dist/assets/WorkspaceTaskDrawer-CpZHAcj1.js +0 -1
  247. package/web/dist/assets/WorkspaceTerminalPanels-7If2mDyp.js +0 -1
  248. package/web/dist/assets/WorkspaceTerminalPanels-DDGTF8rc.css +0 -1
  249. package/web/dist/assets/index-5zh61jMg.css +0 -1
  250. package/web/dist/assets/index-CxNL0O-C.js +0 -73
  251. package/web/dist/assets/path-join-7MR1s7b1.js +0 -1
@@ -0,0 +1,2 @@
1
+ export declare function cleanMachineName(raw: string): string | null;
2
+ export declare function getMachineName(): string | null;
@@ -0,0 +1,13 @@
1
+ import { hostname } from 'node:os';
2
+ const MAX_LEN = 64;
3
+ // Trim surrounding whitespace, strip a trailing '.local' suffix (common on macOS),
4
+ // and cap at 64 chars. Returns null if the result is empty.
5
+ export function cleanMachineName(raw) {
6
+ const cleaned = raw.trim().replace(/\.local$/i, '');
7
+ if (cleaned.length === 0)
8
+ return null;
9
+ return cleaned.slice(0, MAX_LEN);
10
+ }
11
+ export function getMachineName() {
12
+ return cleanMachineName(hostname());
13
+ }
@@ -6,6 +6,7 @@ export declare const resolveOpenTargetPlatform: (platform: NodeJS.Platform) => O
6
6
  export interface OpenAttempt {
7
7
  command: string;
8
8
  args: string[];
9
+ options?: ExecFileOptions;
9
10
  }
10
11
  /**
11
12
  * Returns the ordered list of commands to try. First success wins; remaining
@@ -58,6 +58,7 @@ const linuxAttempts = (targetId, path) => {
58
58
  const cmdExeShimAttempt = (bin, path) => ({
59
59
  command: 'cmd.exe',
60
60
  args: ['/d', '/s', '/c', buildCmdCallCommand(bin, [path])],
61
+ options: { windowsHide: true },
61
62
  });
62
63
  const windowsAttempts = (targetId, path) => {
63
64
  switch (targetId) {
@@ -123,6 +124,8 @@ const APP_NOT_INSTALLED_PATTERNS = [
123
124
  const classifyFailure = (result) => {
124
125
  if (result.spawnError?.code === 'ENOENT')
125
126
  return 'command-not-in-path';
127
+ if (result.status === 9009)
128
+ return 'app-not-installed';
126
129
  const stderr = result.stderr.toLowerCase();
127
130
  if (APP_NOT_INSTALLED_PATTERNS.some((re) => re.test(stderr)))
128
131
  return 'app-not-installed';
@@ -168,7 +171,7 @@ export const openWorkspace = async (input, options = {}) => {
168
171
  const attempts = buildOpenAttempts(input.targetId, input.path, platform);
169
172
  let lastFailure = null;
170
173
  for (const attempt of attempts) {
171
- const result = await run(attempt.command, attempt.args, {});
174
+ const result = await run(attempt.command, attempt.args, attempt.options ?? {});
172
175
  // Windows `explorer.exe` returns exit code 1 even on success — checking
173
176
  // exit code here would surface a spurious error to the user on every
174
177
  // File Explorer open. spawnError still catches the "explorer not on PATH"
@@ -4,7 +4,7 @@ import { getStartupCommandExecutable } from './startup-command-parser.js';
4
4
  // "command not found" then dies with exit 127 (POSIX) or 9009 (Windows)
5
5
  // — typically <100ms in practice. 800ms balances reliability vs the perceived
6
6
  // workspace-create latency cost.
7
- const SETTLE_WAIT_MS = 800;
7
+ const SETTLE_WAIT_MS = process.platform === 'win32' ? 2000 : 800;
8
8
  const POLL_INTERVAL_MS = 25;
9
9
  // Shells emit a "command not found" exit code when the requested binary is
10
10
  // missing on PATH. node-pty does NOT raise a synchronous spawn error for that
@@ -1,3 +1,4 @@
1
+ export declare const expandHomePath: (path: string) => string;
1
2
  export declare const arePathsEqual: (left: string, right: string, platform?: NodeJS.Platform) => boolean;
2
3
  export declare const containsPathMarker: (haystack: string, marker: string, platform?: NodeJS.Platform) => boolean;
3
4
  export declare const indexOfPathMarker: (haystack: string, marker: string, platform?: NodeJS.Platform) => number;
@@ -1,5 +1,18 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
1
3
  const toForwardSlashes = (path) => path.replace(/\\/g, '/');
2
4
  const normalizeForWin32 = (path) => toForwardSlashes(path).toLowerCase();
5
+ export const expandHomePath = (path) => {
6
+ if (path === '~')
7
+ return homedir();
8
+ const match = /^~[\\/](.*)$/u.exec(path);
9
+ if (!match)
10
+ return path;
11
+ const rest = match[1] ?? '';
12
+ if (!rest)
13
+ return homedir();
14
+ return join(homedir(), ...rest.split(/[\\/]+/u).filter(Boolean));
15
+ };
3
16
  export const arePathsEqual = (left, right, platform = process.platform) => {
4
17
  if (platform === 'win32')
5
18
  return normalizeForWin32(left) === normalizeForWin32(right);
@@ -8,6 +21,6 @@ export const arePathsEqual = (left, right, platform = process.platform) => {
8
21
  export const containsPathMarker = (haystack, marker, platform = process.platform) => indexOfPathMarker(haystack, marker, platform) !== -1;
9
22
  export const indexOfPathMarker = (haystack, marker, platform = process.platform) => {
10
23
  if (platform === 'win32')
11
- return toForwardSlashes(haystack).indexOf(marker);
24
+ return normalizeForWin32(haystack).indexOf(normalizeForWin32(marker));
12
25
  return haystack.indexOf(marker);
13
26
  };
@@ -1,5 +1,5 @@
1
+ import { BUILTIN_INTERACTIVE_COMMANDS } from './command-preset-defaults.js';
1
2
  import { normalizeExecutableToken } from './startup-command-parser.js';
2
- const INTERACTIVE_COMMANDS = new Set(['claude', 'codex', 'gemini', 'hermes', 'opencode']);
3
3
  const READY_CHECK_INTERVAL_MS = 50;
4
4
  const READY_TIMEOUT_MS = 3000;
5
5
  const MIN_SUBMIT_AFTER_PASTE_DELAY_MS = 600;
@@ -19,25 +19,57 @@ const CODEX_PASTE_ACK_TIMEOUT_MS = 10000;
19
19
  // timeout before Enter is sent.
20
20
  const CODEX_PASTE_ACK_MIN_CHARS = 2000;
21
21
  const CODEX_SUBMIT_RETRY_DELAY_MS = 500;
22
- const COMMANDS_WITH_BRACKETED_PASTE = new Set(['claude', 'codex', 'hermes', 'opencode']);
22
+ const GROK_SUBMIT_AFTER_PASTE_DELAY_MS = 100;
23
+ const COMMANDS_WITH_BRACKETED_PASTE = new Set([
24
+ 'agy',
25
+ 'claude',
26
+ 'codex',
27
+ 'grok',
28
+ 'hermes',
29
+ 'opencode',
30
+ ]);
23
31
  const COMMANDS_WAITING_FOR_PASTE_ACK = new Set(['claude', 'codex']);
24
32
  const BRACKETED_PASTE_END = '\u001b[201~';
25
33
  const PASTE_ACK_PATTERN = /(?:^|[\r\n])\s*(?:[❯›]\s*)?\[(?:Pasted text #\d+[^\]]*|Pasted Content [\d,]+ chars)\]/u;
26
34
  const PASTE_ACK_ONLY_DELTA_PATTERN = /^[\s\r\n]*(?:[❯›]\s*)?\[(?:Pasted text #\d+[^\]]*|Pasted Content [\d,]+ chars)\]\s*(?:[❯›]\s*)?$/u;
35
+ const ESCAPE = String.fromCharCode(27);
36
+ const BELL = String.fromCharCode(7);
37
+ const TERMINAL_CONTROL_PATTERN = new RegExp(`${ESCAPE}\\[[0-?]*[ -/]*[@-~]|${ESCAPE}\\][^${BELL}${ESCAPE}]*(?:${BELL}|${ESCAPE}\\\\)`, 'gu');
27
38
  export const toBracketedPasteSubmission = (text) => `\u001b[200~${text}\u001b[201~`;
28
39
  const toError = (error) => (error instanceof Error ? error : new Error(String(error)));
29
40
  const createRunInactiveError = (runId) => new Error(`Run became inactive before input was submitted: ${runId}`);
30
- const getSubmitAfterPasteDelayMs = (text) => Math.min(MAX_SUBMIT_AFTER_PASTE_DELAY_MS, Math.max(MIN_SUBMIT_AFTER_PASTE_DELAY_MS, Math.ceil(text.length / PASTE_CHARS_PER_DELAY_MS)));
41
+ const getSubmitAfterPasteDelayMs = (command, text) => {
42
+ if (getCommandName(command) === 'grok')
43
+ return GROK_SUBMIT_AFTER_PASTE_DELAY_MS;
44
+ return Math.min(MAX_SUBMIT_AFTER_PASTE_DELAY_MS, Math.max(MIN_SUBMIT_AFTER_PASTE_DELAY_MS, Math.ceil(text.length / PASTE_CHARS_PER_DELAY_MS)));
45
+ };
46
+ const getCommandName = (command) => normalizeExecutableToken(command) ?? '';
31
47
  export const isInteractiveAgentCommand = (command) => {
32
48
  const brand = normalizeExecutableToken(command);
33
- return brand !== null && INTERACTIVE_COMMANDS.has(brand);
49
+ return brand !== null && BUILTIN_INTERACTIVE_COMMANDS.has(brand);
34
50
  };
35
- const getCommandName = (command) => normalizeExecutableToken(command) ?? '';
36
51
  const hasGeminiPromptReady = (output) => /\bType your message\b/u.test(output);
52
+ const hasOpencodePromptReady = (output) => /\bAsk anything\.\.\./u.test(output);
53
+ const hasGrokPromptReady = (output) => /\b(?:Enter:send|Composer\s+\S+)/u.test(output);
54
+ const getPlainTerminalOutput = (output) => output.replace(/\r/g, '\n').replace(TERMINAL_CONTROL_PATTERN, '');
55
+ const hasAgyPromptReady = (output) => /(?:^|\n)\s*>\s*\n\s*(?:[─-]{8,}|\?\s*for shortcuts)/u.test(getPlainTerminalOutput(output));
56
+ const getLastNonEmptyTerminalLine = (output) => {
57
+ const normalized = getPlainTerminalOutput(output);
58
+ for (const line of normalized.split('\n').reverse()) {
59
+ if (line.trim().length > 0)
60
+ return line.trim();
61
+ }
62
+ return '';
63
+ };
64
+ const hasBarePromptLine = (output) => /^[❯›]$/u.test(getLastNonEmptyTerminalLine(output));
65
+ const hasCodexChoicePrompt = (output, command) => getCommandName(command) === 'codex' && /^[❯›]\s+\S/u.test(getLastNonEmptyTerminalLine(output));
37
66
  export const hasInteractivePromptReady = (output, command = '') => {
38
67
  const commandName = getCommandName(command);
39
- return (/(?:^|[\r\n])\s*[❯›]\s*/u.test(output) ||
40
- (commandName === 'gemini' && hasGeminiPromptReady(output)));
68
+ return (hasBarePromptLine(output) ||
69
+ (commandName === 'agy' && hasAgyPromptReady(output)) ||
70
+ (commandName === 'grok' && hasGrokPromptReady(output)) ||
71
+ ((commandName === 'gemini' || commandName === 'qwen') && hasGeminiPromptReady(output)) ||
72
+ (commandName === 'opencode' && hasOpencodePromptReady(output)));
41
73
  };
42
74
  export const hasBracketedPasteAcknowledgement = (output, baselineLength) => {
43
75
  const outputAfterPaste = output.slice(baselineLength);
@@ -58,7 +90,10 @@ const retriesSubmit = (command) => getCommandName(command) === 'codex';
58
90
  const getPasteAckSettleDelayMs = (command) => getCommandName(command) === 'codex' ? CODEX_PASTE_ACK_SETTLE_DELAY_MS : PASTE_ACK_SETTLE_DELAY_MS;
59
91
  const getPasteAckTimeoutMs = (command) => getCommandName(command) === 'codex' ? CODEX_PASTE_ACK_TIMEOUT_MS : PASTE_ACK_TIMEOUT_MS;
60
92
  const usesBracketedPaste = (command) => COMMANDS_WITH_BRACKETED_PASTE.has(getCommandName(command));
61
- const canTimeoutBeforePromptReady = (command) => getCommandName(command) !== 'gemini';
93
+ const canTimeoutBeforePromptReady = (command) => {
94
+ const commandName = getCommandName(command);
95
+ return commandName !== 'agy' && commandName !== 'gemini';
96
+ };
62
97
  const isWritableRunStatus = (status) => status === undefined || status === 'starting' || status === 'running';
63
98
  const writeIfRunWritable = (agentManager, runId, text) => {
64
99
  let run;
@@ -73,9 +108,9 @@ const writeIfRunWritable = (agentManager, runId, text) => {
73
108
  agentManager.writeInput(runId, text);
74
109
  return true;
75
110
  };
76
- const submitPastedInteractiveInput = (agentManager, runId, text, baselineLength, waitForPasteAck, pasteAckSettleDelayMs, pasteAckTimeoutMs, retrySubmit, onDone, onError) => {
111
+ const submitPastedInteractiveInput = (agentManager, runId, command, text, baselineLength, waitForPasteAck, pasteAckSettleDelayMs, pasteAckTimeoutMs, retrySubmit, onDone, onError) => {
77
112
  const pastedAt = Date.now();
78
- const minDelay = getSubmitAfterPasteDelayMs(text);
113
+ const minDelay = getSubmitAfterPasteDelayMs(command, text);
79
114
  let acknowledgedAt = null;
80
115
  const getWritableOutput = () => {
81
116
  try {
@@ -164,7 +199,7 @@ export const createPostStartInputWriter = (agentManager, command) => {
164
199
  return (runId, text) => {
165
200
  // Synchronous write; an EPIPE/inactive failure still throws synchronously
166
201
  // (before the promise is returned), preserving the dispatcher's contract.
167
- if (!writeIfRunWritable(agentManager, runId, `${text}\n`)) {
202
+ if (!writeIfRunWritable(agentManager, runId, `${text}\r`)) {
168
203
  throw createRunInactiveError(runId);
169
204
  }
170
205
  return Promise.resolve();
@@ -194,7 +229,9 @@ export const createPostStartInputWriter = (agentManager, command) => {
194
229
  return;
195
230
  }
196
231
  if (hasInteractivePromptReady(output, command) ||
197
- (canTimeoutBeforePromptReady(command) && Date.now() - startedAt >= READY_TIMEOUT_MS)) {
232
+ (!hasCodexChoicePrompt(output, command) &&
233
+ canTimeoutBeforePromptReady(command) &&
234
+ Date.now() - startedAt >= READY_TIMEOUT_MS)) {
198
235
  const baselineLength = output.length;
199
236
  const input = usesBracketedPaste(command) ? toBracketedPasteSubmission(text) : text;
200
237
  try {
@@ -209,7 +246,7 @@ export const createPostStartInputWriter = (agentManager, command) => {
209
246
  rejectDone(toError(error));
210
247
  return;
211
248
  }
212
- submitPastedInteractiveInput(agentManager, runId, text, baselineLength, shouldWaitForPasteAck(command, text), getPasteAckSettleDelayMs(command), getPasteAckTimeoutMs(command), retriesSubmit(command), resolveDone, rejectDone);
249
+ submitPastedInteractiveInput(agentManager, runId, command, text, baselineLength, shouldWaitForPasteAck(command, text), getPasteAckSettleDelayMs(command), getPasteAckTimeoutMs(command), retriesSubmit(command), resolveDone, rejectDone);
213
250
  return;
214
251
  }
215
252
  setTimeout(tryWrite, READY_CHECK_INTERVAL_MS);
@@ -64,6 +64,7 @@ const supportsPresetResume = (capture) => capture?.source === 'claude_project_js
64
64
  capture?.source === 'codex_session_jsonl_dir' ||
65
65
  capture?.source === 'gemini_session_json_dir' ||
66
66
  capture?.source === 'opencode_session_db' ||
67
+ capture?.source === 'qwen_session_json_dir' ||
67
68
  capture?.source === 'stdout_regex';
68
69
  export const withPresetResumeArgs = (config, preset, lastSessionId, cwd, discriminator, onInvalidSessionId) => {
69
70
  const launchConfig = normalizeCodexNodeEntrypoint(config, preset);
@@ -1,9 +1,10 @@
1
1
  import type { AgentSummary, WorkspaceSummary } from '../shared/types.js';
2
2
  import { type FeatureFlags } from './feature-flags.js';
3
3
  import type { RecoveryMessage } from './message-log-store.js';
4
- export declare const buildRecoverySummary: ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, flags, }: {
4
+ export declare const buildRecoverySummary: ({ agent, allTaskMessages, memoryDigest, messages, tasksContent, workers, workspace, flags, }: {
5
5
  agent: AgentSummary;
6
6
  allTaskMessages?: RecoveryMessage[];
7
+ memoryDigest?: string | null | undefined;
7
8
  messages: RecoveryMessage[];
8
9
  tasksContent: string;
9
10
  workers: AgentSummary[];
@@ -65,7 +65,7 @@ const formatWorkers = (workers) => {
65
65
  return workers.map((worker) => `- ${worker.name} (${worker.role}, ${worker.status}, pending_task_count: ${worker.pendingTaskCount})`);
66
66
  };
67
67
  const getTaskSectionTitle = (agent) => agent.role === 'orchestrator' ? '## Tasks you dispatched' : '## Tasks recently sent to you';
68
- export const buildRecoverySummary = ({ agent, allTaskMessages, messages, tasksContent, workers, workspace, flags = FEATURE_FLAGS_ALL_OFF, }) => wrapSystemMessage([
68
+ export const buildRecoverySummary = ({ agent, allTaskMessages, memoryDigest, messages, tasksContent, workers, workspace, flags = FEATURE_FLAGS_ALL_OFF, }) => wrapSystemMessage([
69
69
  `You are ${agent.name} (${agent.role}) in workspace ${workspace.name}.`,
70
70
  'Hive just restarted you and could not recover via native session resume. Here is the handover context.',
71
71
  '',
@@ -84,6 +84,7 @@ export const buildRecoverySummary = ({ agent, allTaskMessages, messages, tasksCo
84
84
  '## Active workers',
85
85
  ...formatWorkers(workers),
86
86
  '',
87
+ ...(memoryDigest ? ['## Hive memory digest', memoryDigest, ''] : []),
87
88
  agent.role === 'orchestrator' ? '## Hive worker dispatch rules' : '## Hive worker boundaries',
88
89
  ...getHiveTeamRules(agent, flags),
89
90
  '',
@@ -0,0 +1,51 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ export type RemoteAuditResult = 'ok' | 'rejected' | 'error';
3
+ export type RemoteAuditAction = 'http' | 'ws_input' | 'ws_open' | 'session_open' | 'session_close' | 'revoke' | 'reject';
4
+ export interface RemoteAuditEvent {
5
+ /** M1 session device id. Audit/revocation tag only — never a permission branch. */
6
+ deviceId?: string | null;
7
+ /** Coarse category (see RemoteAuditAction). */
8
+ action: RemoteAuditAction;
9
+ /** Whitelisted path for http/ws actions (`/api/...`, `/ws/...`); omit otherwise. */
10
+ endpoint?: string | null;
11
+ /** Affected workspace, when the action is scoped to one. */
12
+ workspaceId?: string | null;
13
+ result: RemoteAuditResult;
14
+ /** Required-in-spirit on a rejection: the concrete reason (off-whitelist, revoked, …). */
15
+ rejectReason?: string | null;
16
+ /** WS-input byte count. The full chunk is NEVER stored. */
17
+ byteCount?: number | null;
18
+ /** Short truncated preview of WS input. Bounded here, not by the caller. */
19
+ preview?: string | null;
20
+ }
21
+ export interface RemoteAuditRecord {
22
+ id: number;
23
+ deviceId: string | null;
24
+ ts: number;
25
+ workspaceId: string | null;
26
+ action: string;
27
+ endpoint: string | null;
28
+ result: string;
29
+ rejectReason: string | null;
30
+ byteCount: number | null;
31
+ preview: string | null;
32
+ }
33
+ export declare const AUDIT_PREVIEW_MAX = 120;
34
+ export declare const createRemoteAuditStore: (db: Database) => {
35
+ /**
36
+ * Record an audit event. Returns immediately; the row is written on a
37
+ * later microtask. This is the ONLY method the tunnel calls on the hot path.
38
+ */
39
+ enqueue(event: RemoteAuditEvent, ts?: number): void;
40
+ /**
41
+ * Drain the buffer synchronously and await the pending write. Tests await
42
+ * this to assert rows deterministically; shutdown calls it to avoid losing
43
+ * the tail of the buffer. Idempotent when there's nothing pending.
44
+ */
45
+ flush(): Promise<void>;
46
+ /** Newest-first audit rows, capped at `limit`. Backs the Settings stream. */
47
+ list(limit?: number): RemoteAuditRecord[];
48
+ /** Newest-first rows for one device. Used by the device-detail view. */
49
+ listForDevice(deviceId: string, limit?: number): RemoteAuditRecord[];
50
+ };
51
+ export type RemoteAuditStore = ReturnType<typeof createRemoteAuditStore>;
@@ -0,0 +1,108 @@
1
+ // stdin previews are bounded so a paste of a megabyte of secrets can't end up in
2
+ // the audit table. byte_count carries the real size; preview is just enough to
3
+ // recognise the input in the Settings stream.
4
+ export const AUDIT_PREVIEW_MAX = 120;
5
+ const truncatePreview = (preview) => {
6
+ if (preview === null || preview === undefined)
7
+ return null;
8
+ if (preview.length <= AUDIT_PREVIEW_MAX)
9
+ return preview;
10
+ return `${preview.slice(0, AUDIT_PREVIEW_MAX)}…`;
11
+ };
12
+ export const createRemoteAuditStore = (db) => {
13
+ const insert = db.prepare(`INSERT INTO remote_audit
14
+ (remote_device_id, ts, workspace_id, action, endpoint, result, reject_reason, byte_count, preview)
15
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
16
+ const listStmt = db.prepare(`SELECT id, remote_device_id, ts, workspace_id, action, endpoint, result, reject_reason, byte_count, preview
17
+ FROM remote_audit
18
+ ORDER BY id DESC
19
+ LIMIT ?`);
20
+ const listForDeviceStmt = db.prepare(`SELECT id, remote_device_id, ts, workspace_id, action, endpoint, result, reject_reason, byte_count, preview
21
+ FROM remote_audit
22
+ WHERE remote_device_id = ?
23
+ ORDER BY id DESC
24
+ LIMIT ?`);
25
+ // Pending events buffered between flushes. enqueue() never touches SQLite
26
+ // directly — it appends here and arms a single microtask drain, so a burst of
27
+ // terminal keystrokes coalesces into one transaction instead of N.
28
+ const pending = [];
29
+ let flushScheduled = false;
30
+ let draining = null;
31
+ const writePending = () => {
32
+ if (pending.length === 0)
33
+ return;
34
+ const batch = pending.splice(0, pending.length);
35
+ const tx = db.transaction((events) => {
36
+ for (const e of events) {
37
+ insert.run(e.deviceId ?? null, e.ts, e.workspaceId ?? null, e.action, e.endpoint ?? null, e.result, e.rejectReason ?? null, typeof e.byteCount === 'number' ? e.byteCount : null, truncatePreview(e.preview));
38
+ }
39
+ });
40
+ tx(batch);
41
+ };
42
+ const scheduleFlush = () => {
43
+ if (flushScheduled)
44
+ return;
45
+ flushScheduled = true;
46
+ draining = new Promise((resolve) => {
47
+ // queueMicrotask keeps the write off the forwarding call stack while
48
+ // still landing before the event loop yields to I/O, so audit rows for a
49
+ // request are durable well before the response round-trips.
50
+ queueMicrotask(() => {
51
+ flushScheduled = false;
52
+ try {
53
+ writePending();
54
+ }
55
+ catch {
56
+ // A failed audit write must not crash the tunnel. Drop the batch and
57
+ // keep forwarding; losing an audit row is strictly better than
58
+ // dropping a user's terminal input.
59
+ }
60
+ resolve();
61
+ });
62
+ });
63
+ };
64
+ const mapRow = (row) => ({
65
+ id: row.id,
66
+ deviceId: row.remote_device_id,
67
+ ts: row.ts,
68
+ workspaceId: row.workspace_id,
69
+ action: row.action,
70
+ endpoint: row.endpoint,
71
+ result: row.result,
72
+ rejectReason: row.reject_reason,
73
+ byteCount: row.byte_count,
74
+ preview: row.preview,
75
+ });
76
+ return {
77
+ /**
78
+ * Record an audit event. Returns immediately; the row is written on a
79
+ * later microtask. This is the ONLY method the tunnel calls on the hot path.
80
+ */
81
+ enqueue(event, ts = Date.now()) {
82
+ pending.push({ ...event, ts });
83
+ scheduleFlush();
84
+ },
85
+ /**
86
+ * Drain the buffer synchronously and await the pending write. Tests await
87
+ * this to assert rows deterministically; shutdown calls it to avoid losing
88
+ * the tail of the buffer. Idempotent when there's nothing pending.
89
+ */
90
+ async flush() {
91
+ // Settle any already-scheduled drain first, then force-write whatever the
92
+ // caller enqueued after it was scheduled (or never scheduled at all).
93
+ if (draining)
94
+ await draining;
95
+ writePending();
96
+ },
97
+ /** Newest-first audit rows, capped at `limit`. Backs the Settings stream. */
98
+ list(limit = 100) {
99
+ writePending();
100
+ return listStmt.all(limit).map(mapRow);
101
+ },
102
+ /** Newest-first rows for one device. Used by the device-detail view. */
103
+ listForDevice(deviceId, limit = 100) {
104
+ writePending();
105
+ return listForDeviceStmt.all(deviceId, limit).map(mapRow);
106
+ },
107
+ };
108
+ };
@@ -0,0 +1,17 @@
1
+ export declare const REMOTE_GATEWAY_URL_KEY = "remote_gateway_url";
2
+ export declare const REMOTE_DAEMON_TOKEN_KEY = "remote_daemon_token";
3
+ export declare const REMOTE_DAEMON_ID_KEY = "remote_daemon_id";
4
+ export declare const REMOTE_ENABLED_KEY = "remote_enabled";
5
+ export declare const DEFAULT_GATEWAY_URL = "https://app.hivehq.dev";
6
+ export interface RemoteAppStateReader {
7
+ get(key: string): {
8
+ value: string | null;
9
+ } | undefined;
10
+ }
11
+ export interface RemoteConfigSource {
12
+ isEnabled(): boolean;
13
+ getGatewayUrl(): string | null;
14
+ getDaemonToken(): string | null;
15
+ getDaemonId(): string | null;
16
+ }
17
+ export declare const createRemoteConfigSource: (store: RemoteAppStateReader) => RemoteConfigSource;
@@ -0,0 +1,27 @@
1
+ // Remote-access config lives in the existing app_state KV. These are the
2
+ // canonical key literals — the single source of truth for BOTH sides of the
3
+ // feature: `hive remote login/logout` (src/cli/hive-remote.ts) writes them, and
4
+ // the daemon-side tunnel reads them. The CLI re-exports these so there is one
5
+ // definition, not two that can drift (a rename on only one side would silently
6
+ // stop login from reaching the tunnel).
7
+ //
8
+ // snake_case to match the existing app_state keys (active_workspace_id, …).
9
+ export const REMOTE_GATEWAY_URL_KEY = 'remote_gateway_url';
10
+ export const REMOTE_DAEMON_TOKEN_KEY = 'remote_daemon_token';
11
+ export const REMOTE_DAEMON_ID_KEY = 'remote_daemon_id';
12
+ export const REMOTE_ENABLED_KEY = 'remote_enabled';
13
+ export const DEFAULT_GATEWAY_URL = 'https://app.hivehq.dev';
14
+ // remote_enabled is ON only when it is exactly the string 'true'. Everything
15
+ // else — absent, '', 'false', '1', 'yes' — is OFF. This is invariant 4 (off ==
16
+ // zero behavior change): a truthy check would let a stray value silently arm
17
+ // the outbound tunnel, so the test pins the exact-string semantics.
18
+ const ENABLED_VALUE = 'true';
19
+ export const createRemoteConfigSource = (store) => {
20
+ const read = (key) => store.get(key)?.value ?? null;
21
+ return {
22
+ isEnabled: () => read(REMOTE_ENABLED_KEY) === ENABLED_VALUE,
23
+ getGatewayUrl: () => read(REMOTE_GATEWAY_URL_KEY),
24
+ getDaemonToken: () => read(REMOTE_DAEMON_TOKEN_KEY),
25
+ getDaemonId: () => read(REMOTE_DAEMON_ID_KEY),
26
+ };
27
+ };
@@ -0,0 +1,30 @@
1
+ export declare const GW_CONTROL_PREFIX = "\0gw:";
2
+ export declare const RelayCloseCode: {
3
+ readonly Normal: 1000;
4
+ readonly ProtocolError: 4400;
5
+ readonly Unauthorized: 4401;
6
+ readonly Forbidden: 4403;
7
+ readonly DaemonOffline: 4404;
8
+ readonly Replaced: 4409;
9
+ readonly Revoked: 4410;
10
+ readonly InternalError: 4500;
11
+ };
12
+ export type RelayCloseCode = (typeof RelayCloseCode)[keyof typeof RelayCloseCode];
13
+ export type GatewayControl = {
14
+ t: 'peer-online';
15
+ role: 'daemon' | 'device' | 'pair';
16
+ jti?: string;
17
+ } | {
18
+ t: 'peer-offline';
19
+ role: 'daemon' | 'device' | 'pair';
20
+ } | {
21
+ t: 'revoked';
22
+ reason: string;
23
+ } | {
24
+ t: 'error';
25
+ code: number;
26
+ message: string;
27
+ };
28
+ export declare const HB_PING = "hb:ping";
29
+ export declare const HB_PONG = "hb:pong";
30
+ export declare const isAuthFatalCloseCode: (code: number) => boolean;
@@ -0,0 +1,29 @@
1
+ // Daemon-side re-declaration of the gateway's control-band wire contract.
2
+ //
3
+ // The gateway lives in gateway/src/relay-do.ts — a separate Cloudflare-Workers package that is NOT
4
+ // import-reachable from src/ (importing it would drag in cloudflare:workers). So the daemon mirrors
5
+ // the few values it has to agree on, and tests/unit/remote-control-constants.test.ts pins each one
6
+ // to the literal gateway value so the wire format can never silently drift between the two sides.
7
+ // Control-frame sentinel. The gateway prefixes every control message (peer presence, revocation)
8
+ // with this so it can never be confused with an opaque binary E2E frame. relay-do.ts:29.
9
+ export const GW_CONTROL_PREFIX = '\x00gw:';
10
+ // WebSocket close codes (4xxx app range). relay-do.ts:32-41.
11
+ export const RelayCloseCode = {
12
+ Normal: 1000,
13
+ ProtocolError: 4400,
14
+ Unauthorized: 4401,
15
+ Forbidden: 4403,
16
+ DaemonOffline: 4404,
17
+ Replaced: 4409,
18
+ Revoked: 4410,
19
+ InternalError: 4500,
20
+ };
21
+ // Idle keepalive: the daemon SENDS this app-level string and the DO auto-replies HB_PONG WITHOUT
22
+ // waking (setWebSocketAutoResponse, relay-do.ts:144). A protocol-level ws.ping() would wake the DO
23
+ // and burn duration — the heartbeat MUST be this string, never ws.ping().
24
+ export const HB_PING = 'hb:ping';
25
+ export const HB_PONG = 'hb:pong';
26
+ // Only these two close codes are authoritative credential death: the daemon must LATCH (stop
27
+ // retrying until refresh() re-reads config). Every other close — including 4404 DaemonOffline,
28
+ // 4409 Replaced, and the transport 1006 — is transient and backs off + retries.
29
+ export const isAuthFatalCloseCode = (code) => code === RelayCloseCode.Unauthorized || code === RelayCloseCode.Revoked;
@@ -0,0 +1,40 @@
1
+ import type { Direction } from '../shared/remote-crypto.js';
2
+ export interface DeviceSession {
3
+ deviceId: string;
4
+ /**
5
+ * 32-byte directional ROOT keys from M1 deriveDaemonSession. M6.1: these are ROOTS — the bridge
6
+ * derives the per-connection AEAD keys from them via deriveConnectionKeys + the bilateral connection
7
+ * salts, and seals/opens ONLY under those connKeys. These bytes are NEVER passed to sealNext/openNext
8
+ * directly; that is what makes resetting seq to 0 per connection safe (the AEAD key is fresh per
9
+ * connection even though the root is persisted/reused).
10
+ */
11
+ keys: {
12
+ d2p: Uint8Array;
13
+ p2d: Uint8Array;
14
+ };
15
+ }
16
+ export interface DeviceSessionProvider {
17
+ /**
18
+ * null => unknown / revoked / no established session. The tunnel resets the stream and audits
19
+ * 'no_session'; it never bridges a frame for a device it has no key for.
20
+ */
21
+ get(deviceId: string): DeviceSession | null;
22
+ /**
23
+ * Every established session. Used ONLY to resolve which device a CHANNEL_STREAM_ID Hello came
24
+ * from: the relay does not tag the source device, so the daemon trial-opens the Hello against
25
+ * each candidate's p2d key until one authenticates (AEAD makes a wrong key fail cleanly). After
26
+ * the Hello opens, the daemon binds (deviceId, streamId)->device and never trial-opens that
27
+ * stream again. deviceId is therefore only ever the result of a successful open (invariant 5).
28
+ */
29
+ candidates(): DeviceSession[];
30
+ }
31
+ export declare const DAEMON_OPEN_DIRECTION: Direction;
32
+ export declare const DAEMON_SEAL_DIRECTION: Direction;
33
+ export declare class InMemoryDeviceSessionProvider implements DeviceSessionProvider {
34
+ private readonly sessions;
35
+ set(session: DeviceSession): void;
36
+ /** Models a revoke: the key is gone, so the next frame for this device fails 'no_session'. */
37
+ remove(deviceId: string): void;
38
+ get(deviceId: string): DeviceSession | null;
39
+ candidates(): DeviceSession[];
40
+ }
@@ -0,0 +1,22 @@
1
+ // Daemon directions — the mirror of the device's. Exported so the tunnel, the bridge and the tests
2
+ // all agree on ONE source of truth for which key opens/seals which way.
3
+ // - the daemon OPENS inbound phone→daemon frames with p2d
4
+ // - the daemon SEALS outbound daemon→phone frames with d2p
5
+ export const DAEMON_OPEN_DIRECTION = 'p2d';
6
+ export const DAEMON_SEAL_DIRECTION = 'd2p';
7
+ export class InMemoryDeviceSessionProvider {
8
+ sessions = new Map();
9
+ set(session) {
10
+ this.sessions.set(session.deviceId, session);
11
+ }
12
+ /** Models a revoke: the key is gone, so the next frame for this device fails 'no_session'. */
13
+ remove(deviceId) {
14
+ this.sessions.delete(deviceId);
15
+ }
16
+ get(deviceId) {
17
+ return this.sessions.get(deviceId) ?? null;
18
+ }
19
+ candidates() {
20
+ return [...this.sessions.values()];
21
+ }
22
+ }
@@ -0,0 +1,36 @@
1
+ import type { Database } from 'better-sqlite3';
2
+ import type { DeviceSession, DeviceSessionProvider } from './remote-device-session.js';
3
+ export interface RemoteDeviceRecord {
4
+ id: string;
5
+ name: string;
6
+ createdAt: number;
7
+ lastActive: number | null;
8
+ revokedAt: number | null;
9
+ }
10
+ export interface PersistDeviceInput {
11
+ id: string;
12
+ name: string;
13
+ /** The M3 DeviceSession.keys — a stored secret. */
14
+ keys: {
15
+ d2p: Uint8Array;
16
+ p2d: Uint8Array;
17
+ };
18
+ devicePublicKey: Uint8Array;
19
+ }
20
+ export interface RemoteDeviceStore {
21
+ /** TRUST-ROOT write. ONLY remote-pairing.confirmPairing calls this, ONLY on desktop confirm. */
22
+ insert(input: PersistDeviceInput, now?: number): RemoteDeviceRecord;
23
+ /** Provider read path (returns key material). null if absent OR revoked. INTERNAL. */
24
+ getLiveSession(deviceId: string): DeviceSession | null;
25
+ /** Active (non-revoked) sessions only — backs DeviceSessionProvider.candidates(). */
26
+ liveSessions(): DeviceSession[];
27
+ /** Device-management read path — metadata ONLY, no keys. Newest first. */
28
+ list(includeRevoked?: boolean): RemoteDeviceRecord[];
29
+ get(deviceId: string): RemoteDeviceRecord | null;
30
+ /** Local half of the revocation closed loop. Idempotent; false if unknown / already revoked. */
31
+ revoke(deviceId: string, now?: number): boolean;
32
+ /** Best-effort last_active bump. Never resurrects a revoked row. */
33
+ touchActive(deviceId: string, now?: number): void;
34
+ }
35
+ export declare const createRemoteDeviceStore: (db: Database) => RemoteDeviceStore;
36
+ export declare const createPersistentDeviceSessionProvider: (store: RemoteDeviceStore) => DeviceSessionProvider;