@zhivex-ai/harness 1.2.0 → 1.3.0-rc.7

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 (164) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +9 -10
  3. package/contracts/engine-api.json +2921 -0
  4. package/contracts/public-api.json +267 -1
  5. package/contracts/stable-api-signatures.json +43 -43
  6. package/dist/acp-cli.js +137 -15002
  7. package/dist/approvals/approval-history.d.ts +22 -0
  8. package/dist/approvals/explicit-review.d.ts +24 -0
  9. package/dist/approvals/host-policy-identity.d.ts +3 -0
  10. package/dist/cli/arguments.d.ts +6 -2
  11. package/dist/cli/checkpoints.d.ts +230 -0
  12. package/dist/cli/cli-options.d.ts +2 -41
  13. package/dist/cli/cli-profiles.d.ts +4 -2
  14. package/dist/cli/cli-stream.d.ts +2 -167
  15. package/dist/cli/governance-report.d.ts +5 -0
  16. package/dist/cli/policy-decisions.d.ts +9 -0
  17. package/dist/cli/policy.d.ts +5 -0
  18. package/dist/cli/presentation.d.ts +3 -1
  19. package/dist/cli/run-document.d.ts +2 -91
  20. package/dist/cli/terminal/terminal-continuation.d.ts +2 -3
  21. package/dist/cli/terminal/terminal-ui.d.ts +4 -0
  22. package/dist/{cli-entry-c2f58zj2.js → cli-entry-ae7x44d4.js} +14969 -10731
  23. package/dist/{cli-entry-427869g9.js → cli-entry-d5788aay.js} +388 -47
  24. package/dist/cli-entry-ev76ezcd.js +656 -0
  25. package/dist/{cli-3jmy4h5h.js → cli-t7hrhayq.js} +918 -2223
  26. package/dist/cli.js +3 -5
  27. package/dist/client/acp-mcp-admission.d.ts +32 -0
  28. package/dist/client/acp-mcp-host.d.ts +19 -0
  29. package/dist/client/acp-mcp-session.d.ts +14 -0
  30. package/dist/client/acp-stdio.d.ts +2 -0
  31. package/dist/client/acp.d.ts +4 -1
  32. package/dist/client/adapter.d.ts +13 -1
  33. package/dist/client/continuation.d.ts +3 -0
  34. package/dist/client/json-contracts.d.ts +107 -4
  35. package/dist/client/local-service.d.ts +2 -0
  36. package/dist/client/protocol.d.ts +127 -1
  37. package/dist/client/run-document.d.ts +91 -0
  38. package/dist/client/service-events.d.ts +4 -0
  39. package/dist/client/stream.d.ts +167 -0
  40. package/dist/compat/cli-options.d.ts +53 -0
  41. package/dist/compat/code-support.d.ts +22 -0
  42. package/dist/compat/code-support.js +74 -0
  43. package/dist/{config-1k9t4jhh.js → config-c002f06f.js} +1 -1
  44. package/dist/config-j3jga5wh.js +61 -0
  45. package/dist/context/adaptive-compaction.d.ts +1 -0
  46. package/dist/context/compaction.d.ts +1 -1
  47. package/dist/context/request-measurements.d.ts +17 -0
  48. package/dist/context/request-projection.d.ts +10 -0
  49. package/dist/context/task-memory.d.ts +1 -0
  50. package/dist/engine/acp.d.ts +6 -0
  51. package/dist/engine/acp.js +205 -0
  52. package/dist/engine/client.d.ts +6 -0
  53. package/dist/engine/client.js +28 -0
  54. package/dist/engine/desktop-providers-v1.d.ts +4 -0
  55. package/dist/engine/desktop-providers-v1.js +18 -0
  56. package/dist/engine/desktop-state-v1.d.ts +10 -0
  57. package/dist/engine/desktop-state-v1.js +168 -0
  58. package/dist/engine/index.d.ts +73 -0
  59. package/dist/engine/index.js +977 -0
  60. package/dist/engine/mcp-stdio-v1.d.ts +11 -0
  61. package/dist/engine/mcp-stdio-v1.js +108 -0
  62. package/dist/engine/models.d.ts +3 -0
  63. package/dist/engine/models.js +894 -0
  64. package/dist/engine/protocol.d.ts +7 -0
  65. package/dist/engine/protocol.js +182 -0
  66. package/dist/engine/service.d.ts +5 -0
  67. package/dist/engine/service.js +28 -0
  68. package/dist/execution/acceptance-snapshot.d.ts +2 -0
  69. package/dist/execution/execution-environment.d.ts +10 -0
  70. package/dist/execution/mcp-oci-channel.d.ts +11 -0
  71. package/dist/execution/mcp-oci-server.d.ts +38 -0
  72. package/dist/execution/mcp-oci-workspace.d.ts +23 -0
  73. package/dist/execution/mcp-resource-recovery.d.ts +19 -0
  74. package/dist/execution/mcp-workspace-snapshot.d.ts +20 -0
  75. package/dist/hostile-repository-demo.js +3368 -1304
  76. package/dist/index-1bx83sw4.js +4 -0
  77. package/dist/index-2gz7jv7g.js +549 -0
  78. package/dist/{cli-entry-b1mfvmxe.js → index-2jrdcj6b.js} +72 -115
  79. package/dist/index-3ah1f369.js +356 -0
  80. package/dist/index-42gp2hy8.js +1555 -0
  81. package/dist/index-4jw3tvy8.js +301 -0
  82. package/dist/index-8705zxpr.js +145 -0
  83. package/dist/index-8vm2m34s.js +2080 -0
  84. package/dist/index-8zzhggxv.js +477 -0
  85. package/dist/index-a97bezbz.js +1073 -0
  86. package/dist/index-gq9xagx2.js +1343 -0
  87. package/dist/index-h0mqx2ed.js +1373 -0
  88. package/dist/index-jgrx804e.js +5589 -0
  89. package/dist/index-qfpz5y2v.js +174 -0
  90. package/dist/index-t60rva31.js +1004 -0
  91. package/dist/index-wg1sd3at.js +80 -0
  92. package/dist/index-xqxeypt5.js +888 -0
  93. package/dist/index-yb239ghk.js +961 -0
  94. package/dist/index-z0qxmkm2.js +3389 -0
  95. package/dist/index-z4e8d0ph.js +1768 -0
  96. package/dist/index.js +251 -18649
  97. package/dist/integrations/mcp-host-session.d.ts +26 -0
  98. package/dist/integrations/mcp-stdio-admission.d.ts +54 -0
  99. package/dist/integrations/mcp-stdio-client.d.ts +44 -0
  100. package/dist/integrations/mcp-stdio-framing.d.ts +18 -0
  101. package/dist/integrations/mcp-stdio-launch.d.ts +20 -0
  102. package/dist/integrations/mcp-stdio-session.d.ts +7 -0
  103. package/dist/integrations/mcp-stdio-tools.d.ts +12 -0
  104. package/dist/internal/desktop/catalog.d.ts +2 -0
  105. package/dist/internal/desktop/persistence.d.ts +2 -3
  106. package/dist/internal/desktop/providers.d.ts +1 -0
  107. package/dist/models/catalog.d.ts +2 -0
  108. package/dist/persistence/governance-context.d.ts +126 -0
  109. package/dist/persistence/governance-report.d.ts +299 -0
  110. package/dist/persistence/host-database-backup.d.ts +28 -0
  111. package/dist/persistence/mcp-execution-journal.d.ts +19 -0
  112. package/dist/persistence/mcp-reconciliation.d.ts +33 -0
  113. package/dist/persistence/mcp-resource-journal.d.ts +54 -0
  114. package/dist/persistence/workspace-checkpoints.d.ts +138 -0
  115. package/dist/providers/anthropic-continuation.d.ts +5 -0
  116. package/dist/providers/providers.d.ts +1 -1
  117. package/dist/providers/vertex-auth.d.ts +27 -0
  118. package/dist/runtime/delegation-contracts.d.ts +9 -0
  119. package/dist/runtime/delegation-correction.d.ts +9 -0
  120. package/dist/runtime/delegation-result.d.ts +73 -0
  121. package/dist/runtime/error-diagnostics.d.ts +86 -0
  122. package/dist/runtime/harness.d.ts +11 -1
  123. package/dist/runtime/policy-decisions.d.ts +66 -0
  124. package/dist/runtime/policy-inspection.d.ts +52 -0
  125. package/dist/runtime/provider-diagnostics.d.ts +1 -0
  126. package/dist/runtime/runtime-instructions.d.ts +8 -0
  127. package/dist/runtime/task-acceptance-checks.d.ts +103 -0
  128. package/dist/runtime/task-acceptance-delivery.d.ts +107 -0
  129. package/dist/runtime/task-acceptance-host.d.ts +20 -0
  130. package/dist/runtime/task-acceptance-record.d.ts +107 -0
  131. package/dist/runtime/task-acceptance-validation.d.ts +25 -0
  132. package/dist/runtime/task-acceptance.d.ts +47 -0
  133. package/dist/runtime/time-to-safe-fix.d.ts +83 -0
  134. package/dist/runtime/tool-policy-file.d.ts +23 -0
  135. package/dist/runtime/tool-policy-paths.d.ts +5 -0
  136. package/dist/runtime/tool-policy.d.ts +6 -0
  137. package/dist/service-cli.js +19 -14504
  138. package/dist/{service-client-cli-yx56n0hc.js → service-client-cli-kns4mf57.js} +307 -248
  139. package/dist/verify-historical-migrations.js +3314 -1251
  140. package/dist/workspace/path-policy.d.ts +4 -0
  141. package/dist/zhx.js +3 -5
  142. package/docs/ACP.md +14 -2
  143. package/docs/ANTHROPIC.md +33 -0
  144. package/docs/CLI.md +132 -0
  145. package/docs/CLIENT_PROTOCOL.md +45 -0
  146. package/docs/CODE_RELEASE.md +93 -0
  147. package/docs/ENGINE_API.md +319 -0
  148. package/docs/EXTENSIBILITY.md +46 -3
  149. package/docs/FIRST_USE.md +6 -6
  150. package/docs/LOCAL_SERVICE.md +13 -0
  151. package/docs/MCP_STDIO.md +20 -0
  152. package/docs/README.md +6 -0
  153. package/docs/STABILITY.md +9 -1
  154. package/docs/SUPPORT_MATRIX.md +1 -1
  155. package/docs/TOOL_POLICY.md +81 -0
  156. package/docs/VERTEX.md +50 -0
  157. package/docs/adr/0002-isolated-mcp-stdio.md +135 -0
  158. package/docs/fixtures/mcp-stdio-contract-v1.json +189 -0
  159. package/docs/support-matrix.json +2 -0
  160. package/evaluations/representative-assembly-matrix.json +64 -1
  161. package/evaluations/task-acceptance-routes.json +7 -0
  162. package/examples/README.md +1 -1
  163. package/package.json +74 -12
  164. package/dist/cli-entry-64pdp3k5.js +0 -17
