@sentropic/h2a-runtime 0.81.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 (253) hide show
  1. package/dist/account-pool.d.ts +168 -0
  2. package/dist/account-pool.d.ts.map +1 -0
  3. package/dist/account-pool.js +385 -0
  4. package/dist/account-pool.js.map +1 -0
  5. package/dist/agents-projection.d.ts +56 -0
  6. package/dist/agents-projection.d.ts.map +1 -0
  7. package/dist/agents-projection.js +82 -0
  8. package/dist/agents-projection.js.map +1 -0
  9. package/dist/attach.d.ts +75 -0
  10. package/dist/attach.d.ts.map +1 -0
  11. package/dist/attach.js +432 -0
  12. package/dist/attach.js.map +1 -0
  13. package/dist/auth-bundle.d.ts +21 -0
  14. package/dist/auth-bundle.d.ts.map +1 -0
  15. package/dist/auth-bundle.js +112 -0
  16. package/dist/auth-bundle.js.map +1 -0
  17. package/dist/auth-diagnostics.d.ts +18 -0
  18. package/dist/auth-diagnostics.d.ts.map +1 -0
  19. package/dist/auth-diagnostics.js +23 -0
  20. package/dist/auth-diagnostics.js.map +1 -0
  21. package/dist/auth-login.d.ts +9 -0
  22. package/dist/auth-login.d.ts.map +1 -0
  23. package/dist/auth-login.js +21 -0
  24. package/dist/auth-login.js.map +1 -0
  25. package/dist/auth-refresh.d.ts +32 -0
  26. package/dist/auth-refresh.d.ts.map +1 -0
  27. package/dist/auth-refresh.js +83 -0
  28. package/dist/auth-refresh.js.map +1 -0
  29. package/dist/auth-tools.d.ts +55 -0
  30. package/dist/auth-tools.d.ts.map +1 -0
  31. package/dist/auth-tools.js +163 -0
  32. package/dist/auth-tools.js.map +1 -0
  33. package/dist/browser-bridge-local.d.ts +27 -0
  34. package/dist/browser-bridge-local.d.ts.map +1 -0
  35. package/dist/browser-bridge-local.js +51 -0
  36. package/dist/browser-bridge-local.js.map +1 -0
  37. package/dist/browser.d.ts +53 -0
  38. package/dist/browser.d.ts.map +1 -0
  39. package/dist/browser.js +61 -0
  40. package/dist/browser.js.map +1 -0
  41. package/dist/conductor-launch.d.ts +179 -0
  42. package/dist/conductor-launch.d.ts.map +1 -0
  43. package/dist/conductor-launch.js +414 -0
  44. package/dist/conductor-launch.js.map +1 -0
  45. package/dist/config.d.ts +182 -0
  46. package/dist/config.d.ts.map +1 -0
  47. package/dist/config.js +334 -0
  48. package/dist/config.js.map +1 -0
  49. package/dist/conv-guard.d.ts +76 -0
  50. package/dist/conv-guard.d.ts.map +1 -0
  51. package/dist/conv-guard.js +131 -0
  52. package/dist/conv-guard.js.map +1 -0
  53. package/dist/convsync.d.ts +24 -0
  54. package/dist/convsync.d.ts.map +1 -0
  55. package/dist/convsync.js +108 -0
  56. package/dist/convsync.js.map +1 -0
  57. package/dist/cred-health.d.ts +96 -0
  58. package/dist/cred-health.d.ts.map +1 -0
  59. package/dist/cred-health.js +170 -0
  60. package/dist/cred-health.js.map +1 -0
  61. package/dist/delegate.d.ts +406 -0
  62. package/dist/delegate.d.ts.map +1 -0
  63. package/dist/delegate.js +739 -0
  64. package/dist/delegate.js.map +1 -0
  65. package/dist/enroll.d.ts +91 -0
  66. package/dist/enroll.d.ts.map +1 -0
  67. package/dist/enroll.js +248 -0
  68. package/dist/enroll.js.map +1 -0
  69. package/dist/fanout.d.ts +63 -0
  70. package/dist/fanout.d.ts.map +1 -0
  71. package/dist/fanout.js +115 -0
  72. package/dist/fanout.js.map +1 -0
  73. package/dist/forward.d.ts +66 -0
  74. package/dist/forward.d.ts.map +1 -0
  75. package/dist/forward.js +141 -0
  76. package/dist/forward.js.map +1 -0
  77. package/dist/gitdiff.d.ts +31 -0
  78. package/dist/gitdiff.d.ts.map +1 -0
  79. package/dist/gitdiff.js +133 -0
  80. package/dist/gitdiff.js.map +1 -0
  81. package/dist/h2a-bridge.d.ts +102 -0
  82. package/dist/h2a-bridge.d.ts.map +1 -0
  83. package/dist/h2a-bridge.js +333 -0
  84. package/dist/h2a-bridge.js.map +1 -0
  85. package/dist/h2a-jobs.d.ts +235 -0
  86. package/dist/h2a-jobs.d.ts.map +1 -0
  87. package/dist/h2a-jobs.js +388 -0
  88. package/dist/h2a-jobs.js.map +1 -0
  89. package/dist/h2a-ping.d.ts +38 -0
  90. package/dist/h2a-ping.d.ts.map +1 -0
  91. package/dist/h2a-ping.js +58 -0
  92. package/dist/h2a-ping.js.map +1 -0
  93. package/dist/index.d.ts +98 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +5887 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/interactive-throttle.d.ts +164 -0
  98. package/dist/interactive-throttle.d.ts.map +1 -0
  99. package/dist/interactive-throttle.js +194 -0
  100. package/dist/interactive-throttle.js.map +1 -0
  101. package/dist/lineage-client.d.ts +77 -0
  102. package/dist/lineage-client.d.ts.map +1 -0
  103. package/dist/lineage-client.js +117 -0
  104. package/dist/lineage-client.js.map +1 -0
  105. package/dist/lineage-lease.d.ts +152 -0
  106. package/dist/lineage-lease.d.ts.map +1 -0
  107. package/dist/lineage-lease.js +303 -0
  108. package/dist/lineage-lease.js.map +1 -0
  109. package/dist/llm-gateway-runtime/accounts.d.ts +48 -0
  110. package/dist/llm-gateway-runtime/accounts.d.ts.map +1 -0
  111. package/dist/llm-gateway-runtime/accounts.js +226 -0
  112. package/dist/llm-gateway-runtime/accounts.js.map +1 -0
  113. package/dist/llm-gateway-runtime/index.d.ts +4 -0
  114. package/dist/llm-gateway-runtime/index.d.ts.map +1 -0
  115. package/dist/llm-gateway-runtime/index.js +58 -0
  116. package/dist/llm-gateway-runtime/index.js.map +1 -0
  117. package/dist/llm-gateway-runtime/model-catalog.d.ts +36 -0
  118. package/dist/llm-gateway-runtime/model-catalog.d.ts.map +1 -0
  119. package/dist/llm-gateway-runtime/model-catalog.js +146 -0
  120. package/dist/llm-gateway-runtime/model-catalog.js.map +1 -0
  121. package/dist/llm-gateway-runtime/proxy-anthropic.d.ts +3 -0
  122. package/dist/llm-gateway-runtime/proxy-anthropic.d.ts.map +1 -0
  123. package/dist/llm-gateway-runtime/proxy-anthropic.js +139 -0
  124. package/dist/llm-gateway-runtime/proxy-anthropic.js.map +1 -0
  125. package/dist/llm-gateway-runtime/proxy-openai.d.ts +117 -0
  126. package/dist/llm-gateway-runtime/proxy-openai.d.ts.map +1 -0
  127. package/dist/llm-gateway-runtime/proxy-openai.js +1132 -0
  128. package/dist/llm-gateway-runtime/proxy-openai.js.map +1 -0
  129. package/dist/llm-gateway-runtime/session-ledger.d.ts +32 -0
  130. package/dist/llm-gateway-runtime/session-ledger.d.ts.map +1 -0
  131. package/dist/llm-gateway-runtime/session-ledger.js +62 -0
  132. package/dist/llm-gateway-runtime/session-ledger.js.map +1 -0
  133. package/dist/llm-gateway-runtime/sticky.d.ts +40 -0
  134. package/dist/llm-gateway-runtime/sticky.d.ts.map +1 -0
  135. package/dist/llm-gateway-runtime/sticky.js +254 -0
  136. package/dist/llm-gateway-runtime/sticky.js.map +1 -0
  137. package/dist/llm-mesh.d.ts +102 -0
  138. package/dist/llm-mesh.d.ts.map +1 -0
  139. package/dist/llm-mesh.js +433 -0
  140. package/dist/llm-mesh.js.map +1 -0
  141. package/dist/migrate-candidates.d.ts +35 -0
  142. package/dist/migrate-candidates.d.ts.map +1 -0
  143. package/dist/migrate-candidates.js +128 -0
  144. package/dist/migrate-candidates.js.map +1 -0
  145. package/dist/migrate.d.ts +125 -0
  146. package/dist/migrate.d.ts.map +1 -0
  147. package/dist/migrate.js +561 -0
  148. package/dist/migrate.js.map +1 -0
  149. package/dist/plugin-manifest.d.ts +143 -0
  150. package/dist/plugin-manifest.d.ts.map +1 -0
  151. package/dist/plugin-manifest.js +233 -0
  152. package/dist/plugin-manifest.js.map +1 -0
  153. package/dist/plugin.d.ts +274 -0
  154. package/dist/plugin.d.ts.map +1 -0
  155. package/dist/plugin.js +919 -0
  156. package/dist/plugin.js.map +1 -0
  157. package/dist/pod-liveness.d.ts +51 -0
  158. package/dist/pod-liveness.d.ts.map +1 -0
  159. package/dist/pod-liveness.js +80 -0
  160. package/dist/pod-liveness.js.map +1 -0
  161. package/dist/profile-menu.d.ts +7 -0
  162. package/dist/profile-menu.d.ts.map +1 -0
  163. package/dist/profile-menu.js +52 -0
  164. package/dist/profile-menu.js.map +1 -0
  165. package/dist/profiles.d.ts +23 -0
  166. package/dist/profiles.d.ts.map +1 -0
  167. package/dist/profiles.js +66 -0
  168. package/dist/profiles.js.map +1 -0
  169. package/dist/protocol-local.d.ts +27 -0
  170. package/dist/protocol-local.d.ts.map +1 -0
  171. package/dist/protocol-local.js +12 -0
  172. package/dist/protocol-local.js.map +1 -0
  173. package/dist/pty.d.ts +26 -0
  174. package/dist/pty.d.ts.map +1 -0
  175. package/dist/pty.js +45 -0
  176. package/dist/pty.js.map +1 -0
  177. package/dist/readiness.d.ts +38 -0
  178. package/dist/readiness.d.ts.map +1 -0
  179. package/dist/readiness.js +169 -0
  180. package/dist/readiness.js.map +1 -0
  181. package/dist/registry.d.ts +258 -0
  182. package/dist/registry.d.ts.map +1 -0
  183. package/dist/registry.js +570 -0
  184. package/dist/registry.js.map +1 -0
  185. package/dist/relaunch.d.ts +52 -0
  186. package/dist/relaunch.d.ts.map +1 -0
  187. package/dist/relaunch.js +76 -0
  188. package/dist/relaunch.js.map +1 -0
  189. package/dist/restore.d.ts +165 -0
  190. package/dist/restore.d.ts.map +1 -0
  191. package/dist/restore.js +530 -0
  192. package/dist/restore.js.map +1 -0
  193. package/dist/run.d.ts +29 -0
  194. package/dist/run.d.ts.map +1 -0
  195. package/dist/run.js +206 -0
  196. package/dist/run.js.map +1 -0
  197. package/dist/secrets.d.ts +25 -0
  198. package/dist/secrets.d.ts.map +1 -0
  199. package/dist/secrets.js +98 -0
  200. package/dist/secrets.js.map +1 -0
  201. package/dist/session-restore.d.ts +26 -0
  202. package/dist/session-restore.d.ts.map +1 -0
  203. package/dist/session-restore.js +122 -0
  204. package/dist/session-restore.js.map +1 -0
  205. package/dist/skills-sync.d.ts +126 -0
  206. package/dist/skills-sync.d.ts.map +1 -0
  207. package/dist/skills-sync.js +255 -0
  208. package/dist/skills-sync.js.map +1 -0
  209. package/dist/smoke.d.ts +23 -0
  210. package/dist/smoke.d.ts.map +1 -0
  211. package/dist/smoke.js +138 -0
  212. package/dist/smoke.js.map +1 -0
  213. package/dist/soft-refresh.d.ts +107 -0
  214. package/dist/soft-refresh.d.ts.map +1 -0
  215. package/dist/soft-refresh.js +426 -0
  216. package/dist/soft-refresh.js.map +1 -0
  217. package/dist/sync-status.d.ts +22 -0
  218. package/dist/sync-status.d.ts.map +1 -0
  219. package/dist/sync-status.js +41 -0
  220. package/dist/sync-status.js.map +1 -0
  221. package/dist/sync.d.ts +77 -0
  222. package/dist/sync.d.ts.map +1 -0
  223. package/dist/sync.js +302 -0
  224. package/dist/sync.js.map +1 -0
  225. package/dist/throttle-signatures.d.ts +43 -0
  226. package/dist/throttle-signatures.d.ts.map +1 -0
  227. package/dist/throttle-signatures.js +116 -0
  228. package/dist/throttle-signatures.js.map +1 -0
  229. package/dist/tmux.d.ts +286 -0
  230. package/dist/tmux.d.ts.map +1 -0
  231. package/dist/tmux.js +970 -0
  232. package/dist/tmux.js.map +1 -0
  233. package/dist/tunnel.d.ts +25 -0
  234. package/dist/tunnel.d.ts.map +1 -0
  235. package/dist/tunnel.js +127 -0
  236. package/dist/tunnel.js.map +1 -0
  237. package/dist/workspace-merge.d.ts +17 -0
  238. package/dist/workspace-merge.d.ts.map +1 -0
  239. package/dist/workspace-merge.js +153 -0
  240. package/dist/workspace-merge.js.map +1 -0
  241. package/dist/workspace-sync-incremental.d.ts +64 -0
  242. package/dist/workspace-sync-incremental.d.ts.map +1 -0
  243. package/dist/workspace-sync-incremental.js +199 -0
  244. package/dist/workspace-sync-incremental.js.map +1 -0
  245. package/dist/workspace-sync.d.ts +3 -0
  246. package/dist/workspace-sync.d.ts.map +1 -0
  247. package/dist/workspace-sync.js +187 -0
  248. package/dist/workspace-sync.js.map +1 -0
  249. package/dist/workspace.d.ts +70 -0
  250. package/dist/workspace.d.ts.map +1 -0
  251. package/dist/workspace.js +173 -0
  252. package/dist/workspace.js.map +1 -0
  253. package/package.json +44 -0
