@hwip/cli 0.0.1 → 0.1.0

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 (245) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +11 -0
  3. package/README.md +1979 -0
  4. package/dist/agent-list.d.ts +76 -0
  5. package/dist/agent-list.js +102 -0
  6. package/dist/agent-read.d.ts +105 -0
  7. package/dist/agent-read.js +309 -0
  8. package/dist/agent-rules-fetch.d.ts +51 -0
  9. package/dist/agent-rules-fetch.js +275 -0
  10. package/dist/agent-rules.default.d.ts +30 -0
  11. package/dist/agent-rules.default.js +292 -0
  12. package/dist/agent-session.d.ts +23 -0
  13. package/dist/agent-session.js +44 -0
  14. package/dist/agent-state.d.ts +84 -0
  15. package/dist/agent-state.js +188 -0
  16. package/dist/agent-target.d.ts +60 -0
  17. package/dist/agent-target.js +81 -0
  18. package/dist/agents.d.ts +31 -0
  19. package/dist/agents.js +75 -0
  20. package/dist/api.d.ts +28 -0
  21. package/dist/api.js +76 -0
  22. package/dist/args.d.ts +74 -0
  23. package/dist/args.js +254 -0
  24. package/dist/ask-context-attach.d.ts +73 -0
  25. package/dist/ask-context-attach.js +151 -0
  26. package/dist/ask-context-screen.d.ts +21 -0
  27. package/dist/ask-context-screen.js +49 -0
  28. package/dist/ask-context-switch.d.ts +31 -0
  29. package/dist/ask-context-switch.js +74 -0
  30. package/dist/ask-context.d.ts +84 -0
  31. package/dist/ask-context.js +172 -0
  32. package/dist/ask-pending.d.ts +47 -0
  33. package/dist/ask-pending.js +221 -0
  34. package/dist/ask-seal.d.ts +120 -0
  35. package/dist/ask-seal.js +179 -0
  36. package/dist/ask.d.ts +107 -0
  37. package/dist/ask.js +551 -0
  38. package/dist/blocked-prompt-seal.d.ts +35 -0
  39. package/dist/blocked-prompt-seal.js +89 -0
  40. package/dist/blocked-prompt.d.ts +57 -0
  41. package/dist/blocked-prompt.js +181 -0
  42. package/dist/channel.d.ts +67 -0
  43. package/dist/channel.js +136 -0
  44. package/dist/check-update.d.ts +28 -0
  45. package/dist/check-update.js +153 -0
  46. package/dist/cli.d.ts +3 -0
  47. package/dist/cli.js +813 -0
  48. package/dist/codex-config.d.ts +24 -0
  49. package/dist/codex-config.js +169 -0
  50. package/dist/codex-transcript.d.ts +59 -0
  51. package/dist/codex-transcript.js +373 -0
  52. package/dist/command-refusal.d.ts +56 -0
  53. package/dist/command-refusal.js +127 -0
  54. package/dist/command-scan.d.ts +46 -0
  55. package/dist/command-scan.js +154 -0
  56. package/dist/command-seal.d.ts +57 -0
  57. package/dist/command-seal.js +88 -0
  58. package/dist/command-seq.d.ts +30 -0
  59. package/dist/command-seq.js +150 -0
  60. package/dist/config.d.ts +156 -0
  61. package/dist/config.js +392 -0
  62. package/dist/core-sections.d.ts +18 -0
  63. package/dist/core-sections.js +28 -0
  64. package/dist/crypto.d.ts +278 -0
  65. package/dist/crypto.js +698 -0
  66. package/dist/desktop-notify.d.ts +33 -0
  67. package/dist/desktop-notify.js +49 -0
  68. package/dist/device-directory.d.ts +36 -0
  69. package/dist/device-directory.js +97 -0
  70. package/dist/device-presence.d.ts +17 -0
  71. package/dist/device-presence.js +62 -0
  72. package/dist/downloads.d.ts +29 -0
  73. package/dist/downloads.js +106 -0
  74. package/dist/errors.d.ts +12 -0
  75. package/dist/errors.js +28 -0
  76. package/dist/file-crypto.d.ts +94 -0
  77. package/dist/file-crypto.js +272 -0
  78. package/dist/file-view-budget.d.ts +15 -0
  79. package/dist/file-view-budget.js +34 -0
  80. package/dist/file-view-switch.d.ts +28 -0
  81. package/dist/file-view-switch.js +122 -0
  82. package/dist/file-view.d.ts +108 -0
  83. package/dist/file-view.js +327 -0
  84. package/dist/gate.d.ts +141 -0
  85. package/dist/gate.js +336 -0
  86. package/dist/gemini-transcript.d.ts +32 -0
  87. package/dist/gemini-transcript.js +189 -0
  88. package/dist/http-failure.d.ts +98 -0
  89. package/dist/http-failure.js +349 -0
  90. package/dist/hwip-core.generated.d.ts +7 -0
  91. package/dist/hwip-core.generated.js +14 -0
  92. package/dist/hwip-hook.d.ts +77 -0
  93. package/dist/hwip-hook.js +340 -0
  94. package/dist/index.d.ts +7 -0
  95. package/dist/index.js +14 -0
  96. package/dist/input-sequencer.d.ts +65 -0
  97. package/dist/input-sequencer.js +142 -0
  98. package/dist/insecure-url.d.ts +16 -0
  99. package/dist/insecure-url.js +76 -0
  100. package/dist/installer.d.ts +96 -0
  101. package/dist/installer.js +850 -0
  102. package/dist/inventory-offload.d.ts +49 -0
  103. package/dist/inventory-offload.js +209 -0
  104. package/dist/inventory-worker.d.ts +2 -0
  105. package/dist/inventory-worker.js +27 -0
  106. package/dist/jsonc-doc.d.ts +106 -0
  107. package/dist/jsonc-doc.js +354 -0
  108. package/dist/keep-awake.d.ts +32 -0
  109. package/dist/keep-awake.js +46 -0
  110. package/dist/key-changes.d.ts +48 -0
  111. package/dist/key-changes.js +140 -0
  112. package/dist/lan-auth.d.ts +140 -0
  113. package/dist/lan-auth.js +217 -0
  114. package/dist/lan-endpoint.d.ts +41 -0
  115. package/dist/lan-endpoint.js +115 -0
  116. package/dist/lan-receiver.d.ts +122 -0
  117. package/dist/lan-receiver.js +510 -0
  118. package/dist/lan-sender.d.ts +76 -0
  119. package/dist/lan-sender.js +184 -0
  120. package/dist/lan-transfer.d.ts +36 -0
  121. package/dist/lan-transfer.js +54 -0
  122. package/dist/listener-device-id.d.ts +20 -0
  123. package/dist/listener-device-id.js +133 -0
  124. package/dist/listener-log.d.ts +4 -0
  125. package/dist/listener-log.js +23 -0
  126. package/dist/listener-process.d.ts +200 -0
  127. package/dist/listener-process.js +470 -0
  128. package/dist/listener-service.d.ts +225 -0
  129. package/dist/listener-service.js +682 -0
  130. package/dist/listener-worktree-cleanup.d.ts +27 -0
  131. package/dist/listener-worktree-cleanup.js +62 -0
  132. package/dist/listener.d.ts +1325 -0
  133. package/dist/listener.js +5680 -0
  134. package/dist/login-address.d.ts +25 -0
  135. package/dist/login-address.js +97 -0
  136. package/dist/login-page.d.ts +29 -0
  137. package/dist/login-page.js +93 -0
  138. package/dist/login.d.ts +98 -0
  139. package/dist/login.js +388 -0
  140. package/dist/mcp-command.d.ts +45 -0
  141. package/dist/mcp-command.js +50 -0
  142. package/dist/mcp.d.ts +19 -0
  143. package/dist/mcp.js +29 -0
  144. package/dist/node-support.d.ts +19 -0
  145. package/dist/node-support.js +38 -0
  146. package/dist/notify-failure.d.ts +57 -0
  147. package/dist/notify-failure.js +150 -0
  148. package/dist/opencode-transcript.d.ts +139 -0
  149. package/dist/opencode-transcript.js +390 -0
  150. package/dist/optional-features.d.ts +6 -0
  151. package/dist/optional-features.js +19 -0
  152. package/dist/pane-fit.d.ts +40 -0
  153. package/dist/pane-fit.js +119 -0
  154. package/dist/peer-key.d.ts +33 -0
  155. package/dist/peer-key.js +34 -0
  156. package/dist/pi-transcript.d.ts +26 -0
  157. package/dist/pi-transcript.js +255 -0
  158. package/dist/presence.d.ts +14 -0
  159. package/dist/presence.js +124 -0
  160. package/dist/private-files.d.ts +51 -0
  161. package/dist/private-files.js +218 -0
  162. package/dist/project-switch.d.ts +19 -0
  163. package/dist/project-switch.js +60 -0
  164. package/dist/redact.d.ts +10 -0
  165. package/dist/redact.js +21 -0
  166. package/dist/remote-agents.d.ts +410 -0
  167. package/dist/remote-agents.js +1218 -0
  168. package/dist/remote-hook.d.ts +12 -0
  169. package/dist/remote-hook.js +196 -0
  170. package/dist/replace-file.d.ts +50 -0
  171. package/dist/replace-file.js +328 -0
  172. package/dist/schedule-command.d.ts +35 -0
  173. package/dist/schedule-command.js +260 -0
  174. package/dist/schedule-launch.d.ts +15 -0
  175. package/dist/schedule-launch.js +24 -0
  176. package/dist/schedule-ready.d.ts +44 -0
  177. package/dist/schedule-ready.js +71 -0
  178. package/dist/schedule-remote.d.ts +77 -0
  179. package/dist/schedule-remote.js +167 -0
  180. package/dist/schedule-runner.d.ts +89 -0
  181. package/dist/schedule-runner.js +452 -0
  182. package/dist/schedule.d.ts +149 -0
  183. package/dist/schedule.js +428 -0
  184. package/dist/secret-mask.d.ts +29 -0
  185. package/dist/secret-mask.js +245 -0
  186. package/dist/self-register.d.ts +45 -0
  187. package/dist/self-register.js +67 -0
  188. package/dist/send.d.ts +56 -0
  189. package/dist/send.js +104 -0
  190. package/dist/session-diff.d.ts +161 -0
  191. package/dist/session-diff.js +445 -0
  192. package/dist/session-registry.d.ts +143 -0
  193. package/dist/session-registry.js +216 -0
  194. package/dist/sessions-report.d.ts +59 -0
  195. package/dist/sessions-report.js +99 -0
  196. package/dist/shell-audit.d.ts +34 -0
  197. package/dist/shell-audit.js +47 -0
  198. package/dist/subagent-transcripts.d.ts +167 -0
  199. package/dist/subagent-transcripts.js +437 -0
  200. package/dist/subscriber-trust.d.ts +128 -0
  201. package/dist/subscriber-trust.js +397 -0
  202. package/dist/templates.d.ts +105 -0
  203. package/dist/templates.js +871 -0
  204. package/dist/tmux-install.d.ts +44 -0
  205. package/dist/tmux-install.js +65 -0
  206. package/dist/tmux-liveness.d.ts +45 -0
  207. package/dist/tmux-liveness.js +71 -0
  208. package/dist/tmux-pane.d.ts +55 -0
  209. package/dist/tmux-pane.js +126 -0
  210. package/dist/tmux-socket.d.ts +20 -0
  211. package/dist/tmux-socket.js +292 -0
  212. package/dist/todo-fold.d.ts +31 -0
  213. package/dist/todo-fold.js +102 -0
  214. package/dist/transcript-tail.d.ts +312 -0
  215. package/dist/transcript-tail.js +718 -0
  216. package/dist/trust-command.d.ts +49 -0
  217. package/dist/trust-command.js +200 -0
  218. package/dist/trust-lock.d.ts +26 -0
  219. package/dist/trust-lock.js +94 -0
  220. package/dist/trust-pins.d.ts +37 -0
  221. package/dist/trust-pins.js +82 -0
  222. package/dist/turn-recorder-wire.d.ts +70 -0
  223. package/dist/turn-recorder-wire.js +165 -0
  224. package/dist/turn-recorder.d.ts +190 -0
  225. package/dist/turn-recorder.js +695 -0
  226. package/dist/turn-ring.d.ts +93 -0
  227. package/dist/turn-ring.js +418 -0
  228. package/dist/turn-watch.d.ts +177 -0
  229. package/dist/turn-watch.js +630 -0
  230. package/dist/types.d.ts +63 -0
  231. package/dist/types.js +2 -0
  232. package/dist/uninstall.d.ts +55 -0
  233. package/dist/uninstall.js +507 -0
  234. package/dist/usage.d.ts +17 -0
  235. package/dist/usage.js +421 -0
  236. package/dist/verify.d.ts +47 -0
  237. package/dist/verify.js +396 -0
  238. package/dist/worktree.d.ts +144 -0
  239. package/dist/worktree.js +310 -0
  240. package/dist/wrapper.d.ts +254 -0
  241. package/dist/wrapper.js +704 -0
  242. package/dist/ws-url.d.ts +109 -0
  243. package/dist/ws-url.js +202 -0
  244. package/package.json +78 -10
  245. package/bin/hwip.js +0 -3
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ /**
3
+ * The world the recorder (`turn-recorder.ts`) talks to: the account API that
4
+ * stores chunks, the push-v1 envelope that seals them, and the list of devices
5
+ * the account has signed in. Kept apart so the recorder's tests need none of it.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.createLatestSeqReader = exports.createChunkUploader = exports.accountRecipients = exports.sealChunk = exports.RECORDER_UPLOADS_PER_MINUTE = void 0;
9
+ const api_js_1 = require("./api.js");
10
+ const ws_url_js_1 = require("./ws-url.js");
11
+ const http_failure_js_1 = require("./http-failure.js");
12
+ const crypto_js_1 = require("./crypto.js");
13
+ const subscriber_trust_js_1 = require("./subscriber-trust.js");
14
+ const trust_pins_js_1 = require("./trust-pins.js");
15
+ const turn_recorder_js_1 = require("./turn-recorder.js");
16
+ const UPLOAD_TIMEOUT_MS = 15_000;
17
+ /** The turns route has its own per-minute allowance on the server (240); chunks keep to 30, well inside it. */
18
+ exports.RECORDER_UPLOADS_PER_MINUTE = 30;
19
+ /** How long a session waits after a 403 before the same chunk is tried again. */
20
+ const RECORDER_KEY_RETRY_MS = 5 * 60_000;
21
+ /** After this many 403s in a row the chunk is dropped: the key it was sealed with is not the one on file, and will not be. */
22
+ const RECORDER_KEY_REFUSALS = 5;
23
+ /** Seal one plaintext body (`{sessionKey, seq, events}`) as a push-v1 envelope for the recipients. */
24
+ const sealChunk = async (plaintext, recipients) => {
25
+ await (0, crypto_js_1.initDeviceCrypto)();
26
+ const sealed = await (0, crypto_js_1.encryptPushBodyForDevices)({ body: plaintext }, recipients);
27
+ return { body: sealed.body, deviceKeyMap: sealed.deviceKeyMap, senderPublicKey: sealed.senderPublicKey };
28
+ };
29
+ exports.sealChunk = sealChunk;
30
+ /**
31
+ * This machine plus the devices the account lists with a key, the newest pins first and
32
+ * within the server's slot limit. The list decides who a chunk is sealed for: a device
33
+ * that signed in reads its history once the directory refreshes its list (a minute at most). Each listed device is pinned on
34
+ * the way (`observeDevices`) — the pin is what `hwip trust` shows and what the listener
35
+ * holds a request against, and its date orders who is cut when the list is longer than
36
+ * the slots. The directory keeps the last list it fetched, so a blip does not stop the
37
+ * history; a list never had means no
38
+ * recipients, and pins from an earlier run do not stand in for it. A machine with no other
39
+ * device has no one who could read a chunk.
40
+ */
41
+ const accountRecipients = (deps) => async () => {
42
+ try {
43
+ const publicKey = await (0, crypto_js_1.initDeviceCrypto)();
44
+ const self = { deviceId: deps.deviceId, publicKey: (0, crypto_js_1.getDevicePublicKey)() ?? publicKey };
45
+ const listed = (0, crypto_js_1.selectRecipients)(await deps.devices()).filter((d) => d.deviceId !== deps.deviceId);
46
+ (0, trust_pins_js_1.observeDevices)(deps.trustFile, listed, { selfId: deps.deviceId, warn: (line) => deps.log(line) });
47
+ const loaded = (0, subscriber_trust_js_1.readTrustState)(deps.trustFile);
48
+ const pins = 'broken' in loaded ? undefined : loaded.state.devices;
49
+ return (0, turn_recorder_js_1.chooseRecipients)(self, listed.map((d) => ({ ...d, pinnedAt: pins?.get(d.deviceId)?.pinnedAt })));
50
+ }
51
+ catch (err) {
52
+ deps.log(`turn history: recipients unavailable (${err instanceof Error ? err.message : String(err)})`);
53
+ return [];
54
+ }
55
+ };
56
+ exports.accountRecipients = accountRecipients;
57
+ const readError = async (res) => {
58
+ try {
59
+ return (await res.json()).error;
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ };
65
+ /**
66
+ * `POST /sessions/{key}/turns` for one stored chunk. The same bytes go out on
67
+ * every try, so a chunk the server already holds comes back 200 (stored) rather
68
+ * than 409 (a different body under the same `seq`).
69
+ *
70
+ * 201 / 200 stored.
71
+ * 429 RATE_LIMIT a minute-long limit: try again after what the server says.
72
+ * 429 TURN_DAILY the day's cap: leave the session alone until its reset.
73
+ * 408, 5xx, network try again with the recorder's backoff.
74
+ * redirect refused the base URL sends the request elsewhere on every try (a permanent
75
+ * answer, like a wrong key): pause the session for an hour and say why.
76
+ * The chunk is kept, so a corrected base URL (a restart) sends it.
77
+ * 400, 409 the server will never take these bytes (a bad body, a `seq`
78
+ * that holds another): drop the chunk, or it blocks the session;
79
+ * after a 409 the next `seq` moves past what the server holds.
80
+ * 403 the key may only just have been registered: pause the session 5 min;
81
+ * the fifth in a row on one chunk drops it.
82
+ * any other 4xx wrong key, device or session (401, 404): pause the
83
+ * session for an hour and say why in the log.
84
+ */
85
+ const createChunkUploader = (opts) => {
86
+ const fetchFn = opts.fetchFn ?? fetch;
87
+ const now = opts.now ?? Date.now;
88
+ const perMinute = opts.perMinute ?? exports.RECORDER_UPLOADS_PER_MINUTE;
89
+ let sent = [];
90
+ return async (sessionKey, chunk) => {
91
+ const at = now();
92
+ sent = sent.filter((t) => at - t < 60_000);
93
+ if (sent.length >= perMinute) {
94
+ return { kind: 'retry', why: 'own upload budget for this minute is spent', afterMs: Math.max(1_000, 60_000 - (at - sent[0])) };
95
+ }
96
+ sent.push(at);
97
+ let res;
98
+ try {
99
+ res = await (0, api_js_1.apiFetch)(fetchFn, (0, api_js_1.apiUrl)(opts.baseUrl, `/sessions/${encodeURIComponent(sessionKey)}/turns`), opts.apiKey, UPLOAD_TIMEOUT_MS, {
100
+ method: 'POST',
101
+ body: JSON.stringify({ seq: chunk.seq, firstAt: chunk.firstAt, lastAt: chunk.lastAt, sealed: chunk.sealed }),
102
+ });
103
+ }
104
+ catch (err) {
105
+ const why = err instanceof Error ? err.message : String(err);
106
+ // `apiFetch` refuses a redirect (it would carry the key to another host). It is a TypeError like a
107
+ // dropped connection, but it happens on every try: backing off and retrying only repeats it.
108
+ // The same holds for a configuration failure (bad key, bad base URL): retry with backoff only
109
+ // repeats it, so the chunk waits for a corrected configuration instead.
110
+ if ((0, http_failure_js_1.isRedirectRefusal)(err) || (0, http_failure_js_1.isConfigFailure)(err) || (0, http_failure_js_1.isInvalidUrl)(err) || (0, http_failure_js_1.isUnsendableHeader)(err) || (0, ws_url_js_1.carriesCredentials)(opts.baseUrl))
111
+ return { kind: 'pause', why };
112
+ return { kind: 'retry', why };
113
+ }
114
+ if (res.status === 200 || res.status === 201)
115
+ return { kind: 'ok' };
116
+ const error = await readError(res);
117
+ const code = typeof error?.code === 'string' ? ` ${error.code}` : '';
118
+ const why = `HTTP ${res.status}${code}`;
119
+ if (res.status === 429) {
120
+ if (error?.code === 'TURN_DAILY_LIMIT') {
121
+ const reset = typeof error.details?.resetAt === 'string' ? Date.parse(error.details.resetAt) : NaN;
122
+ return { kind: 'pause', why, untilMs: Number.isFinite(reset) && reset > at ? reset : at + turn_recorder_js_1.RECORDER_PAUSE_MS };
123
+ }
124
+ const header = Number(res.headers.get('retry-after'));
125
+ const detail = typeof error?.details?.retryAfter === 'number' ? error.details.retryAfter : NaN;
126
+ const seconds = Number.isFinite(header) && header > 0 ? header : detail;
127
+ return { kind: 'retry', why, afterMs: Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : turn_recorder_js_1.RECORDER_BACKOFF_BASE_MS * 8 };
128
+ }
129
+ if (res.status === 408 || res.status >= 500) {
130
+ const header = Number(res.headers.get('retry-after'));
131
+ return { kind: 'retry', why, ...(Number.isFinite(header) && header > 0 ? { afterMs: header * 1000 } : {}) };
132
+ }
133
+ if (res.status === 400)
134
+ return { kind: 'drop', why };
135
+ // The same `seq` already holds another body: this chunk is lost, and the numbering must move on.
136
+ if (res.status === 409)
137
+ return { kind: 'drop', why, conflict: true };
138
+ // The key frame and the first chunk can cross: a 403 just after the first registration passes by itself.
139
+ if (res.status === 403)
140
+ return { kind: 'pause', why, untilMs: at + RECORDER_KEY_RETRY_MS, giveUpAfter: RECORDER_KEY_REFUSALS };
141
+ return { kind: 'pause', why };
142
+ };
143
+ };
144
+ exports.createChunkUploader = createChunkUploader;
145
+ /**
146
+ * The newest `seq` the server holds for a session (`GET .../turns?limit=1`, newest
147
+ * first), or null when it cannot say. Used only after a 409, to number past it.
148
+ */
149
+ const createLatestSeqReader = (opts) => {
150
+ const fetchFn = opts.fetchFn ?? fetch;
151
+ return async (sessionKey) => {
152
+ try {
153
+ const res = await (0, api_js_1.apiFetch)(fetchFn, (0, api_js_1.apiUrl)(opts.baseUrl, `/sessions/${encodeURIComponent(sessionKey)}/turns?limit=1`), opts.apiKey, UPLOAD_TIMEOUT_MS);
154
+ if (!res.ok)
155
+ return null;
156
+ const body = (await res.json());
157
+ const seq = body.data?.[0]?.seq;
158
+ return typeof seq === 'number' && Number.isInteger(seq) && seq >= 0 ? seq : null;
159
+ }
160
+ catch {
161
+ return null;
162
+ }
163
+ };
164
+ };
165
+ exports.createLatestSeqReader = createLatestSeqReader;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Always-on conversation recorder (I28, hwip `docs/session-history.md` §3).
3
+ *
4
+ * `turn-watch.ts` follows a transcript for a viewer who is looking. This reads
5
+ * the transcript of every agent session this listener knows, with nobody
6
+ * looking, and uploads what was said to the account so the phone can show the
7
+ * conversation later: the same projection (`TranscriptSource.project`), the
8
+ * same privacy line (a tool is a name, a label and counts — never its
9
+ * arguments or output), a different lifetime. The live watcher and its ring are
10
+ * left alone.
11
+ *
12
+ * What is uploaded, and how it stays correct:
13
+ *
14
+ * - A chunk is a page of projected events, at most 12 KB of plaintext, sealed
15
+ * with push envelope v1 for the account's pinned devices plus this machine
16
+ * (the relay is not a recipient; there is no plaintext upload at all). The
17
+ * sealed body carries `{sessionKey, seq, events}` so the reader can check the
18
+ * chunk is the one it asked for.
19
+ * - `seq` is per session and only goes up. It starts from the clock when a
20
+ * session has no state (the server allows gaps), so a wiped state directory
21
+ * cannot collide with chunks the server already holds.
22
+ * - Reading and uploading are decoupled by an outbox on disk. A read becomes
23
+ * chunks, and the chunks, the new byte offset and `seq` are written in one
24
+ * atomic replace; only then is anything uploaded, and an upload retries the
25
+ * stored bytes. A crash therefore loses nothing and, because a retry sends
26
+ * the same bytes under the same `seq`, duplicates nothing (the server answers
27
+ * a repeated chunk 200).
28
+ * - The offset is the end of the last complete line that went out (inside a line
29
+ * too long to read, where that line began), so the state file holds numbers and
30
+ * ciphertext, never conversation text. It also names the agent kind, so a
31
+ * session that is gone after a restart is still read to the end of its transcript.
32
+ * - A transcript that is not a file (opencode's SQLite store) is read by its
33
+ * agent's own `TranscriptReader`, and where it was read to is that reader's
34
+ * cursor, kept in the state file beside the offset. The cursor is the reader's
35
+ * to keep free of conversation text (opencode's holds row numbers and keys).
36
+ *
37
+ * Flush rules: events wait for 2 s of quiet, or 12 KB, or a prompt in the read
38
+ * (a person typed — that goes out now). A transcript that changes name
39
+ * (`/clear`, compaction, resume) keeps the session's key; events not newer than
40
+ * the last one sent are dropped on every read of a new file until the first
41
+ * newer one, because a new file may start with a copy of the old history.
42
+ *
43
+ * Every dependency that touches the world is injected, so tests run real ticks
44
+ * on a temporary directory with a fake clock and no network.
45
+ */
46
+ import { type TranscriptProjector, type TranscriptReader } from './transcript-tail.js';
47
+ /** Poll cadence for every observed session; an idle transcript costs one `stat`. */
48
+ export declare const RECORDER_TICK_MS = 1000;
49
+ /** Quiet time after the last append before pending events go out. */
50
+ export declare const RECORDER_QUIET_MS = 2000;
51
+ /** Plaintext bytes that flush on their own (the live frame's budget, `MAX_TURN_FRAME_BYTES`). */
52
+ export declare const RECORDER_FLUSH_BYTES: number;
53
+ /** First run on a session: only the last this-many events of the transcript. */
54
+ export declare const RECORDER_BACKFILL_EVENTS = 400;
55
+ /** Chunks waiting to upload, per session; past it nothing more is read until some go out. */
56
+ export declare const RECORDER_OUTBOX_MAX = 20;
57
+ /** Recipient slots on one chunk: the server's `deviceKeyMap` limit. */
58
+ export declare const RECORDER_MAX_RECIPIENTS = 50;
59
+ /** Retry backoff for a failing upload: doubles from the base up to the cap. */
60
+ export declare const RECORDER_BACKOFF_BASE_MS = 2000;
61
+ export declare const RECORDER_BACKOFF_MAX_MS: number;
62
+ /** How long a session is left alone after the server refused it (a daily cap, a bad key). */
63
+ export declare const RECORDER_PAUSE_MS: number;
64
+ /** A reader's transcript a session has left is over once it has read as nothing new this long: a reader
65
+ * answers nothing also when it did not look (opencode asks its store at most once a second, and not
66
+ * again at once after a failed query), so one empty read says nothing. */
67
+ export declare const RECORDER_READER_SETTLE_MS = 3000;
68
+ /** A session no longer in the inventory is followed this long, so its last words still go out. */
69
+ export declare const RECORDER_LINGER_MS = 60000;
70
+ /** State files untouched this long, with nothing waiting, are deleted at start. */
71
+ export declare const RECORDER_STATE_TTL_MS: number;
72
+ /** Read-but-unsealed events held in memory; past this a read waits (the transcript keeps them). */
73
+ export declare const RECORDER_PENDING_MAX_BYTES: number;
74
+ /** How long a tick waits to learn who a chunk is sealed for; past it nothing is read this round. */
75
+ export declare const RECORDER_RECIPIENTS_TIMEOUT_MS = 10000;
76
+ /** `seq` of a session with no state: seconds since the epoch × this (the server takes up to 10^12 - 1). */
77
+ export declare const RECORDER_SEQ_PER_SECOND = 100;
78
+ export interface RecordableSession {
79
+ /** The transcript this session writes right now. */
80
+ path: string;
81
+ /** `RegisteredRemoteAgent.kind` — which projector reads it. */
82
+ kind: string;
83
+ }
84
+ /** A pinned device's key, or this machine's own. */
85
+ export interface RecorderRecipient {
86
+ deviceId: string;
87
+ publicKey: string;
88
+ }
89
+ /** The push-v1 envelope of one chunk, as the server stores it. */
90
+ export interface SealedChunk {
91
+ body: string;
92
+ deviceKeyMap: Record<string, string>;
93
+ senderPublicKey: string;
94
+ }
95
+ export interface OutboxChunk {
96
+ seq: number;
97
+ firstAt: string;
98
+ lastAt: string;
99
+ sealed: SealedChunk;
100
+ }
101
+ export type UploadOutcome =
102
+ /** Stored (201) or already stored (200). */
103
+ {
104
+ kind: 'ok';
105
+ }
106
+ /** Try again later: network, 5xx, per-minute rate limit. */
107
+ | {
108
+ kind: 'retry';
109
+ afterMs?: number;
110
+ why: string;
111
+ }
112
+ /** The server will not take this now (daily cap, wrong key or device): leave the session alone for a while. */
113
+ | {
114
+ kind: 'pause';
115
+ untilMs?: number;
116
+ why: string;
117
+ giveUpAfter?: number;
118
+ }
119
+ /** The server will never take these bytes (a bad body, a `seq` already holding another): drop the chunk, or it blocks the session for good. */
120
+ | {
121
+ kind: 'drop';
122
+ why: string;
123
+ conflict?: boolean;
124
+ };
125
+ export interface TurnRecorderDeps {
126
+ /** Where per-session state lives (`<stateDir>/turn-recorder`). */
127
+ dir: string;
128
+ deviceId: () => string;
129
+ /** The projector for an agent kind, or null when its transcript is not one this can read. */
130
+ projectorFor: (kind: string) => TranscriptProjector | null;
131
+ /** How an agent kind's transcript is read when it is not an append-only file (`TranscriptSource.read`); null reads the file. */
132
+ readerFor?: (kind: string) => TranscriptReader | null;
133
+ /** Who a chunk is sealed for. Empty = nobody could read it, so nothing is read or sent. Never rejects. */
134
+ recipients: () => Promise<RecorderRecipient[]>;
135
+ /** Seal one plaintext body for those recipients. Rejecting leaves everything as it was. */
136
+ seal: (plaintext: string, recipients: RecorderRecipient[]) => Promise<SealedChunk>;
137
+ upload: (sessionKey: string, chunk: OutboxChunk) => Promise<UploadOutcome>;
138
+ /** False until this machine's device key is on its device record: the server takes a chunk only
139
+ * when it is sealed with that key. Chunks are still read and sealed meanwhile; they wait. */
140
+ uploadReady?: () => boolean;
141
+ /** This machine's current device public key. A stored chunk sealed with another one (the
142
+ * key file was lost and made anew) can never be accepted — the server wants the key on
143
+ * the device record — so it is dropped rather than refused for ever. */
144
+ devicePublicKey?: () => string | null;
145
+ /** The newest `seq` the server holds for a session, or null when it cannot be read. After a 409
146
+ * (the same `seq` under another body) the next chunk is numbered past it. */
147
+ latestSeq?: (sessionKey: string) => Promise<number | null>;
148
+ log: (message: string) => void;
149
+ now?: () => number;
150
+ }
151
+ interface SessionState {
152
+ v: 1;
153
+ sessionKey: string;
154
+ /** The transcript the offset belongs to. */
155
+ path: string;
156
+ /** The agent kind that wrote it, so a session that is gone after a restart can still be read to its end. */
157
+ kind?: string;
158
+ ino: number;
159
+ /** End of the last complete line whose events are in a chunk. */
160
+ offset: number;
161
+ /** Where the agent's own reader had read to when the chunks were made (`TailState.cursor`); only a reader's transcript has one. */
162
+ cursor?: unknown;
163
+ /** The last `seq` handed out. */
164
+ seq: number;
165
+ /** Newest event time sent so far. */
166
+ lastAt?: string;
167
+ outbox: OutboxChunk[];
168
+ }
169
+ /** What is on disk, or null when it is missing or not ours (a bad file is a fresh start, never a crash). */
170
+ export declare const readSessionState: (dir: string, sessionKey: string) => SessionState | null;
171
+ /**
172
+ * Delete state files nothing has touched for a long while, nothing is waiting in, and whose
173
+ * transcript is gone. A session that is quiet but alive keeps its state: without it the next
174
+ * read takes the last 400 events of the transcript again and uploads them under new `seq`s.
175
+ */
176
+ export declare const sweepRecorderState: (dir: string, now?: number) => void;
177
+ export declare const createTurnRecorder: (deps: TurnRecorderDeps) => {
178
+ observe: (found: Readonly<Record<string, RecordableSession>>) => void;
179
+ tick: () => Promise<void>;
180
+ start: () => void;
181
+ stop: () => void;
182
+ size: () => number;
183
+ };
184
+ export type TurnRecorder = ReturnType<typeof createTurnRecorder>;
185
+ /** The recipients of a chunk: this machine, then the newest pinned devices, within the server's slot limit. */
186
+ export declare const chooseRecipients: (self: RecorderRecipient | null, pinned: ReadonlyArray<RecorderRecipient & {
187
+ pinnedAt?: string;
188
+ }>) => RecorderRecipient[];
189
+ export {};
190
+ //# sourceMappingURL=turn-recorder.d.ts.map