@@ -0,0 +1,1073 @@
1
+ import {
2
+ HARNESS_OPERATIONS_SCHEMA_VERSION,
3
+ HARNESS_SQLITE_FILE,
4
+ openHarnessPersistence,
5
+ sqliteAdapter,
6
+ workspaceCheckpointSchema,
7
+ workspaceRestoreOperationSchema
8
+ } from "./index-h0mqx2ed.js";
9
+ import {
10
+ HARNESS_SESSION_SCHEMA_VERSION,
11
+ openCliSessionStore,
12
+ validateStateDirectory
13
+ } from "./index-42gp2hy8.js";
14
+ import {
15
+ SqliteDatabase,
16
+ readRegularFileChunksNoFollow
17
+ } from "./index-yb239ghk.js";
18
+ import {
19
+ HarnessStateConflictError,
20
+ HarnessWorkspaceError
21
+ } from "./index-gq9xagx2.js";
22
+
23
+ // src/persistence/recorded-workspace.ts
24
+ import { lstat, realpath } from "node:fs/promises";
25
+ import path from "node:path";
26
+ async function validateRecordedWorkspace(workspace, allowAbsent = false) {
27
+ if (!path.isAbsolute(workspace) || path.normalize(workspace) !== workspace || /[\x00-\x1f\x7f]/.test(workspace))
28
+ throw new Error("RECORDED_WORKSPACE_INVALID");
29
+ let ancestor = workspace;
30
+ for (;; ) {
31
+ try {
32
+ const info = await lstat(ancestor);
33
+ if (!info.isDirectory() || info.isSymbolicLink() || await realpath(ancestor) !== ancestor)
34
+ throw new Error("RECORDED_WORKSPACE_INVALID");
35
+ return workspace;
36
+ } catch (error) {
37
+ if (!allowAbsent || error.code !== "ENOENT")
38
+ throw error;
39
+ const parent = path.dirname(ancestor);
40
+ if (parent === ancestor)
41
+ throw new Error("RECORDED_WORKSPACE_INVALID");
42
+ ancestor = parent;
43
+ }
44
+ }
45
+ }
46
+
47
+ // src/persistence/state-backup-stream.ts
48
+ var MAGIC = `ZHIVEX-STATE-SEGMENTS-1
49
+ `;
50
+ var STRING_CHARS = 8192;
51
+ var MAX_FRAME_BYTES = 64 * 1024;
52
+ function* canonicalChunks(value) {
53
+ if (typeof value === "string") {
54
+ yield '"';
55
+ for (let i = 0;i < value.length; ) {
56
+ let end = Math.min(i + STRING_CHARS, value.length);
57
+ if (end < value.length && /[\uD800-\uDBFF]/.test(value[end - 1]))
58
+ end--;
59
+ yield JSON.stringify(value.slice(i, end)).slice(1, -1);
60
+ i = end;
61
+ }
62
+ yield '"';
63
+ } else if (value === null || typeof value !== "object") {
64
+ yield JSON.stringify(value);
65
+ } else if (Array.isArray(value)) {
66
+ yield "[";
67
+ for (let i = 0;i < value.length; i++) {
68
+ if (i)
69
+ yield ",";
70
+ yield* canonicalChunks(value[i]);
71
+ }
72
+ yield "]";
73
+ } else {
74
+ yield "{";
75
+ let first = true;
76
+ for (const key of Object.keys(value).sort()) {
77
+ if (!first)
78
+ yield ",";
79
+ first = false;
80
+ yield* canonicalChunks(key);
81
+ yield ":";
82
+ yield* canonicalChunks(value[key]);
83
+ }
84
+ yield "}";
85
+ }
86
+ }
87
+ function* tokens(value) {
88
+ if (typeof value === "string") {
89
+ for (let i = 0;i < value.length; i += STRING_CHARS)
90
+ yield ["text", value.slice(i, i + STRING_CHARS)];
91
+ yield ["string"];
92
+ } else if (value === null || typeof value !== "object")
93
+ yield ["value", value];
94
+ else {
95
+ yield [Array.isArray(value) ? "array" : "object"];
96
+ if (Array.isArray(value))
97
+ for (const item of value)
98
+ yield* tokens(item);
99
+ else
100
+ for (const [key, item] of Object.entries(value)) {
101
+ yield* tokens(key);
102
+ yield* tokens(item);
103
+ }
104
+ yield ["end"];
105
+ }
106
+ }
107
+ function* segmentedChunks(value) {
108
+ yield MAGIC;
109
+ for (const token of tokens(value))
110
+ yield `${JSON.stringify(token)}
111
+ `;
112
+ }
113
+ async function readBackupTransport(source, legacyMaxBytes) {
114
+ const fail = () => {
115
+ throw new Error("Harness state backup segment stream is invalid or incomplete.");
116
+ };
117
+ const stack = [];
118
+ let root;
119
+ let hasRoot = false;
120
+ let pieces = [];
121
+ let pending = Buffer.alloc(0);
122
+ let segmented;
123
+ const legacy = [];
124
+ let legacyBytes = 0;
125
+ const append = (value) => {
126
+ const parent = stack.at(-1);
127
+ if (!parent) {
128
+ if (hasRoot)
129
+ fail();
130
+ root = value;
131
+ hasRoot = true;
132
+ } else if (Array.isArray(parent.value))
133
+ parent.value.push(value);
134
+ else if (parent.key === undefined) {
135
+ if (typeof value !== "string" || Object.hasOwn(parent.value, value))
136
+ fail();
137
+ parent.key = value;
138
+ } else {
139
+ Object.defineProperty(parent.value, parent.key, { value, enumerable: true, writable: true, configurable: true });
140
+ delete parent.key;
141
+ }
142
+ };
143
+ const consume = (line) => {
144
+ if (line.length > MAX_FRAME_BYTES)
145
+ fail();
146
+ const token = JSON.parse(line.toString("utf8"));
147
+ if (!Array.isArray(token))
148
+ fail();
149
+ const frame = token;
150
+ const [kind, value] = frame;
151
+ if (kind === "text" && frame.length === 2 && typeof value === "string") {
152
+ pieces.push(value);
153
+ return;
154
+ }
155
+ if (kind === "string" && frame.length === 1) {
156
+ append(pieces.join(""));
157
+ pieces = [];
158
+ return;
159
+ }
160
+ if (pieces.length)
161
+ fail();
162
+ if (kind === "value" && frame.length === 2 && (value === null || typeof value === "boolean" || typeof value === "number"))
163
+ append(value);
164
+ else if ((kind === "array" || kind === "object") && frame.length === 1) {
165
+ if (stack.length >= 512)
166
+ fail();
167
+ const value2 = kind === "array" ? [] : {};
168
+ append(value2);
169
+ stack.push({ value: value2 });
170
+ } else if (kind === "end" && frame.length === 1) {
171
+ const parent = stack.pop();
172
+ if (!parent || parent.key !== undefined)
173
+ fail();
174
+ } else
175
+ fail();
176
+ };
177
+ for await (const chunk of readRegularFileChunksNoFollow(source, {
178
+ label: "Harness state backup",
179
+ requireSingleLink: true,
180
+ requirePrivate: true
181
+ })) {
182
+ pending = Buffer.concat([pending, chunk]);
183
+ if (segmented === undefined) {
184
+ if (pending.length < Buffer.byteLength(MAGIC))
185
+ continue;
186
+ segmented = pending.subarray(0, MAGIC.length).toString() === MAGIC;
187
+ if (segmented)
188
+ pending = pending.subarray(MAGIC.length);
189
+ }
190
+ if (!segmented) {
191
+ legacyBytes += pending.length;
192
+ if (legacyBytes > legacyMaxBytes)
193
+ throw new Error("Harness state backup exceeds the legacy size limit.");
194
+ legacy.push(pending);
195
+ pending = Buffer.alloc(0);
196
+ continue;
197
+ }
198
+ let newline;
199
+ while ((newline = pending.indexOf(10)) !== -1) {
200
+ consume(pending.subarray(0, newline));
201
+ pending = pending.subarray(newline + 1);
202
+ }
203
+ if (pending.length > MAX_FRAME_BYTES)
204
+ fail();
205
+ }
206
+ if (!segmented) {
207
+ if (legacyBytes + pending.length > legacyMaxBytes)
208
+ throw new Error("Harness state backup exceeds the legacy size limit.");
209
+ return JSON.parse(Buffer.concat([...legacy, pending]).toString("utf8"));
210
+ }
211
+ if (pending.length || stack.length || pieces.length || !hasRoot)
212
+ fail();
213
+ return root;
214
+ }
215
+
216
+ // src/persistence/state-backup.ts
217
+ import { createHash, randomUUID } from "node:crypto";
218
+ import { constants as fsConstants } from "node:fs";
219
+ import { chmod, link, lstat as lstat2, open, realpath as realpath2, unlink } from "node:fs/promises";
220
+ import path2 from "node:path";
221
+ import { z } from "zod";
222
+ import { normalizeAgentRunState } from "@zhivex-ai/agents";
223
+ import { createSqliteAgentRunStore } from "@zhivex-ai/agents/ops";
224
+ var HARNESS_STATE_BACKUP_SCHEMA_VERSION = 1;
225
+ var HARNESS_STATE_BACKUP_MAX_BYTES = 64 * 1024 * 1024;
226
+ var terminalRunStatuses = new Set(["completed", "failed", "cancelled", "timed_out"]);
227
+ var terminalSessionStatuses = new Set(["completed", "failed", "cancelled", "timed_out"]);
228
+ var digestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/);
229
+ var bindingKeySchema = z.string().regex(/^[a-f0-9]{64}$/);
230
+ var jsonRecordSchema = z.record(z.string(), z.unknown());
231
+ var durableScopeSchema = z.object({
232
+ tenantId: z.string().min(1),
233
+ userId: z.string().min(1).optional(),
234
+ namespace: z.string().min(1).optional()
235
+ }).strict();
236
+ var journalEntrySchema = z.object({
237
+ runId: z.string().min(1),
238
+ scope: durableScopeSchema,
239
+ toolCallId: z.string().min(1),
240
+ toolName: z.string().min(1),
241
+ status: z.enum(["pending", "running", "completed", "failed"]),
242
+ idempotencyKey: z.string().min(1),
243
+ revision: z.number().int().nonnegative(),
244
+ input: z.json().optional(),
245
+ output: z.json().optional(),
246
+ error: z.object({ message: z.string() }).strict().optional(),
247
+ startedAt: z.number().nonnegative().optional(),
248
+ completedAt: z.number().nonnegative().optional(),
249
+ updatedAt: z.number().nonnegative()
250
+ }).passthrough();
251
+ var runRecordSchema = z.object({
252
+ key: z.string().min(1).max(1024),
253
+ state: jsonRecordSchema,
254
+ updatedAt: z.number().int().nonnegative()
255
+ }).strict();
256
+ var journalRecordSchema = z.object({
257
+ runKey: z.string().min(1).max(1024),
258
+ toolCallId: z.string().min(1).max(1024),
259
+ entry: journalEntrySchema,
260
+ revision: z.number().int().nonnegative(),
261
+ updatedAt: z.number().int().nonnegative()
262
+ }).strict();
263
+ var idempotencyRecordSchema = z.object({
264
+ key: z.string().min(1).max(1024),
265
+ runKey: z.string().min(1).max(1024),
266
+ updatedAt: z.number().int().nonnegative()
267
+ }).strict();
268
+ var parentRecordSchema = z.object({
269
+ runKey: z.string().min(1).max(1024),
270
+ parentRunKey: z.string().min(1).max(1024),
271
+ updatedAt: z.number().int().nonnegative()
272
+ }).strict();
273
+ var memoryRecordSchema = z.object({
274
+ key: z.string().min(1).max(1024),
275
+ messages: z.array(z.unknown()),
276
+ updatedAt: z.number().int().nonnegative()
277
+ }).strict();
278
+ var sessionRecordSchema = z.object({
279
+ sessionId: z.string().min(1).max(260),
280
+ workspaceKey: bindingKeySchema,
281
+ scopeKey: bindingKeySchema,
282
+ title: z.string().nullable(),
283
+ parentSessionId: z.string().nullable(),
284
+ forkedFromTurnId: z.string().nullable(),
285
+ revision: z.number().int().nonnegative(),
286
+ activitySequence: z.number().int().nonnegative(),
287
+ createdAt: z.number().int().nonnegative(),
288
+ updatedAt: z.number().int().nonnegative(),
289
+ archivedAt: z.number().int().nonnegative().nullable(),
290
+ deletedAt: z.number().int().nonnegative().nullable(),
291
+ metadataBytes: z.number().int().nonnegative()
292
+ }).strict();
293
+ var sessionRunRecordSchema = z.object({
294
+ turnId: z.string().min(1).max(260),
295
+ sourceTurnId: z.string().nullable(),
296
+ sessionId: z.string().min(1).max(260),
297
+ ordinal: z.number().int().nonnegative(),
298
+ runId: z.string().min(1).max(260),
299
+ provider: z.string().min(1).max(260),
300
+ model: z.string().min(1).max(260),
301
+ role: z.string().nullable(),
302
+ status: z.string().min(1),
303
+ createdAt: z.number().int().nonnegative(),
304
+ updatedAt: z.number().int().nonnegative(),
305
+ completedAt: z.number().int().nonnegative().nullable(),
306
+ metadataBytes: z.number().int().nonnegative()
307
+ }).strict();
308
+ var checkpointRecordSchema = z.discriminatedUnion("kind", [
309
+ z.object({
310
+ id: z.string().uuid(),
311
+ workspaceKey: bindingKeySchema,
312
+ scopeKey: bindingKeySchema,
313
+ kind: z.literal("checkpoint"),
314
+ body: workspaceCheckpointSchema
315
+ }).strict(),
316
+ z.object({
317
+ id: z.string().uuid(),
318
+ workspaceKey: bindingKeySchema,
319
+ scopeKey: bindingKeySchema,
320
+ kind: z.literal("restore"),
321
+ body: workspaceRestoreOperationSchema
322
+ }).strict()
323
+ ]);
324
+ var checkpointTableSql = "CREATE TABLE IF NOT EXISTS zhivex_workspace_checkpoints (id TEXT PRIMARY KEY, workspace_key TEXT NOT NULL, scope_key TEXT NOT NULL, kind TEXT NOT NULL, body TEXT NOT NULL)";
325
+ var stateBackupPayloadSchema = z.object({
326
+ schemaVersion: z.literal(HARNESS_STATE_BACKUP_SCHEMA_VERSION),
327
+ kind: z.literal("state-backup"),
328
+ createdAt: z.iso.datetime({ precision: 3 }),
329
+ binding: z.object({
330
+ workspaceKey: bindingKeySchema,
331
+ scopeKey: bindingKeySchema,
332
+ scopePrefix: z.string().min(1).max(1024)
333
+ }).strict(),
334
+ schemas: z.object({
335
+ operations: z.literal(HARNESS_OPERATIONS_SCHEMA_VERSION),
336
+ sessions: z.literal(HARNESS_SESSION_SCHEMA_VERSION)
337
+ }).strict(),
338
+ records: z.object({
339
+ runs: z.array(runRecordSchema),
340
+ toolJournal: z.array(journalRecordSchema),
341
+ idempotency: z.array(idempotencyRecordSchema),
342
+ parents: z.array(parentRecordSchema),
343
+ memory: z.array(memoryRecordSchema),
344
+ sessions: z.array(sessionRecordSchema),
345
+ sessionRuns: z.array(sessionRunRecordSchema),
346
+ workspaceCheckpoints: z.array(checkpointRecordSchema).max(100).optional(),
347
+ budgetLedgers: z.array(runRecordSchema).optional()
348
+ }).strict()
349
+ }).strict();
350
+ var stateBackupBundleSchema = stateBackupPayloadSchema.extend({ checksum: digestSchema }).strict();
351
+ var canonicalJson = (value) => {
352
+ if (value === null || typeof value !== "object")
353
+ return JSON.stringify(value);
354
+ if (Array.isArray(value))
355
+ return `[${value.map(canonicalJson).join(",")}]`;
356
+ return `{${Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
357
+ };
358
+ var checksumPayload = (payload) => {
359
+ const hash = createHash("sha256");
360
+ for (const chunk of canonicalChunks(payload))
361
+ hash.update(chunk);
362
+ return `sha256:${hash.digest("hex")}`;
363
+ };
364
+ var stableKey = (kind, value) => createHash("sha256").update(kind).update("\x00").update(value).digest("hex");
365
+ var scopeValue = (config) => `${config.scope.tenantId}\x00${config.scope.userId ?? ""}\x00${config.scope.namespace ?? ""}`;
366
+ var scopePrefix = (config) => `${encodeURIComponent(config.scope.namespace ?? "default")}:${encodeURIComponent(config.scope.tenantId)}:${encodeURIComponent(config.scope.userId ?? "*")}:`;
367
+ var stateMatchesScope = (config, state) => {
368
+ if (!state || typeof state !== "object" || Array.isArray(state))
369
+ return false;
370
+ const candidate = state;
371
+ if (!candidate.scope || typeof candidate.scope !== "object" || Array.isArray(candidate.scope))
372
+ return false;
373
+ const scope = candidate.scope;
374
+ return scope.tenantId === config.scope.tenantId && (scope.userId ?? undefined) === (config.scope.userId ?? undefined) && (scope.namespace ?? undefined) === (config.scope.namespace ?? undefined);
375
+ };
376
+ var budgetScopePrefix = (config) => `__zhivex_budget__:${encodeURIComponent(config.scope.tenantId)}:${encodeURIComponent(config.scope.userId ?? "*")}:`;
377
+ var coordinatorIds = (runs) => new Set(runs.flatMap(({ state }) => {
378
+ const id = state.budgetCoordinatorId;
379
+ return typeof id === "string" ? [id] : [];
380
+ }));
381
+ var validateBudgetLedger = (config, run, identities) => {
382
+ const state = validatedRunState(run.state, `Budget ledger ${run.key}`);
383
+ const identity = state.metadata?.budgetIdentity;
384
+ if (state.scope?.namespace !== "__zhivex_budget__" || state.scope?.tenantId !== config.scope.tenantId || (state.scope?.userId ?? undefined) !== (config.scope.userId ?? undefined) || state.provider !== "zhivex" || state.modelId !== "budget-coordinator" || state.metadata?.budgetCoordinator !== true || typeof identity !== "string" || !identities.has(identity) || !/^budget_[a-f0-9]{64}$/.test(state.runId) || run.key !== `${budgetScopePrefix(config)}${state.runId}`) {
385
+ throw new HarnessStateConflictError("Budget ledger is not bound to an exported run and durable scope.");
386
+ }
387
+ assertTerminalState(state, `Budget ledger ${run.key}`);
388
+ const allocations = state.metadata?.allocations;
389
+ if (!allocations || typeof allocations !== "object" || Array.isArray(allocations))
390
+ throw new HarnessStateConflictError("Invalid budget ledger allocations.");
391
+ for (const [id, value] of Object.entries(allocations)) {
392
+ if (!/^sha256:[a-f0-9]{64}$/.test(id) || !value || typeof value !== "object" || Array.isArray(value) || value.status !== "confirmed") {
393
+ throw new HarnessStateConflictError("Budget ledger retains unresolved reservations or unknown consumption.");
394
+ }
395
+ const tokens2 = value.tokens;
396
+ if (!tokens2 || typeof tokens2 !== "object" || Array.isArray(tokens2) || ![tokens2.inputTokens, tokens2.outputTokens, tokens2.totalTokens].every((amount) => typeof amount === "number" && Number.isSafeInteger(amount) && amount >= 0) || tokens2.totalTokens < tokens2.inputTokens + tokens2.outputTokens) {
397
+ throw new HarnessStateConflictError("Invalid budget ledger token receipt.");
398
+ }
399
+ }
400
+ return identity;
401
+ };
402
+ var bindingForConfig = async (config) => ({
403
+ workspaceKey: stableKey("workspace", await realpath2(path2.resolve(config.workspace))),
404
+ scopeKey: stableKey("scope", scopeValue(config)),
405
+ scopePrefix: scopePrefix(config)
406
+ });
407
+ var parseJsonRecord = (value, label) => {
408
+ let parsed;
409
+ try {
410
+ parsed = JSON.parse(value);
411
+ } catch (error) {
412
+ throw new HarnessStateConflictError(`${label} contains invalid JSON.`, { cause: error });
413
+ }
414
+ return jsonRecordSchema.parse(parsed);
415
+ };
416
+ var prepareDatabase = async (config) => {
417
+ if (config.storeBackend !== "sqlite") {
418
+ throw new HarnessStateConflictError("State backup requires the sqlite store backend.");
419
+ }
420
+ const persistence = await openHarnessPersistence(config, { migrateLegacyFileStore: false });
421
+ persistence.close();
422
+ const sessions = await openCliSessionStore({
423
+ workspace: config.workspace,
424
+ stateDirectory: config.stateDirectory,
425
+ scope: config.scope
426
+ });
427
+ sessions.close();
428
+ return path2.join(await realpath2(config.stateDirectory), HARNESS_SQLITE_FILE);
429
+ };
430
+ var locateExistingDatabase = async (config) => {
431
+ if (config.storeBackend !== "sqlite") {
432
+ throw new HarnessStateConflictError("State backup requires the sqlite store backend.");
433
+ }
434
+ await validateStateDirectory(config.workspace, config.stateDirectory);
435
+ let canonicalStateDirectory;
436
+ try {
437
+ canonicalStateDirectory = await realpath2(config.stateDirectory);
438
+ } catch (error) {
439
+ if (error.code === "ENOENT")
440
+ return;
441
+ throw error;
442
+ }
443
+ const databasePath = path2.join(canonicalStateDirectory, HARNESS_SQLITE_FILE);
444
+ try {
445
+ const entry = await lstat2(databasePath);
446
+ if (entry.isSymbolicLink() || !entry.isFile()) {
447
+ throw new HarnessWorkspaceError(`The SQLite state path must be a real file: ${databasePath}.`);
448
+ }
449
+ return databasePath;
450
+ } catch (error) {
451
+ if (error.code === "ENOENT")
452
+ return;
453
+ throw error;
454
+ }
455
+ };
456
+ var assertTerminalState = (state, label) => {
457
+ if (!state || typeof state !== "object" || Array.isArray(state)) {
458
+ throw new HarnessStateConflictError(`${label} is not a terminal run.`);
459
+ }
460
+ const candidate = state;
461
+ if (typeof candidate.runId !== "string" || !terminalRunStatuses.has(String(candidate.status))) {
462
+ throw new HarnessStateConflictError(`${label} is not a terminal run.`);
463
+ }
464
+ const unresolvedAuxiliary = (value) => {
465
+ if (!value || typeof value !== "object" || Array.isArray(value))
466
+ return false;
467
+ const entry = value;
468
+ return entry.unknownCompactionUsage === true || Array.isArray(entry.compactionAttempts) && entry.compactionAttempts.some((attempt) => !attempt || typeof attempt !== "object" || attempt.status !== "confirmed") || Array.isArray(entry.childRuns) && entry.childRuns.some(unresolvedAuxiliary) || entry.resumeState !== undefined && unresolvedAuxiliary(entry.resumeState);
469
+ };
470
+ if (unresolvedAuxiliary(candidate))
471
+ throw new HarnessStateConflictError(`${label} retains unresolved auxiliary model consumption.`);
472
+ if (Array.isArray(candidate.pendingApprovals) && candidate.pendingApprovals.length > 0) {
473
+ throw new HarnessStateConflictError(`${label} retains approval authority and cannot be backed up.`);
474
+ }
475
+ };
476
+ var validatedRunState = (value, label) => {
477
+ try {
478
+ return normalizeAgentRunState(value);
479
+ } catch (error) {
480
+ throw new HarnessStateConflictError(`${label} is not a valid AgentRunState.`, { cause: error });
481
+ }
482
+ };
483
+ var readPayload = async (config, recordedWorkspace, accessLease) => {
484
+ const databasePath = recordedWorkspace ? await locateExistingDatabase(config) : await prepareDatabase(config);
485
+ if (!databasePath)
486
+ throw new HarnessStateConflictError("Archived state database is missing.");
487
+ const binding = recordedWorkspace ? { workspaceKey: stableKey("workspace", recordedWorkspace), scopeKey: stableKey("scope", scopeValue(config)), scopePrefix: scopePrefix(config) } : await bindingForConfig(config);
488
+ const database = new SqliteDatabase(databasePath, { create: false, strict: true, ...recordedWorkspace ? { readonly: true } : {}, ...accessLease ? { accessLease } : {} });
489
+ let transactionStarted = false;
490
+ try {
491
+ database.exec("PRAGMA busy_timeout = 5000");
492
+ database.exec("PRAGMA foreign_keys = ON");
493
+ database.exec(recordedWorkspace ? "BEGIN" : "BEGIN IMMEDIATE");
494
+ transactionStarted = true;
495
+ if (recordedWorkspace && database.query("SELECT version FROM zhivex_cli_session_schema WHERE singleton=1").get()?.version !== HARNESS_SESSION_SCHEMA_VERSION)
496
+ throw new HarnessStateConflictError("Archived session schema is incompatible.");
497
+ const allRunRows = database.query("SELECT run_id, state_json, updated_at_ms FROM zhivex_agent_runs ORDER BY run_id").all();
498
+ let historyReader;
499
+ const runRows = allRunRows.flatMap((row) => {
500
+ const record = parseJsonRecord(row.state_json, `Run ${row.run_id}`);
501
+ const hydrated = record.checkpointHistory ? (historyReader ??= createSqliteAgentRunStore({ db: sqliteAdapter(database), history: "incremental" })).load(String(record.runId), record.scope) : record;
502
+ if (hydrated instanceof Promise || !hydrated)
503
+ throw new HarnessStateConflictError("Cannot hydrate state backup history.");
504
+ const state = validatedRunState(hydrated, `Run ${row.run_id}`);
505
+ return stateMatchesScope(config, state) ? [{ row, state }] : [];
506
+ });
507
+ const runs = runRows.map(({ row, state }) => {
508
+ assertTerminalState(state, `Run ${row.run_id}`);
509
+ return { key: row.run_id, state, updatedAt: row.updated_at_ms };
510
+ });
511
+ const identities = coordinatorIds(runs);
512
+ const budgetLedgers = allRunRows.flatMap((row) => {
513
+ const state = parseJsonRecord(row.state_json, `Run ${row.run_id}`);
514
+ const metadata = state.metadata;
515
+ if (typeof metadata?.budgetIdentity !== "string" || !identities.has(metadata.budgetIdentity)) {
516
+ const scope = state.scope;
517
+ if (scope?.namespace === "__zhivex_budget__" && scope.tenantId === config.scope.tenantId && (scope.userId ?? undefined) === (config.scope.userId ?? undefined)) {
518
+ throw new HarnessStateConflictError("An unlinked shared budget ledger cannot be safely assigned to this backup scope; retain the original database.");
519
+ }
520
+ return [];
521
+ }
522
+ const record = { key: row.run_id, state, updatedAt: row.updated_at_ms };
523
+ validateBudgetLedger(config, record, identities);
524
+ return [record];
525
+ });
526
+ if (new Set(budgetLedgers.map((run) => run.state.metadata.budgetIdentity)).size !== identities.size || budgetLedgers.length !== identities.size) {
527
+ throw new HarnessStateConflictError("An exported run has a missing or duplicate shared budget ledger.");
528
+ }
529
+ const runKeys = new Set([...runs, ...budgetLedgers].map((run) => run.key));
530
+ const activeLeases = database.query("SELECT run_key, expires_at_ms FROM zhivex_agent_runs_leases").all().filter((row) => runKeys.has(row.run_key) && row.expires_at_ms > Date.now()).length;
531
+ if (activeLeases > 0) {
532
+ throw new HarnessStateConflictError("State export refuses to capture active run leases.", { retryable: true });
533
+ }
534
+ const journal = database.query("SELECT run_key, tool_call_id, entry_json, revision, updated_at_ms FROM zhivex_agent_runs_tool_journal ORDER BY run_key, tool_call_id").all().filter((row) => runKeys.has(row.run_key)).map((row) => {
535
+ let entry;
536
+ try {
537
+ entry = journalEntrySchema.parse(parseJsonRecord(row.entry_json, `Tool journal ${row.tool_call_id}`));
538
+ } catch (error) {
539
+ throw new HarnessStateConflictError(`Tool journal ${row.tool_call_id} is invalid.`, { cause: error });
540
+ }
541
+ if (!stateMatchesScope(config, entry)) {
542
+ throw new HarnessStateConflictError(`Tool journal ${row.tool_call_id} is bound to another durable scope.`);
543
+ }
544
+ return {
545
+ runKey: row.run_key,
546
+ toolCallId: row.tool_call_id,
547
+ entry,
548
+ revision: row.revision,
549
+ updatedAt: row.updated_at_ms
550
+ };
551
+ });
552
+ const idempotency = database.query("SELECT idempotency_key, run_id, updated_at_ms FROM zhivex_agent_runs_idempotency ORDER BY idempotency_key").all().filter((row) => runKeys.has(row.run_id)).map((row) => ({
553
+ key: row.idempotency_key,
554
+ runKey: row.run_id,
555
+ updatedAt: row.updated_at_ms
556
+ }));
557
+ const parents = database.query("SELECT run_id, parent_run_id, updated_at_ms FROM zhivex_agent_runs_parents ORDER BY run_id").all().filter((row) => runKeys.has(row.run_id) && runKeys.has(row.parent_run_id)).map((row) => ({
558
+ runKey: row.run_id,
559
+ parentRunKey: row.parent_run_id,
560
+ updatedAt: row.updated_at_ms
561
+ }));
562
+ const memoryKeys = new Set(runs.map((run) => `${binding.scopePrefix}${String(run.state.runId)}`));
563
+ const memory = database.query("SELECT memory_key, messages_json, updated_at_ms FROM zhivex_agent_memory ORDER BY memory_key").all().filter((row) => memoryKeys.has(row.memory_key)).map((row) => ({
564
+ key: row.memory_key,
565
+ messages: z.array(z.unknown()).parse(JSON.parse(row.messages_json)),
566
+ updatedAt: row.updated_at_ms
567
+ }));
568
+ const sessionRows = database.query("SELECT * FROM zhivex_cli_sessions ORDER BY session_id").all().filter((row) => row.workspace_key === binding.workspaceKey && row.scope_key === binding.scopeKey);
569
+ const selectedSessionIds = new Set(sessionRows.map((row) => row.session_id));
570
+ const sessionRunRows = database.query("SELECT * FROM zhivex_cli_session_runs ORDER BY session_id, ordinal").all().filter((row) => selectedSessionIds.has(row.session_id));
571
+ for (const row of sessionRunRows) {
572
+ if (!terminalSessionStatuses.has(row.status) || !runs.some((run) => run.state.runId === row.run_id)) {
573
+ throw new HarnessStateConflictError(`Session run ${row.run_id} is active or has no terminal run state.`);
574
+ }
575
+ }
576
+ const sessions = sessionRows.map((row) => ({
577
+ sessionId: row.session_id,
578
+ workspaceKey: row.workspace_key,
579
+ scopeKey: row.scope_key,
580
+ title: row.title,
581
+ parentSessionId: row.parent_session_id,
582
+ forkedFromTurnId: row.forked_from_turn_id,
583
+ revision: row.revision,
584
+ activitySequence: row.activity_seq,
585
+ createdAt: row.created_at,
586
+ updatedAt: row.updated_at,
587
+ archivedAt: row.archived_at,
588
+ deletedAt: row.deleted_at,
589
+ metadataBytes: row.metadata_bytes
590
+ }));
591
+ const sessionRuns = sessionRunRows.map((row) => ({
592
+ turnId: row.turn_id,
593
+ sourceTurnId: row.source_turn_id,
594
+ sessionId: row.session_id,
595
+ ordinal: row.ordinal,
596
+ runId: row.run_id,
597
+ provider: row.provider,
598
+ model: row.model,
599
+ role: row.role,
600
+ status: row.status,
601
+ createdAt: row.created_at,
602
+ updatedAt: row.updated_at,
603
+ completedAt: row.completed_at,
604
+ metadataBytes: row.metadata_bytes
605
+ }));
606
+ const hasCheckpoints = database.query("SELECT name FROM sqlite_master WHERE type='table' AND name='zhivex_workspace_checkpoints'").get();
607
+ const workspaceCheckpoints = hasCheckpoints ? database.query("SELECT * FROM zhivex_workspace_checkpoints WHERE workspace_key=? AND scope_key=? ORDER BY id").all(binding.workspaceKey, binding.scopeKey).map((row) => ({
608
+ id: row.id,
609
+ workspaceKey: row.workspace_key,
610
+ scopeKey: row.scope_key,
611
+ kind: row.kind,
612
+ body: JSON.parse(row.body)
613
+ })) : [];
614
+ const payload = stateBackupPayloadSchema.parse({
615
+ schemaVersion: HARNESS_STATE_BACKUP_SCHEMA_VERSION,
616
+ kind: "state-backup",
617
+ createdAt: new Date().toISOString(),
618
+ binding,
619
+ schemas: { operations: HARNESS_OPERATIONS_SCHEMA_VERSION, sessions: HARNESS_SESSION_SCHEMA_VERSION },
620
+ records: { runs, toolJournal: journal, idempotency, parents, memory, sessions, sessionRuns, ...workspaceCheckpoints.length ? { workspaceCheckpoints } : {}, ...budgetLedgers.length ? { budgetLedgers } : {} }
621
+ });
622
+ for (const record of payload.records.workspaceCheckpoints ?? []) {
623
+ if (record.kind === "restore" && record.body.stage !== "completed") {
624
+ throw new HarnessStateConflictError("State backup refuses an unfinished workspace restore operation.");
625
+ }
626
+ }
627
+ database.exec("COMMIT");
628
+ transactionStarted = false;
629
+ return payload;
630
+ } catch (error) {
631
+ if (transactionStarted)
632
+ database.exec("ROLLBACK");
633
+ throw error;
634
+ } finally {
635
+ database.close(false);
636
+ }
637
+ };
638
+ var createHarnessStateBackup = async (config) => {
639
+ const payload = await readPayload(config);
640
+ return stateBackupBundleSchema.parse({ ...payload, checksum: checksumPayload(payload) });
641
+ };
642
+ var createArchivedHarnessStateBackup = async (config, options = {}) => {
643
+ const workspace = await validateRecordedWorkspace(config.workspace, true);
644
+ const payload = await readPayload(config, workspace, options.accessLease);
645
+ return stateBackupBundleSchema.parse({ ...payload, checksum: checksumPayload(payload) });
646
+ };
647
+ var writePrivateBackup = async (target, contents) => {
648
+ const requested = path2.resolve(target);
649
+ const parent = path2.dirname(requested);
650
+ let parentHandle;
651
+ try {
652
+ parentHandle = await open(parent, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
653
+ if (!(await parentHandle.stat()).isDirectory()) {
654
+ throw new Error("Backup parent is not a directory.");
655
+ }
656
+ } catch (error) {
657
+ await parentHandle?.close();
658
+ throw new HarnessWorkspaceError(`Backup parent must be a real non-symlink directory: ${parent}.`, {
659
+ cause: error
660
+ });
661
+ }
662
+ const staged = path2.join(parent, `.zhivex-state-backup-${randomUUID()}.tmp`);
663
+ let handle;
664
+ let published = false;
665
+ let stagedExists = false;
666
+ try {
667
+ handle = await open(staged, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW, 384);
668
+ stagedExists = true;
669
+ let pending = [];
670
+ let pendingBytes = 0;
671
+ for (const chunk of contents) {
672
+ pending.push(chunk);
673
+ pendingBytes += Buffer.byteLength(chunk);
674
+ if (pendingBytes >= 64 * 1024) {
675
+ await handle.writeFile(pending.join(""), "utf8");
676
+ pending = [];
677
+ pendingBytes = 0;
678
+ }
679
+ }
680
+ if (pending.length)
681
+ await handle.writeFile(pending.join(""), "utf8");
682
+ await handle.sync();
683
+ await chmod(staged, 384);
684
+ await handle.close();
685
+ handle = undefined;
686
+ await link(staged, requested);
687
+ published = true;
688
+ await unlink(staged);
689
+ stagedExists = false;
690
+ await parentHandle.sync();
691
+ } catch (error) {
692
+ if (published)
693
+ await unlink(requested).catch(() => {
694
+ return;
695
+ });
696
+ throw error;
697
+ } finally {
698
+ await handle?.close();
699
+ if (stagedExists)
700
+ await unlink(staged).catch(() => {
701
+ return;
702
+ });
703
+ await parentHandle.close();
704
+ }
705
+ };
706
+ var exportHarnessStateBackup = async (config, target) => {
707
+ const bundle = await createHarnessStateBackup(config);
708
+ let bytes = 0;
709
+ for (const chunk of canonicalChunks(bundle)) {
710
+ bytes += Buffer.byteLength(chunk);
711
+ if (bytes > HARNESS_STATE_BACKUP_MAX_BYTES)
712
+ break;
713
+ }
714
+ await writePrivateBackup(target, bytes > HARNESS_STATE_BACKUP_MAX_BYTES ? segmentedChunks(bundle) : canonicalChunks(bundle));
715
+ return {
716
+ schemaVersion: HARNESS_STATE_BACKUP_SCHEMA_VERSION,
717
+ kind: "state-export",
718
+ path: path2.resolve(target),
719
+ checksum: bundle.checksum,
720
+ counts: Object.fromEntries(Object.entries(bundle.records).map(([name, rows]) => [name, rows?.length ?? 0]))
721
+ };
722
+ };
723
+ var readHarnessStateBackup = async (source) => {
724
+ let input;
725
+ try {
726
+ input = await readBackupTransport(path2.resolve(source), HARNESS_STATE_BACKUP_MAX_BYTES);
727
+ } catch (error) {
728
+ if (error instanceof SyntaxError) {
729
+ throw new HarnessStateConflictError("Harness state backup is not valid JSON.", { cause: error });
730
+ }
731
+ throw error;
732
+ }
733
+ return validateStateBackupBundle(input);
734
+ };
735
+ var validateStateBackupBundle = (input) => {
736
+ let bundle;
737
+ try {
738
+ bundle = stateBackupBundleSchema.parse(input);
739
+ } catch (error) {
740
+ throw new HarnessStateConflictError("Harness state backup schema is invalid or unsupported.", { cause: error });
741
+ }
742
+ const { checksum, ...payload } = bundle;
743
+ if (checksumPayload(payload) !== checksum) {
744
+ throw new HarnessStateConflictError("Harness state backup checksum does not match its payload.");
745
+ }
746
+ return bundle;
747
+ };
748
+ var validateBundleBinding = async (config, bundle) => {
749
+ const expected = await bindingForConfig(config);
750
+ if (canonicalJson(expected) !== canonicalJson(bundle.binding)) {
751
+ throw new HarnessStateConflictError("Harness state backup cannot be rebound to a different workspace or scope.");
752
+ }
753
+ const runKeys = new Set(bundle.records.runs.map((run) => run.key));
754
+ if (runKeys.size !== bundle.records.runs.length) {
755
+ throw new HarnessStateConflictError("Harness state backup contains duplicate run keys.");
756
+ }
757
+ const runIds = new Set;
758
+ for (const run of bundle.records.runs) {
759
+ const state = validatedRunState(run.state, `Run ${run.key}`);
760
+ assertTerminalState(state, `Run ${run.key}`);
761
+ if (!stateMatchesScope(config, state)) {
762
+ throw new HarnessStateConflictError(`Run ${run.key} is bound to another durable scope.`);
763
+ }
764
+ runIds.add(state.runId);
765
+ if (run.key !== `${expected.scopePrefix}${state.runId}`) {
766
+ throw new HarnessStateConflictError(`Run ${run.key} does not match its bound scope and runId.`);
767
+ }
768
+ }
769
+ const identities = coordinatorIds(bundle.records.runs);
770
+ const ledgerIdentities = new Set;
771
+ const ledgerKeys = new Set;
772
+ for (const ledger of bundle.records.budgetLedgers ?? []) {
773
+ const identity = validateBudgetLedger(config, ledger, identities);
774
+ if (ledgerIdentities.has(identity) || ledgerKeys.has(ledger.key) || runKeys.has(ledger.key))
775
+ throw new HarnessStateConflictError("Duplicate shared budget ledger.");
776
+ ledgerIdentities.add(identity);
777
+ ledgerKeys.add(ledger.key);
778
+ }
779
+ if (ledgerIdentities.size !== identities.size)
780
+ throw new HarnessStateConflictError("An imported run is missing its shared budget ledger.");
781
+ const journalKeys = new Set;
782
+ for (const entry of bundle.records.toolJournal) {
783
+ const journalKey = `${entry.runKey}\x00${entry.toolCallId}`;
784
+ if (journalKeys.has(journalKey) || !runKeys.has(entry.runKey) || !stateMatchesScope(config, entry.entry) || entry.entry.runId !== entry.runKey.slice(expected.scopePrefix.length)) {
785
+ throw new HarnessStateConflictError(`Tool journal ${entry.toolCallId} is not bound to an imported run.`);
786
+ }
787
+ journalKeys.add(journalKey);
788
+ }
789
+ const idempotencyKeys = new Set;
790
+ for (const entry of bundle.records.idempotency) {
791
+ if (idempotencyKeys.has(entry.key) || !runKeys.has(entry.runKey)) {
792
+ throw new HarnessStateConflictError("Idempotency record is duplicated or targets a missing run.");
793
+ }
794
+ idempotencyKeys.add(entry.key);
795
+ const state = bundle.records.runs.find((run) => run.key === entry.runKey).state;
796
+ if (typeof state.idempotencyKey !== "string" || entry.key !== `${expected.scopePrefix}${state.idempotencyKey}`) {
797
+ throw new HarnessStateConflictError("Idempotency record does not match its bound run state.");
798
+ }
799
+ }
800
+ const parentKeys = new Set;
801
+ for (const entry of bundle.records.parents) {
802
+ if (parentKeys.has(entry.runKey) || !runKeys.has(entry.runKey) || !runKeys.has(entry.parentRunKey)) {
803
+ throw new HarnessStateConflictError("Parent record targets a missing run.");
804
+ }
805
+ parentKeys.add(entry.runKey);
806
+ }
807
+ const memoryKeys = new Set;
808
+ for (const entry of bundle.records.memory) {
809
+ const memoryRunId = entry.key.startsWith(expected.scopePrefix) ? entry.key.slice(expected.scopePrefix.length) : "";
810
+ if (memoryKeys.has(entry.key) || !runIds.has(memoryRunId)) {
811
+ throw new HarnessStateConflictError(`Memory record ${entry.key} is not bound to an imported run.`);
812
+ }
813
+ memoryKeys.add(entry.key);
814
+ }
815
+ for (const session of bundle.records.sessions) {
816
+ if (session.workspaceKey !== expected.workspaceKey || session.scopeKey !== expected.scopeKey) {
817
+ throw new HarnessStateConflictError(`Session ${session.sessionId} is bound to another workspace or scope.`);
818
+ }
819
+ }
820
+ const sessionIds = new Set(bundle.records.sessions.map((session) => session.sessionId));
821
+ if (sessionIds.size !== bundle.records.sessions.length) {
822
+ throw new HarnessStateConflictError("Harness state backup contains duplicate session IDs.");
823
+ }
824
+ for (const session of bundle.records.sessions) {
825
+ if (session.parentSessionId && !sessionIds.has(session.parentSessionId)) {
826
+ throw new HarnessStateConflictError(`Session ${session.sessionId} targets a missing parent session.`);
827
+ }
828
+ }
829
+ const turnIds = new Set;
830
+ const sessionOrdinals = new Set;
831
+ const sessionRunIds = new Set;
832
+ for (const run of bundle.records.sessionRuns) {
833
+ const ordinalKey = `${run.sessionId}\x00${run.ordinal}`;
834
+ const sessionRunKey = `${run.sessionId}\x00${run.runId}`;
835
+ if (turnIds.has(run.turnId) || sessionOrdinals.has(ordinalKey) || sessionRunIds.has(sessionRunKey) || !sessionIds.has(run.sessionId) || !runIds.has(run.runId) || !terminalSessionStatuses.has(run.status)) {
836
+ throw new HarnessStateConflictError(`Session run ${run.runId} is active or targets a missing session.`);
837
+ }
838
+ turnIds.add(run.turnId);
839
+ sessionOrdinals.add(ordinalKey);
840
+ sessionRunIds.add(sessionRunKey);
841
+ }
842
+ const checkpointIds = new Set;
843
+ for (const record of bundle.records.workspaceCheckpoints ?? []) {
844
+ if (checkpointIds.has(record.id) || record.id !== record.body.id || record.workspaceKey !== expected.workspaceKey || record.scopeKey !== expected.scopeKey) {
845
+ throw new HarnessStateConflictError("Invalid checkpoint identity or workspace/scope binding.");
846
+ }
847
+ checkpointIds.add(record.id);
848
+ const checkpoint = record.kind === "checkpoint" ? record.body : record.body.checkpoint;
849
+ const turn = bundle.records.sessionRuns.find((run) => run.turnId === checkpoint.turnId && run.sessionId === checkpoint.sessionId);
850
+ if (!turn || !terminalSessionStatuses.has(turn.status))
851
+ throw new HarnessStateConflictError("Checkpoint targets a missing terminal turn.");
852
+ if (new Set(checkpoint.files.map((file) => file.path)).size !== checkpoint.files.length || checkpoint.files.some((file) => `sha256:${createHash("sha256").update(file.content).digest("hex")}` !== file.digest || Buffer.byteLength(file.content) > 65536)) {
853
+ throw new HarnessStateConflictError("Checkpoint content identity is invalid.");
854
+ }
855
+ if (record.kind === "restore") {
856
+ const operation = record.body;
857
+ if (operation.stage !== "completed")
858
+ throw new HarnessStateConflictError("State backup refuses an unfinished workspace restore operation.");
859
+ const fork = bundle.records.sessions.find((session) => session.sessionId === operation.forkSessionId);
860
+ if (!fork || fork.parentSessionId !== checkpoint.sessionId || fork.forkedFromTurnId !== checkpoint.turnId) {
861
+ throw new HarnessStateConflictError("Restore operation targets a missing conversation fork.");
862
+ }
863
+ }
864
+ }
865
+ for (const run of bundle.records.sessionRuns) {
866
+ if (run.sourceTurnId && !turnIds.has(run.sourceTurnId)) {
867
+ throw new HarnessStateConflictError(`Session run ${run.runId} targets a missing source turn.`);
868
+ }
869
+ }
870
+ };
871
+ var rowJson = (value) => canonicalJson(value);
872
+ var setEquals = (left, right) => left.size === right.size && [...left].every((value) => right.has(value));
873
+ var importHarnessStateBackup = async (config, bundle, options = {}) => {
874
+ bundle = validateStateBackupBundle(bundle);
875
+ await validateBundleBinding(config, bundle);
876
+ const databasePath = options.dryRun ? await locateExistingDatabase(config) : await prepareDatabase(config);
877
+ if (!databasePath) {
878
+ return {
879
+ schemaVersion: HARNESS_STATE_BACKUP_SCHEMA_VERSION,
880
+ kind: "state-import",
881
+ dryRun: true,
882
+ checksum: bundle.checksum,
883
+ inserted: {},
884
+ identical: 0
885
+ };
886
+ }
887
+ const database = new SqliteDatabase(databasePath, {
888
+ create: false,
889
+ strict: true,
890
+ ...options.dryRun ? { readonly: true } : {}
891
+ });
892
+ database.exec("PRAGMA busy_timeout = 5000");
893
+ database.exec("PRAGMA foreign_keys = ON");
894
+ database.exec(options.dryRun ? "BEGIN" : "BEGIN IMMEDIATE");
895
+ const availableTables = new Set(database.query("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((row) => row.name));
896
+ let writes = 0;
897
+ let identical = 0;
898
+ const inserted = {};
899
+ const checkFailure = () => {
900
+ if (options.failAfterWrites !== undefined && writes >= options.failAfterWrites) {
901
+ throw new Error("Injected state import failure.");
902
+ }
903
+ };
904
+ const insertOrCompare = (table, keyWhere, keyBindings, selectColumns, expected, insertSql, insertBindings) => {
905
+ if (!availableTables.has(table)) {
906
+ if (options.dryRun)
907
+ return;
908
+ throw new HarnessStateConflictError(`Required state table ${table} is unavailable.`);
909
+ }
910
+ const existing = database.query(`SELECT ${selectColumns} FROM ${table} WHERE ${keyWhere}`).get(...keyBindings);
911
+ if (existing) {
912
+ const actual = Object.values(existing).map((value) => typeof value === "string" && (value.startsWith("{") || value.startsWith("[")) ? rowJson(JSON.parse(value)) : value);
913
+ const normalizedExpected = expected.map((value) => typeof value === "string" && (value.startsWith("{") || value.startsWith("[")) ? rowJson(JSON.parse(value)) : value);
914
+ if (rowJson(actual) !== rowJson(normalizedExpected)) {
915
+ throw new HarnessStateConflictError(`Conflicting state already exists in ${table}.`);
916
+ }
917
+ identical += 1;
918
+ return;
919
+ }
920
+ if (options.dryRun)
921
+ return;
922
+ checkFailure();
923
+ database.query(insertSql).run(...insertBindings);
924
+ writes += 1;
925
+ inserted[table] = (inserted[table] ?? 0) + 1;
926
+ };
927
+ try {
928
+ const expectedKeys = {
929
+ runs: new Set([...bundle.records.runs, ...bundle.records.budgetLedgers ?? []].map((row) => row.key)),
930
+ toolJournal: new Set(bundle.records.toolJournal.map((row) => `${row.runKey}\x00${row.toolCallId}`)),
931
+ idempotency: new Set(bundle.records.idempotency.map((row) => row.key)),
932
+ parents: new Set(bundle.records.parents.map((row) => row.runKey)),
933
+ memory: new Set(bundle.records.memory.map((row) => row.key)),
934
+ sessions: new Set(bundle.records.sessions.map((row) => row.sessionId)),
935
+ sessionRuns: new Set(bundle.records.sessionRuns.map((row) => row.turnId)),
936
+ workspaceCheckpoints: new Set((bundle.records.workspaceCheckpoints ?? []).map((row) => row.id))
937
+ };
938
+ const expectedBudgetKeys = new Set((bundle.records.budgetLedgers ?? []).map((row) => row.key));
939
+ const destinationRunRows = availableTables.has("zhivex_agent_runs") ? database.query("SELECT run_id, state_json, updated_at_ms FROM zhivex_agent_runs ORDER BY run_id").all().filter((row) => expectedBudgetKeys.has(row.run_id) || stateMatchesScope(config, parseJsonRecord(row.state_json, `Run ${row.run_id}`))) : [];
940
+ const destinationRunKeys = new Set(destinationRunRows.map((row) => row.run_id));
941
+ const destinationSessionRows = availableTables.has("zhivex_cli_sessions") ? database.query("SELECT * FROM zhivex_cli_sessions ORDER BY session_id").all().filter((row) => row.workspace_key === bundle.binding.workspaceKey && row.scope_key === bundle.binding.scopeKey) : [];
942
+ const destinationSessionIds = new Set(destinationSessionRows.map((row) => row.session_id));
943
+ const destinationKeys = {
944
+ workspaceCheckpoints: new Set(availableTables.has("zhivex_workspace_checkpoints") ? database.query("SELECT * FROM zhivex_workspace_checkpoints WHERE workspace_key=? AND scope_key=?").all(bundle.binding.workspaceKey, bundle.binding.scopeKey).map((row) => row.id) : []),
945
+ runs: destinationRunKeys,
946
+ toolJournal: new Set(availableTables.has("zhivex_agent_runs_tool_journal") ? database.query("SELECT run_key, tool_call_id, entry_json, revision, updated_at_ms FROM zhivex_agent_runs_tool_journal").all().filter((row) => destinationRunKeys.has(row.run_key)).map((row) => `${row.run_key}\x00${row.tool_call_id}`) : []),
947
+ idempotency: new Set(availableTables.has("zhivex_agent_runs_idempotency") ? database.query("SELECT idempotency_key, run_id, updated_at_ms FROM zhivex_agent_runs_idempotency").all().filter((row) => destinationRunKeys.has(row.run_id)).map((row) => row.idempotency_key) : []),
948
+ parents: new Set(availableTables.has("zhivex_agent_runs_parents") ? database.query("SELECT run_id, parent_run_id, updated_at_ms FROM zhivex_agent_runs_parents").all().filter((row) => destinationRunKeys.has(row.run_id)).map((row) => row.run_id) : []),
949
+ memory: new Set(availableTables.has("zhivex_agent_memory") ? database.query("SELECT memory_key, messages_json, updated_at_ms FROM zhivex_agent_memory").all().filter((row) => destinationRunRows.some((run) => {
950
+ const state = JSON.parse(run.state_json);
951
+ return row.memory_key === `${bundle.binding.scopePrefix}${String(state.runId)}`;
952
+ })).map((row) => row.memory_key) : []),
953
+ sessions: destinationSessionIds,
954
+ sessionRuns: new Set(availableTables.has("zhivex_cli_session_runs") ? database.query("SELECT * FROM zhivex_cli_session_runs ORDER BY session_id, ordinal").all().filter((row) => destinationSessionIds.has(row.session_id)).map((row) => row.turn_id) : [])
955
+ };
956
+ if (availableTables.has("zhivex_agent_runs_leases")) {
957
+ const activeLease = database.query("SELECT run_key, expires_at_ms FROM zhivex_agent_runs_leases").all().some((row) => (destinationRunKeys.has(row.run_key) || expectedKeys.runs.has(row.run_key)) && row.expires_at_ms > Date.now());
958
+ if (activeLease) {
959
+ throw new HarnessStateConflictError("State import refuses a destination with active run leases.", {
960
+ retryable: true
961
+ });
962
+ }
963
+ }
964
+ const destinationHasState = Object.values(destinationKeys).some((keys) => keys.size > 0);
965
+ if (destinationHasState && Object.keys(expectedKeys).some((name) => !setEquals(destinationKeys[name], expectedKeys[name]))) {
966
+ throw new HarnessStateConflictError("State import requires an empty destination or an exactly identical prior import.");
967
+ }
968
+ for (const run of [...bundle.records.runs, ...bundle.records.budgetLedgers ?? []])
969
+ insertOrCompare("zhivex_agent_runs", "run_id = ?", [run.key], "state_json, updated_at_ms", [JSON.stringify(run.state), run.updatedAt], "INSERT INTO zhivex_agent_runs (run_id, state_json, updated_at_ms) VALUES (?, ?, ?)", [run.key, JSON.stringify(run.state), run.updatedAt]);
970
+ for (const entry of bundle.records.idempotency)
971
+ insertOrCompare("zhivex_agent_runs_idempotency", "idempotency_key = ?", [entry.key], "run_id, updated_at_ms", [entry.runKey, entry.updatedAt], "INSERT INTO zhivex_agent_runs_idempotency (idempotency_key, run_id, updated_at_ms) VALUES (?, ?, ?)", [entry.key, entry.runKey, entry.updatedAt]);
972
+ for (const entry of bundle.records.parents)
973
+ insertOrCompare("zhivex_agent_runs_parents", "run_id = ?", [entry.runKey], "parent_run_id, updated_at_ms", [entry.parentRunKey, entry.updatedAt], "INSERT INTO zhivex_agent_runs_parents (run_id, parent_run_id, updated_at_ms) VALUES (?, ?, ?)", [entry.runKey, entry.parentRunKey, entry.updatedAt]);
974
+ for (const entry of bundle.records.toolJournal)
975
+ insertOrCompare("zhivex_agent_runs_tool_journal", "run_key = ? AND tool_call_id = ?", [entry.runKey, entry.toolCallId], "entry_json, revision, updated_at_ms", [JSON.stringify(entry.entry), entry.revision, entry.updatedAt], "INSERT INTO zhivex_agent_runs_tool_journal (run_key, tool_call_id, entry_json, revision, updated_at_ms) VALUES (?, ?, ?, ?, ?)", [entry.runKey, entry.toolCallId, JSON.stringify(entry.entry), entry.revision, entry.updatedAt]);
976
+ for (const entry of bundle.records.memory)
977
+ insertOrCompare("zhivex_agent_memory", "memory_key = ?", [entry.key], "messages_json, updated_at_ms", [JSON.stringify(entry.messages), entry.updatedAt], "INSERT INTO zhivex_agent_memory (memory_key, messages_json, updated_at_ms) VALUES (?, ?, ?)", [entry.key, JSON.stringify(entry.messages), entry.updatedAt]);
978
+ for (const session of bundle.records.sessions)
979
+ insertOrCompare("zhivex_cli_sessions", "session_id = ?", [session.sessionId], "workspace_key, scope_key, title, parent_session_id, forked_from_turn_id, revision, activity_seq, created_at, updated_at, archived_at, deleted_at, metadata_bytes", [
980
+ session.workspaceKey,
981
+ session.scopeKey,
982
+ session.title,
983
+ session.parentSessionId,
984
+ session.forkedFromTurnId,
985
+ session.revision,
986
+ session.activitySequence,
987
+ session.createdAt,
988
+ session.updatedAt,
989
+ session.archivedAt,
990
+ session.deletedAt,
991
+ session.metadataBytes
992
+ ], "INSERT INTO zhivex_cli_sessions (session_id, workspace_key, scope_key, title, parent_session_id, forked_from_turn_id, revision, activity_seq, created_at, updated_at, archived_at, deleted_at, metadata_bytes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
993
+ session.sessionId,
994
+ session.workspaceKey,
995
+ session.scopeKey,
996
+ session.title,
997
+ session.parentSessionId,
998
+ session.forkedFromTurnId,
999
+ session.revision,
1000
+ session.activitySequence,
1001
+ session.createdAt,
1002
+ session.updatedAt,
1003
+ session.archivedAt,
1004
+ session.deletedAt,
1005
+ session.metadataBytes
1006
+ ]);
1007
+ for (const run of bundle.records.sessionRuns)
1008
+ insertOrCompare("zhivex_cli_session_runs", "turn_id = ?", [run.turnId], "source_turn_id, session_id, ordinal, run_id, provider, model, role, status, created_at, updated_at, completed_at, metadata_bytes", [
1009
+ run.sourceTurnId,
1010
+ run.sessionId,
1011
+ run.ordinal,
1012
+ run.runId,
1013
+ run.provider,
1014
+ run.model,
1015
+ run.role,
1016
+ run.status,
1017
+ run.createdAt,
1018
+ run.updatedAt,
1019
+ run.completedAt,
1020
+ run.metadataBytes
1021
+ ], "INSERT INTO zhivex_cli_session_runs (turn_id, source_turn_id, session_id, ordinal, run_id, provider, model, role, status, created_at, updated_at, completed_at, metadata_bytes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
1022
+ run.turnId,
1023
+ run.sourceTurnId,
1024
+ run.sessionId,
1025
+ run.ordinal,
1026
+ run.runId,
1027
+ run.provider,
1028
+ run.model,
1029
+ run.role,
1030
+ run.status,
1031
+ run.createdAt,
1032
+ run.updatedAt,
1033
+ run.completedAt,
1034
+ run.metadataBytes
1035
+ ]);
1036
+ if ((bundle.records.workspaceCheckpoints?.length ?? 0) > 0 && !options.dryRun && !availableTables.has("zhivex_workspace_checkpoints")) {
1037
+ database.exec(checkpointTableSql);
1038
+ availableTables.add("zhivex_workspace_checkpoints");
1039
+ }
1040
+ for (const record of bundle.records.workspaceCheckpoints ?? [])
1041
+ insertOrCompare("zhivex_workspace_checkpoints", "id=?", [record.id], "workspace_key, scope_key, kind, body", [record.workspaceKey, record.scopeKey, record.kind, JSON.stringify(record.body)], "INSERT INTO zhivex_workspace_checkpoints(id,workspace_key,scope_key,kind,body) VALUES(?,?,?,?,?)", [record.id, record.workspaceKey, record.scopeKey, record.kind, JSON.stringify(record.body)]);
1042
+ if (options.dryRun)
1043
+ database.exec("ROLLBACK");
1044
+ else
1045
+ database.exec("COMMIT");
1046
+ return {
1047
+ schemaVersion: HARNESS_STATE_BACKUP_SCHEMA_VERSION,
1048
+ kind: "state-import",
1049
+ dryRun: options.dryRun ?? false,
1050
+ checksum: bundle.checksum,
1051
+ inserted,
1052
+ identical
1053
+ };
1054
+ } catch (error) {
1055
+ database.exec("ROLLBACK");
1056
+ throw error;
1057
+ } finally {
1058
+ database.close(false);
1059
+ }
1060
+ };
1061
+ var importHarnessStateBackupFile = async (config, source, options = {}) => importHarnessStateBackup(config, await readHarnessStateBackup(source), options);
1062
+ var inspectHarnessState = async (config) => {
1063
+ const payload = await readPayload(config);
1064
+ return {
1065
+ schemaVersion: HARNESS_STATE_BACKUP_SCHEMA_VERSION,
1066
+ kind: "state-status",
1067
+ compatible: true,
1068
+ schemas: payload.schemas,
1069
+ counts: Object.fromEntries(Object.entries(payload.records).map(([name, rows]) => [name, rows?.length ?? 0]))
1070
+ };
1071
+ };
1072
+
1073
+ export { validateRecordedWorkspace, HARNESS_STATE_BACKUP_SCHEMA_VERSION, HARNESS_STATE_BACKUP_MAX_BYTES, stateBackupBundleSchema, createHarnessStateBackup, createArchivedHarnessStateBackup, exportHarnessStateBackup, readHarnessStateBackup, importHarnessStateBackup, importHarnessStateBackupFile, inspectHarnessState };