package/dist/sync.js ADDED
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Conversation sync between local and a session Pod (`remote sync`): copy the
3
+ * conversation .jsonl in either direction over `kubectl exec`, base64 on the
4
+ * wire (encoded exactly ONCE — never double-encode, that was a real bug), with
5
+ * an ahead-GUARD so the side that has MORE of the conversation is never
6
+ * silently overwritten (--force to override), and a `.bak-<epoch>` backup of
7
+ * whatever gets overwritten, taken BEFORE writing. Conversation content is
8
+ * transferred verbatim but never printed.
9
+ */
10
+ import { spawnSync } from "node:child_process";
11
+ import { createHash } from "node:crypto";
12
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { dirname, join } from "node:path";
15
+ import { getTunnel } from "./config.js";
16
+ import { encodeCwd, localConvStat, remoteConvStat } from "./convsync.js";
17
+ const CLAUDE_PROJECTS = ".claude/projects";
18
+ /**
19
+ * Pure GUARD: never overwrite the side that has MORE of the conversation.
20
+ * `pull` overwrites local → refuse if local is ahead; `push` overwrites the
21
+ * Pod → refuse if remote is ahead. `--force` overrides (a backup is still
22
+ * taken either way).
23
+ */
24
+ export function decideSyncAction(args) {
25
+ const { localLines, remoteLines, direction, force } = args;
26
+ if (force)
27
+ return { allow: true };
28
+ if (direction === "pull" && localLines > remoteLines) {
29
+ return {
30
+ allow: false,
31
+ reason: `local conversation is ahead (${localLines} vs ${remoteLines} lines) — ` +
32
+ `pull would lose ${localLines - remoteLines} local line(s). ` +
33
+ `Push instead, or re-run with --force to overwrite local anyway.`,
34
+ };
35
+ }
36
+ if (direction === "push" && remoteLines > localLines) {
37
+ return {
38
+ allow: false,
39
+ reason: `remote conversation is ahead (${remoteLines} vs ${localLines} lines) — ` +
40
+ `push would lose ${remoteLines - localLines} remote line(s). ` +
41
+ `Pull instead, or re-run with --force to overwrite remote anyway.`,
42
+ };
43
+ }
44
+ return { allow: true };
45
+ }
46
+ /** Local path of a conversation file for a workspace (claude cwd encoding). */
47
+ export function localConvFile(workspacePath, convId, home = homedir()) {
48
+ return join(home, CLAUDE_PROJECTS, encodeCwd(workspacePath), `${convId}.jsonl`);
49
+ }
50
+ /** Pod-side conversation path, relative to $HOME (used as "$HOME/<rel>"). */
51
+ export function remoteConvRel(workspacePath, convId) {
52
+ return `${CLAUDE_PROJECTS}/${encodeCwd(workspacePath)}/${convId}.jsonl`;
53
+ }
54
+ function expandHome(p) {
55
+ return p.startsWith("~") ? join(homedir(), p.slice(1)) : p;
56
+ }
57
+ /** Exec a bash -lc script in the session-agent container. Throws on non-zero. */
58
+ function execPod(tunnel, pod, script, input) {
59
+ const args = ["-n", tunnel.namespace, "exec"];
60
+ if (input !== undefined)
61
+ args.push("-i");
62
+ args.push(pod, "-c", "session-agent", "--", "bash", "-lc", script);
63
+ const env = { ...process.env };
64
+ if (tunnel.kubeconfig)
65
+ env.KUBECONFIG = expandHome(tunnel.kubeconfig);
66
+ const r = spawnSync("kubectl", args, {
67
+ encoding: "utf8",
68
+ env,
69
+ maxBuffer: 512 * 1024 * 1024, // conversations can be many MB of base64
70
+ ...(input !== undefined ? { input } : {}),
71
+ });
72
+ if (r.status !== 0) {
73
+ // stderr only (kubectl/bash diagnostics) — never conversation content.
74
+ throw new Error(`kubectl exec failed: ${(r.stderr || "").trim().slice(0, 200)}`);
75
+ }
76
+ return r.stdout;
77
+ }
78
+ /** Like execPod but never throws — returns {stdout, status}. */
79
+ export function execPodRaw(tunnel, pod, script, input) {
80
+ const args = ["-n", tunnel.namespace, "exec"];
81
+ if (input !== undefined)
82
+ args.push("-i");
83
+ args.push(pod, "-c", "session-agent", "--", "bash", "-lc", script);
84
+ const env = { ...process.env };
85
+ if (tunnel.kubeconfig)
86
+ env.KUBECONFIG = expandHome(tunnel.kubeconfig);
87
+ const r = spawnSync("kubectl", args, {
88
+ encoding: "utf8",
89
+ env,
90
+ maxBuffer: 512 * 1024 * 1024,
91
+ ...(input !== undefined ? { input } : {}),
92
+ });
93
+ return { stdout: r.stdout ?? "", status: r.status ?? 1 };
94
+ }
95
+ /** Non-empty line count of a local file (same convention as convsync). */
96
+ function countLines(file) {
97
+ if (!existsSync(file))
98
+ return 0;
99
+ return readFileSync(file, "utf8").split("\n").filter(Boolean).length;
100
+ }
101
+ function sha256Bytes(buf) {
102
+ return createHash("sha256").update(buf).digest("hex");
103
+ }
104
+ function convSyncStatePath(convId, home = homedir()) {
105
+ return join(home, ".remote", "conv-sync-state", convId + ".json");
106
+ }
107
+ export function readConvSyncState(convId, home) {
108
+ const p = convSyncStatePath(convId, home);
109
+ if (!existsSync(p))
110
+ return undefined;
111
+ try {
112
+ return JSON.parse(readFileSync(p, "utf8"));
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
118
+ export function writeConvSyncState(state, convId, home) {
119
+ const p = convSyncStatePath(convId, home);
120
+ mkdirSync(dirname(p), { recursive: true });
121
+ // Atomic write: temp file in the same dir as target so rename is same-fs.
122
+ const tmp = `${p}.tmp`;
123
+ writeFileSync(tmp, JSON.stringify(state));
124
+ renameSync(tmp, p);
125
+ }
126
+ /**
127
+ * Copy the conversation .jsonl between local and the Pod. `pull`: Pod → local
128
+ * (guard: refuse if local is ahead). `push`: local → Pod (guard: refuse if
129
+ * remote is ahead). Both back up the overwritten file as `.bak-<epoch>` first.
130
+ */
131
+ export function syncConversation(args) {
132
+ const { sessionId, workspacePath, direction, force } = args;
133
+ const tunnel = getTunnel();
134
+ if (!tunnel) {
135
+ throw new Error("sync needs a tunnel configured (remote config tunnel …)");
136
+ }
137
+ const pod = `session-${sessionId}`;
138
+ const epoch = Math.floor(Date.now() / 1000);
139
+ if (direction === "pull") {
140
+ const remote = remoteConvStat(sessionId, workspacePath);
141
+ if (!remote) {
142
+ return { ok: false, reason: "remote has no conversation (or the Pod is unreachable)" };
143
+ }
144
+ const dst = localConvFile(workspacePath, remote.convId);
145
+ const localLines = countLines(dst);
146
+ const decision = decideSyncAction({
147
+ localLines,
148
+ remoteLines: remote.lines,
149
+ direction,
150
+ force,
151
+ });
152
+ if (!decision.allow)
153
+ return { ok: false, reason: decision.reason };
154
+ const rel = remoteConvRel(workspacePath, remote.convId);
155
+ // --- Incremental path (Phase B1) - pull ---
156
+ const pullState = readConvSyncState(remote.convId);
157
+ if (pullState && pullState.offset > 0) {
158
+ if (existsSync(dst)) {
159
+ const localBuf = readFileSync(dst);
160
+ if (localBuf.length >= pullState.offset) {
161
+ const localPrefixHash = sha256Bytes(localBuf.slice(0, pullState.offset));
162
+ if (localPrefixHash === pullState.prefixHash) {
163
+ // Fetch only the tail from pod
164
+ const deltaB64Result = execPodRaw(tunnel, pod, `f="$HOME/${rel}"; ` +
165
+ `actual=$(head -c ${pullState.offset} "$f" 2>/dev/null | sha256sum | cut -d' ' -f1); ` +
166
+ `if [ "$actual" = "${pullState.prefixHash}" ]; then tail -c +${pullState.offset + 1} "$f" | base64 | tr -d '\\n'; else exit 42; fi`);
167
+ if (deltaB64Result.status === 0 && deltaB64Result.stdout.trim()) {
168
+ const delta = Buffer.from(deltaB64Result.stdout.trim(), "base64");
169
+ const newBuf = Buffer.concat([localBuf, delta]);
170
+ writeFileSync(dst, newBuf);
171
+ const newHash = sha256Bytes(newBuf);
172
+ writeConvSyncState({
173
+ offset: newBuf.length,
174
+ prefixHash: newHash,
175
+ generation: pullState.generation + 1,
176
+ updatedAt: new Date().toISOString(),
177
+ }, remote.convId);
178
+ return {
179
+ ok: true,
180
+ direction,
181
+ convId: remote.convId,
182
+ lines: { local: localLines, remote: remote.lines },
183
+ backup: undefined,
184
+ written: dst,
185
+ incremental: true,
186
+ };
187
+ }
188
+ }
189
+ }
190
+ }
191
+ }
192
+ // --- End incremental pull path — fallback to whole-file ---
193
+ // Encoded ONCE in the Pod, decoded ONCE here — never re-encode.
194
+ const b64 = execPod(tunnel, pod, `base64 < "$HOME/${rel}" | tr -d '\\n'`);
195
+ const buf = Buffer.from(b64.trim(), "base64");
196
+ let backup;
197
+ if (existsSync(dst)) {
198
+ backup = `${dst}.bak-${epoch}`;
199
+ copyFileSync(dst, backup); // BEFORE overwriting
200
+ }
201
+ mkdirSync(dirname(dst), { recursive: true });
202
+ writeFileSync(dst, buf);
203
+ // Persist sync state for future incremental syncs.
204
+ writeConvSyncState({
205
+ offset: buf.length,
206
+ prefixHash: sha256Bytes(buf),
207
+ generation: (readConvSyncState(remote.convId)?.generation ?? 0) + 1,
208
+ updatedAt: new Date().toISOString(),
209
+ }, remote.convId);
210
+ return {
211
+ ok: true,
212
+ direction,
213
+ convId: remote.convId,
214
+ lines: { local: localLines, remote: remote.lines },
215
+ backup,
216
+ written: dst,
217
+ incremental: false,
218
+ };
219
+ }
220
+ // push: local → Pod.
221
+ const local = localConvStat(workspacePath);
222
+ if (!local) {
223
+ return { ok: false, reason: "no local conversation for this workspace" };
224
+ }
225
+ const src = localConvFile(workspacePath, local.convId);
226
+ const rel = remoteConvRel(workspacePath, local.convId);
227
+ const probe = execPod(tunnel, pod, `f="$HOME/${rel}"; if [ -f "$f" ]; then printf 'yes\\t%s' "$(wc -l <"$f")"; else printf 'no\\t0'; fi`)
228
+ .trim()
229
+ .split("\t");
230
+ const remoteExists = probe[0] === "yes";
231
+ const remoteLines = Number(probe[1] ?? 0) || 0;
232
+ const decision = decideSyncAction({
233
+ localLines: local.lines,
234
+ remoteLines,
235
+ direction,
236
+ force,
237
+ });
238
+ if (!decision.allow)
239
+ return { ok: false, reason: decision.reason };
240
+ // --- Incremental path (Phase B1) ---
241
+ const state = readConvSyncState(local.convId);
242
+ if (state && state.offset > 0 && state.offset <= local.bytes) {
243
+ const fullBuf = readFileSync(src);
244
+ const localPrefixHash = sha256Bytes(fullBuf.slice(0, state.offset));
245
+ if (localPrefixHash === state.prefixHash) {
246
+ // Local prefix is intact. Check pod prefix AND append atomically (anti-TOCTOU).
247
+ const delta = fullBuf.slice(state.offset);
248
+ const deltaB64 = delta.toString("base64");
249
+ const result = execPodRaw(tunnel, pod,
250
+ // head -c reads <offset> bytes, sha256sum -c checks against expected hash.
251
+ // If mismatch → exit 42 (not 1, to distinguish from other errors).
252
+ // If match → base64 -d appends the delta.
253
+ `f="$HOME/${rel}"; ` +
254
+ `actual=$(head -c ${state.offset} "$f" 2>/dev/null | sha256sum | cut -d' ' -f1); ` +
255
+ `if [ "$actual" = "${state.prefixHash}" ]; then base64 -d >> "$f"; else exit 42; fi`, deltaB64);
256
+ if (result.status === 0) {
257
+ const newHash = sha256Bytes(fullBuf);
258
+ writeConvSyncState({
259
+ offset: fullBuf.length,
260
+ prefixHash: newHash,
261
+ generation: state.generation + 1,
262
+ updatedAt: new Date().toISOString(),
263
+ }, local.convId);
264
+ return {
265
+ ok: true,
266
+ direction,
267
+ convId: local.convId,
268
+ lines: { local: local.lines, remote: remoteLines },
269
+ backup: undefined,
270
+ written: `$HOME/${rel}`,
271
+ incremental: true,
272
+ };
273
+ }
274
+ // rc=42 = prefix mismatch on pod side → fallthrough to whole-file
275
+ // rc=anything else → also fallthrough (don't throw; let whole-file handle it)
276
+ }
277
+ }
278
+ // --- End incremental path — fallback to whole-file below ---
279
+ // Encoded ONCE here, decoded ONCE in the Pod — never re-encode (historical
280
+ // double-encoding bug: the file would end up containing base64 text).
281
+ const payload = readFileSync(src).toString("base64");
282
+ const backupCmd = remoteExists ? `cp -p "$f" "$f.bak-${epoch}" && ` : "";
283
+ execPod(tunnel, pod, `f="$HOME/${rel}"; mkdir -p "$(dirname "$f")" && ${backupCmd}base64 -d > "$f"`, payload);
284
+ // Persist sync state for future incremental syncs.
285
+ const fullContent = readFileSync(src);
286
+ writeConvSyncState({
287
+ offset: fullContent.length,
288
+ prefixHash: sha256Bytes(fullContent),
289
+ generation: (readConvSyncState(local.convId)?.generation ?? 0) + 1,
290
+ updatedAt: new Date().toISOString(),
291
+ }, local.convId);
292
+ return {
293
+ ok: true,
294
+ direction,
295
+ convId: local.convId,
296
+ lines: { local: local.lines, remote: remoteLines },
297
+ backup: remoteExists ? `$HOME/${rel}.bak-${epoch}` : undefined,
298
+ written: `$HOME/${rel}`,
299
+ incremental: false,
300
+ };
301
+ }
302
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Rate-limit ("throttled") DETECTION for HEADLESS LOCAL delegated jobs
3
+ * (reliability slice 1).
4
+ *
5
+ * When a delegated agent CLI hits a TRANSIENT provider rate-limit it exits ≠0
6
+ * and (without this) is marked `failed` — indistinguishable from a real bug, no
7
+ * retry. This module turns the tail of a finished headless job's `output.log`
8
+ * into a throttle verdict so the conductor can auto-resume with backoff instead
9
+ * of giving up.
10
+ *
11
+ * PURE + conservative by design:
12
+ * - We scan only the LAST ~60 lines (the caller passes the already-extracted
13
+ * tail — `tailLines` here only re-bounds it defensively).
14
+ * - We require the PROVIDER-ERROR SHAPE, not a bare keyword: the model itself
15
+ * may print "rate limit" mid-sentence while explaining something, and that
16
+ * must NOT be read as a throttle. Each tool's table lists regexes anchored to
17
+ * the way THAT provider's CLI emits the error (an `API Error:` / `Error:`
18
+ * prefix, a leading bullet, a line that IS the error rather than prose).
19
+ *
20
+ * SCOPE: headless LOCAL only. The remote/kubectl path and interactive tmux panes
21
+ * are phase 2 (see TODOs in index.ts); the signatures here are reused verbatim
22
+ * when those land — only the tail SOURCE changes (kubectl logs / capture-pane).
23
+ */
24
+ import type { DelegateType } from "./delegate.js";
25
+ export type ThrottleVerdict = {
26
+ throttled: boolean;
27
+ /** A short, human-readable tag for the matched signature (for `throttle.lastSignature`). */
28
+ signature?: string;
29
+ };
30
+ /** Default tail window (lines) — providers print the rate-limit line near the end. */
31
+ export declare const THROTTLE_TAIL_LINES = 60;
32
+ /**
33
+ * Decide whether the tail of a finished headless job's output indicates a
34
+ * transient PROVIDER rate-limit (throttle), vs a real failure. Case-insensitive,
35
+ * line-anchored, conservative (provider-error shape required). Scans only the
36
+ * last `tailLines` lines. Pure, exported for tests.
37
+ *
38
+ * @param tailText the captured tail of `output.log` (caller passes ~last 60
39
+ * lines; we re-bound defensively so an over-long buffer can't widen the scan).
40
+ * @param type the delegate tool (claude | codex | agy) — selects the table.
41
+ */
42
+ export declare function detectThrottle(tailText: string, type: DelegateType, tailLines?: number): ThrottleVerdict;
43
+ //# sourceMappingURL=throttle-signatures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throttle-signatures.d.ts","sourceRoot":"","sources":["../src/throttle-signatures.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,KAAK,CAAC;AA+EtC;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,YAAY,EAClB,SAAS,GAAE,MAA4B,GACtC,eAAe,CAajB"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Rate-limit ("throttled") DETECTION for HEADLESS LOCAL delegated jobs
3
+ * (reliability slice 1).
4
+ *
5
+ * When a delegated agent CLI hits a TRANSIENT provider rate-limit it exits ≠0
6
+ * and (without this) is marked `failed` — indistinguishable from a real bug, no
7
+ * retry. This module turns the tail of a finished headless job's `output.log`
8
+ * into a throttle verdict so the conductor can auto-resume with backoff instead
9
+ * of giving up.
10
+ *
11
+ * PURE + conservative by design:
12
+ * - We scan only the LAST ~60 lines (the caller passes the already-extracted
13
+ * tail — `tailLines` here only re-bounds it defensively).
14
+ * - We require the PROVIDER-ERROR SHAPE, not a bare keyword: the model itself
15
+ * may print "rate limit" mid-sentence while explaining something, and that
16
+ * must NOT be read as a throttle. Each tool's table lists regexes anchored to
17
+ * the way THAT provider's CLI emits the error (an `API Error:` / `Error:`
18
+ * prefix, a leading bullet, a line that IS the error rather than prose).
19
+ *
20
+ * SCOPE: headless LOCAL only. The remote/kubectl path and interactive tmux panes
21
+ * are phase 2 (see TODOs in index.ts); the signatures here are reused verbatim
22
+ * when those land — only the tail SOURCE changes (kubectl logs / capture-pane).
23
+ */
24
+ /** Default tail window (lines) — providers print the rate-limit line near the end. */
25
+ export const THROTTLE_TAIL_LINES = 60;
26
+ /**
27
+ * Per-tool signature tables. Each regex targets ONE line of the CLI's stderr as
28
+ * the provider's transport layer prints it. Conservatism guard: every pattern
29
+ * requires either an error prefix (`API Error:`, `Error:`, a leading `·`/bullet),
30
+ * an HTTP status token (`429`), or the provider's exact transient-error phrasing
31
+ * — never a lone word that could appear in normal model output.
32
+ */
33
+ const SIGNATURES = {
34
+ claude: [
35
+ // `API Error: Server is temporarily limiting requests (not your usage
36
+ // limit) · Rate limited` — the canonical transient claude throttle.
37
+ {
38
+ tag: "claude:temporarily-limiting",
39
+ re: /\btemporarily limiting requests\b/i,
40
+ },
41
+ // An `API Error:` / `Error:` line carrying a rate-limit / overloaded / 429
42
+ // — the error PREFIX is what keeps prose ("the rate limit is 5/min") out.
43
+ {
44
+ tag: "claude:rate-limited",
45
+ re: /(?:api )?error.*\b(?:rate[\s-]?limit(?:ed|ing)?|overloaded|429)\b/i,
46
+ },
47
+ // The provider's bulleted transient marker: `· Rate limited` / `· 429`.
48
+ {
49
+ tag: "claude:bullet-rate-limited",
50
+ re: /(?:·|•|·)\s*(?:rate[\s-]?limited|429|overloaded)\b/i,
51
+ },
52
+ // A line that IS just the HTTP status the gateway returns on overload.
53
+ {
54
+ tag: "claude:http-429",
55
+ re: /\b(?:http\s*)?(?:status\s*)?429\b.*\b(?:too many requests|rate)/i,
56
+ },
57
+ ],
58
+ codex: [
59
+ // codex prints `Rate limit reached` / `Rate limit reached for …`.
60
+ {
61
+ tag: "codex:rate-limit-reached",
62
+ re: /\brate limit reached\b/i,
63
+ },
64
+ // codex transport error carrying a 429 / rate-limit.
65
+ {
66
+ tag: "codex:rate-limited",
67
+ re: /error.*\b(?:rate[\s-]?limit(?:ed)?|429)\b/i,
68
+ },
69
+ {
70
+ tag: "codex:stream-429",
71
+ re: /\bstream error\b.*\b429\b/i,
72
+ },
73
+ ],
74
+ agy: [
75
+ // agy (antigravity) equivalent — quota/rate-limit on a `RESOURCE_EXHAUSTED`
76
+ // / `429` transport error (the Gemini-family transient shape).
77
+ {
78
+ tag: "agy:resource-exhausted",
79
+ re: /\bresource[\s_-]?exhausted\b/i,
80
+ },
81
+ {
82
+ tag: "agy:rate-limited",
83
+ re: /error.*\b(?:rate[\s-]?limit(?:ed)?|quota exceeded|429)\b/i,
84
+ },
85
+ {
86
+ tag: "agy:rate-limit-reached",
87
+ re: /\brate limit\b.*\b(?:reached|exceeded)\b/i,
88
+ },
89
+ ],
90
+ };
91
+ /**
92
+ * Decide whether the tail of a finished headless job's output indicates a
93
+ * transient PROVIDER rate-limit (throttle), vs a real failure. Case-insensitive,
94
+ * line-anchored, conservative (provider-error shape required). Scans only the
95
+ * last `tailLines` lines. Pure, exported for tests.
96
+ *
97
+ * @param tailText the captured tail of `output.log` (caller passes ~last 60
98
+ * lines; we re-bound defensively so an over-long buffer can't widen the scan).
99
+ * @param type the delegate tool (claude | codex | agy) — selects the table.
100
+ */
101
+ export function detectThrottle(tailText, type, tailLines = THROTTLE_TAIL_LINES) {
102
+ if (!tailText)
103
+ return { throttled: false };
104
+ const lines = tailText.split(/\r?\n/);
105
+ const scan = lines.slice(Math.max(0, lines.length - tailLines));
106
+ const table = SIGNATURES[type];
107
+ for (const line of scan) {
108
+ for (const sig of table) {
109
+ if (sig.re.test(line)) {
110
+ return { throttled: true, signature: sig.tag };
111
+ }
112
+ }
113
+ }
114
+ return { throttled: false };
115
+ }
116
+ //# sourceMappingURL=throttle-signatures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"throttle-signatures.js","sourceRoot":"","sources":["../src/throttle-signatures.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAUH,sFAAsF;AACtF,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAatC;;;;;;GAMG;AACH,MAAM,UAAU,GAA6D;IAC3E,MAAM,EAAE;QACN,sEAAsE;QACtE,oEAAoE;QACpE;YACE,GAAG,EAAE,6BAA6B;YAClC,EAAE,EAAE,oCAAoC;SACzC;QACD,2EAA2E;QAC3E,0EAA0E;QAC1E;YACE,GAAG,EAAE,qBAAqB;YAC1B,EAAE,EAAE,oEAAoE;SACzE;QACD,wEAAwE;QACxE;YACE,GAAG,EAAE,4BAA4B;YACjC,EAAE,EAAE,qDAAqD;SAC1D;QACD,uEAAuE;QACvE;YACE,GAAG,EAAE,iBAAiB;YACtB,EAAE,EAAE,kEAAkE;SACvE;KACF;IACD,KAAK,EAAE;QACL,kEAAkE;QAClE;YACE,GAAG,EAAE,0BAA0B;YAC/B,EAAE,EAAE,yBAAyB;SAC9B;QACD,qDAAqD;QACrD;YACE,GAAG,EAAE,oBAAoB;YACzB,EAAE,EAAE,4CAA4C;SACjD;QACD;YACE,GAAG,EAAE,kBAAkB;YACvB,EAAE,EAAE,4BAA4B;SACjC;KACF;IACD,GAAG,EAAE;QACH,4EAA4E;QAC5E,+DAA+D;QAC/D;YACE,GAAG,EAAE,wBAAwB;YAC7B,EAAE,EAAE,+BAA+B;SACpC;QACD;YACE,GAAG,EAAE,kBAAkB;YACvB,EAAE,EAAE,2DAA2D;SAChE;QACD;YACE,GAAG,EAAE,wBAAwB;YAC7B,EAAE,EAAE,2CAA2C;SAChD;KACF;CACF,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAC5B,QAAgB,EAChB,IAAkB,EAClB,YAAoB,mBAAmB;IAEvC,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAC3C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAChE,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;YACxB,IAAI,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACtB,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC;YACjD,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAC9B,CAAC"}