@zhivex-ai/harness 1.4.0-rc.6 → 1.4.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 (92) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +124 -71
  3. package/SECURITY.md +1 -1
  4. package/SUPPORT.md +2 -2
  5. package/contracts/engine-api.json +55 -0
  6. package/contracts/stable-api-signatures.json +33 -33
  7. package/dist/acp-cli.js +18 -15
  8. package/dist/cli/arguments.d.ts +3 -0
  9. package/dist/cli/console/console-budget.d.ts +8 -2
  10. package/dist/{cli-s1rs15vv.js → cli-2h37qyvg.js} +34 -15
  11. package/dist/{cli-entry-d5788aay.js → cli-entry-20w6nh0k.js} +359 -21
  12. package/dist/{cli-entry-9c2k0zwz.js → cli-entry-4ngptr62.js} +2902 -1482
  13. package/dist/{cli-entry-4k1cf0m3.js → cli-entry-w0hterjs.js} +8 -5
  14. package/dist/cli.js +2 -2
  15. package/dist/client/json-contracts.d.ts +95 -2
  16. package/dist/client/local-service.d.ts +2 -2
  17. package/dist/client/protocol.d.ts +3 -0
  18. package/dist/client/stream.d.ts +2 -1
  19. package/dist/compat/code-support.d.ts +6 -0
  20. package/dist/compat/code-support.js +29 -6
  21. package/dist/{config-c002f06f.js → config-j9624t6t.js} +1 -1
  22. package/dist/{config-j3jga5wh.js → config-qt69hkqj.js} +4 -2
  23. package/dist/engine/acp.js +14 -12
  24. package/dist/engine/client.js +11 -10
  25. package/dist/engine/desktop-providers-v1.js +2 -1
  26. package/dist/engine/desktop-state-v1.js +9 -9
  27. package/dist/engine/index.js +48 -46
  28. package/dist/engine/mcp-stdio-v1.js +7 -6
  29. package/dist/engine/protocol.js +30 -2
  30. package/dist/engine/service.js +12 -11
  31. package/dist/hostile-repository-demo.js +4773 -3027
  32. package/dist/{index-49ys3s4q.js → index-5jhgatga.js} +35 -16
  33. package/dist/{index-wq2tax80.js → index-61gthdz6.js} +1 -1
  34. package/dist/index-6dft2vev.js +343 -0
  35. package/dist/{index-gq9xagx2.js → index-8zjmnyw5.js} +25 -138
  36. package/dist/index-937cb7y8.js +705 -0
  37. package/dist/index-bpc596v3.js +154 -0
  38. package/dist/index-fm84men1.js +239 -0
  39. package/dist/{index-e6r04a18.js → index-m34q25jj.js} +1127 -170
  40. package/dist/index-m49jx72n.js +220 -0
  41. package/dist/{index-dj5z774k.js → index-m82cfspw.js} +35 -16
  42. package/dist/{index-qfpz5y2v.js → index-nmjdwd4p.js} +8 -2
  43. package/dist/{index-0g939pfs.js → index-pdmevfxv.js} +10 -7
  44. package/dist/{index-4j1h4j8c.js → index-pp474137.js} +1598 -973
  45. package/dist/{index-2tgb2b4e.js → index-rkbf7dqg.js} +3 -5
  46. package/dist/index-s2k2edts.js +3778 -0
  47. package/dist/{index-z6br6qdn.js → index-tfzta544.js} +28 -11
  48. package/dist/index.js +44 -42
  49. package/dist/persistence/operations.d.ts +3 -0
  50. package/dist/providers/vertex-auth.d.ts +5 -1
  51. package/dist/runtime/config.d.ts +8 -0
  52. package/dist/runtime/context-runtime.d.ts +1 -0
  53. package/dist/runtime/error-diagnostics.d.ts +8 -0
  54. package/dist/runtime/errors.d.ts +10 -0
  55. package/dist/runtime/harness.d.ts +9 -0
  56. package/dist/runtime/policy-inspection.d.ts +1 -0
  57. package/dist/runtime/provider-transient.d.ts +33 -0
  58. package/dist/runtime/runtime-diagnostics.d.ts +3 -0
  59. package/dist/runtime/runtime-policy.d.ts +8 -10
  60. package/dist/runtime/task-acceptance-host.d.ts +1 -1
  61. package/dist/runtime/task-budget-host.d.ts +217 -0
  62. package/dist/runtime/task-budget.d.ts +147 -0
  63. package/dist/runtime/task-continuity.d.ts +434 -0
  64. package/dist/runtime/task-telemetry.d.ts +63 -0
  65. package/dist/runtime/task-transport.d.ts +3 -0
  66. package/dist/runtime/task-usage-context.d.ts +13 -0
  67. package/dist/runtime/time-to-safe-fix.d.ts +8 -0
  68. package/dist/runtime/usage-ledger.d.ts +17 -0
  69. package/dist/service-cli.js +12 -11
  70. package/dist/{service-client-cli-b52c67hx.js → service-client-cli-fn54ev6w.js} +18 -7
  71. package/dist/verify-historical-migrations.js +2426 -652
  72. package/dist/zhx.js +2 -2
  73. package/docs/CLI.md +38 -8
  74. package/docs/CLIENT_PROTOCOL.md +9 -1
  75. package/docs/CODE_RELEASE.md +118 -1
  76. package/docs/DURABLE_OPERATIONS.md +1 -1
  77. package/docs/EXTENSIBILITY.md +1 -1
  78. package/docs/FIRST_USE.md +6 -6
  79. package/docs/MINIMUM_OFFLINE_EVALS.md +31 -0
  80. package/docs/MINIMUM_TASK_AUTHORITY.md +395 -0
  81. package/docs/PROJECT_MEMORY.md +1 -1
  82. package/docs/README.md +2 -0
  83. package/docs/STABILITY.md +1 -1
  84. package/docs/WEB_LIMITS.md +117 -0
  85. package/evaluations/representative-assembly-matrix.json +28 -1
  86. package/examples/README.md +5 -5
  87. package/package.json +8 -5
  88. package/dist/index-8705zxpr.js +0 -145
  89. package/dist/index-a1q355va.js +0 -600
  90. package/dist/index-r0tdj04h.js +0 -976
  91. package/dist/index-rd0s3gqj.js +0 -1669
  92. package/dist/index-vtqfjnab.js +0 -1626
@@ -1,17 +1,19 @@
1
1
  import {
2
- createHarnessClientAdapter,
2
+ createHarnessClientAdapter
3
+ } from "./index-937cb7y8.js";
4
+ import {
3
5
  harnessPolicyDecisionEventSchema
4
- } from "./index-4j1h4j8c.js";
6
+ } from "./index-pp474137.js";
5
7
  import {
6
8
  openCliSessionStore
7
- } from "./index-vtqfjnab.js";
9
+ } from "./index-s2k2edts.js";
8
10
  import {
9
11
  SqliteDatabase
10
- } from "./index-r0tdj04h.js";
12
+ } from "./index-m34q25jj.js";
11
13
  import {
12
14
  harnessClientRequestSchema,
13
15
  streamEventDocument
14
- } from "./index-qfpz5y2v.js";
16
+ } from "./index-nmjdwd4p.js";
15
17
  import {
16
18
  __require
17
19
  } from "./index-1bx83sw4.js";
@@ -63,6 +65,14 @@ var openHarnessActivityStore = async (config, options = {}) => {
63
65
  db.exec(`CREATE TABLE IF NOT EXISTS client_activity_events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, scope TEXT NOT NULL, session TEXT NOT NULL, run TEXT NOT NULL, at INTEGER NOT NULL, activity TEXT NOT NULL);
64
66
  CREATE INDEX IF NOT EXISTS client_activity_scope ON client_activity_events(scope, session, sequence);
65
67
  CREATE TABLE IF NOT EXISTS client_activity_snapshots(scope TEXT NOT NULL, session TEXT NOT NULL, sequence INTEGER NOT NULL, expired_through INTEGER NOT NULL DEFAULT 0, snapshot TEXT NOT NULL, PRIMARY KEY(scope,session));`);
68
+ const readSnapshot = db.query("SELECT snapshot,expired_through FROM client_activity_snapshots WHERE scope=? AND session=?");
69
+ const expiredEvents = db.query(`SELECT session,MAX(sequence) AS seq FROM client_activity_events WHERE scope=? AND (at < ? OR sequence NOT IN (SELECT sequence FROM client_activity_events WHERE scope=? ORDER BY sequence DESC LIMIT ?)) GROUP BY session`);
70
+ const markExpired = db.query("UPDATE client_activity_snapshots SET expired_through=MAX(expired_through,?) WHERE scope=? AND session=?");
71
+ const deleteExpired = db.query("DELETE FROM client_activity_events WHERE scope=? AND session=? AND sequence<=?");
72
+ const insertEvent = db.query("INSERT INTO client_activity_events(scope,session,run,at,activity) VALUES(?,?,?,?,?)");
73
+ const lastSequence = db.query("SELECT last_insert_rowid() AS seq");
74
+ const saveSnapshot = db.query("INSERT INTO client_activity_snapshots(scope,session,sequence,snapshot) VALUES(?,?,?,?) ON CONFLICT(scope,session) DO UPDATE SET sequence=excluded.sequence,snapshot=excluded.snapshot");
75
+ const readEvents = db.query("SELECT sequence,session,run,at,activity FROM client_activity_events WHERE scope=? AND session=? AND sequence>? ORDER BY sequence LIMIT 201");
66
76
  const redaction = createRedactionPolicy({ includeEmails: true });
67
77
  const redact = (text) => {
68
78
  let result = text;
@@ -111,14 +121,14 @@ var openHarnessActivityStore = async (config, options = {}) => {
111
121
  return result;
112
122
  };
113
123
  const snapshot = (sessionId) => {
114
- const row = db.query("SELECT snapshot,expired_through FROM client_activity_snapshots WHERE scope=? AND session=?").get(scope, sessionId);
124
+ const row = readSnapshot.get(scope, sessionId);
115
125
  return { state: row ? JSON.parse(row.snapshot) : { schemaVersion: 1, sessionId, sequence: 0, runs: {} }, expiredThrough: row?.expired_through ?? 0 };
116
126
  };
117
127
  const prune = () => {
118
- const expired = db.query(`SELECT session,MAX(sequence) AS seq FROM client_activity_events WHERE scope=? AND (at < ? OR sequence NOT IN (SELECT sequence FROM client_activity_events WHERE scope=? ORDER BY sequence DESC LIMIT ?)) GROUP BY session`).all(scope, now() - retentionMs, scope, maxEvents);
128
+ const expired = expiredEvents.all(scope, now() - retentionMs, scope, maxEvents);
119
129
  for (const row of expired) {
120
- db.query("UPDATE client_activity_snapshots SET expired_through=MAX(expired_through,?) WHERE scope=? AND session=?").run(row.seq, scope, row.session);
121
- db.query("DELETE FROM client_activity_events WHERE scope=? AND session=? AND sequence<=?").run(scope, row.session, row.seq);
130
+ markExpired.run(row.seq, scope, row.session);
131
+ deleteExpired.run(scope, row.session, row.seq);
122
132
  }
123
133
  };
124
134
  const write = (sessionId, runId, activity) => {
@@ -160,10 +170,10 @@ var openHarnessActivityStore = async (config, options = {}) => {
160
170
  if (activity.type === "tool-approval-request" || activity.type === "agent-approval-request")
161
171
  state.status = "waiting_approval";
162
172
  current.runs[runId] = state;
163
- db.query("INSERT INTO client_activity_events(scope,session,run,at,activity) VALUES(?,?,?,?,?)").run(scope, sessionId, runId, at, encoded);
164
- current.sequence = db.query("SELECT last_insert_rowid() AS seq").get().seq;
173
+ insertEvent.run(scope, sessionId, runId, at, encoded);
174
+ current.sequence = lastSequence.get().seq;
165
175
  const bounded = serializeBoundedSnapshot(current, runId);
166
- db.query("INSERT INTO client_activity_snapshots(scope,session,sequence,snapshot) VALUES(?,?,?,?) ON CONFLICT(scope,session) DO UPDATE SET sequence=excluded.sequence,snapshot=excluded.snapshot").run(scope, sessionId, current.sequence, bounded);
176
+ saveSnapshot.run(scope, sessionId, current.sequence, bounded);
167
177
  prune();
168
178
  db.exec("COMMIT");
169
179
  } catch (e) {
@@ -250,7 +260,7 @@ var openHarnessActivityStore = async (config, options = {}) => {
250
260
  throw new Error("ACTIVITY_CURSOR_AHEAD");
251
261
  if (expired)
252
262
  return { schemaVersion: 1, cursorExpired: true, hasMore: false, policyEvidenceIncomplete: true, nextCursor: current.state.sequence, events: [], snapshot: current.state };
253
- const rows = db.query("SELECT sequence,session,run,at,activity FROM client_activity_events WHERE scope=? AND session=? AND sequence>? ORDER BY sequence LIMIT 201").all(scope, sessionId, after);
263
+ const rows = readEvents.all(scope, sessionId, after);
254
264
  const events = rows.slice(0, 200).map((row) => ({ schemaVersion: 1, eventId: createHash("sha256").update(`${scope}:${row.sequence}`).digest("hex"), sequence: row.sequence, sessionId: row.session, runId: row.run, at: row.at, activity: JSON.parse(row.activity) }));
255
265
  return { schemaVersion: 1, cursorExpired: false, hasMore: rows.length > 200, policyEvidenceIncomplete: current.expiredThrough > 0, nextCursor: events.at(-1)?.sequence ?? after, events };
256
266
  },
@@ -416,9 +426,18 @@ var startHarnessLocalService = async (harness, options) => {
416
426
  adapter = await createHarnessClientAdapter(harness, {
417
427
  ...options.approvalNow ? { now: options.approvalNow } : {},
418
428
  onPolicyDecision: (sessionId, runId, event) => activity.policyDecision(sessionId, runId, event),
419
- onPrompt: (sessionId, runId, prompt) => activity.prompt(sessionId, runId, prompt),
420
- onEvent: (sessionId, runId, event) => activity.append(sessionId, runId, event),
421
- onCheckpoint: (sessionId, runId, status) => activity.checkpoint(sessionId, runId, status)
429
+ onPrompt: async (sessionId, runId, prompt) => {
430
+ await activity.prompt(sessionId, runId, prompt);
431
+ await options.onPrompt?.(sessionId, runId, prompt);
432
+ },
433
+ onEvent: async (sessionId, runId, event) => {
434
+ await activity.append(sessionId, runId, event);
435
+ await options.onEvent?.(sessionId, runId, event);
436
+ },
437
+ onCheckpoint: async (sessionId, runId, status) => {
438
+ await activity.checkpoint(sessionId, runId, status);
439
+ await options.onCheckpoint?.(sessionId, runId, status);
440
+ }
422
441
  });
423
442
  await new Promise((resolve, reject) => {
424
443
  server.once("error", reject);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_PROVIDER_REGISTRY,
3
3
  HARNESS_SUBAGENT_PROFILES
4
- } from "./index-gq9xagx2.js";
4
+ } from "./index-8zjmnyw5.js";
5
5
 
6
6
  // src/providers/routing.ts
7
7
  var routeError = (value) => new Error(`Invalid route: ${value}. Use <profile>=<provider>[:<model>].`);
@@ -0,0 +1,343 @@
1
+ import {
2
+ TERMINAL_SESSION_RUN_STATUSES
3
+ } from "./index-s2k2edts.js";
4
+ import {
5
+ SqliteDatabase,
6
+ createEditProposal,
7
+ fileDigestSchema,
8
+ readRegularFileNoFollow,
9
+ statRegularFileNoFollow,
10
+ withWorkspaceMutation,
11
+ workspaceFilePathSchema
12
+ } from "./index-m34q25jj.js";
13
+
14
+ // src/persistence/workspace-checkpoints.ts
15
+ import { createHash, randomUUID } from "node:crypto";
16
+ import path from "node:path";
17
+ import { z } from "zod";
18
+ var fileSchema = z.strictObject({
19
+ path: workspaceFilePathSchema,
20
+ digest: fileDigestSchema,
21
+ content: z.string().max(65536),
22
+ mode: z.number().int().min(0).max(511)
23
+ }).refine((file) => Buffer.byteLength(file.content) <= 65536 && !file.content.includes("\x00") && file.digest === `sha256:${createHash("sha256").update(file.content).digest("hex")}`, "Invalid checkpoint content identity.");
24
+ var workspaceCheckpointSchema = z.strictObject({
25
+ id: z.string().uuid(),
26
+ sessionId: z.string(),
27
+ turnId: z.string(),
28
+ files: z.array(fileSchema).min(1).max(20)
29
+ });
30
+ var workspaceRestoreOperationSchema = z.strictObject({
31
+ id: z.string().uuid(),
32
+ checkpoint: workspaceCheckpointSchema,
33
+ proposalId: fileDigestSchema,
34
+ expected: z.record(z.string(), fileDigestSchema),
35
+ stage: z.enum(["prepared", "forking", "forked", "applying", "applied", "completed"]),
36
+ forkSessionId: z.string().optional()
37
+ });
38
+ var digest = (content) => `sha256:${createHash("sha256").update(content).digest("hex")}`;
39
+ var retentionSelection = z.object({ checkpointIds: z.array(z.string().uuid()).max(100), completedRestoreIds: z.array(z.string().uuid()).max(100) }).strict().refine((value) => value.checkpointIds.length + value.completedRestoreIds.length > 0 && new Set([...value.checkpointIds, ...value.completedRestoreIds]).size === value.checkpointIds.length + value.completedRestoreIds.length, "Select unique checkpoint and completed restore IDs.");
40
+ var openWorkspaceCheckpointStore = async (workspace, sessions) => {
41
+ const key = createHash("sha256").update("workspace\x00").update(workspace.root).digest("hex");
42
+ if (key !== sessions.workspaceKey)
43
+ throw new Error("Checkpoint session store belongs to a different workspace.");
44
+ const before = await statRegularFileNoFollow(sessions.databasePath, { label: "Checkpoint database", requireSingleLink: true });
45
+ if (before.uid !== process.getuid?.() || before.mode & 63)
46
+ throw new Error("Checkpoint database must be private.");
47
+ const db = new SqliteDatabase(sessions.databasePath, { create: false });
48
+ const after = await statRegularFileNoFollow(sessions.databasePath, { label: "Checkpoint database", requireSingleLink: true });
49
+ if (before.dev !== after.dev || before.ino !== after.ino) {
50
+ db.close();
51
+ throw new Error("Checkpoint database changed.");
52
+ }
53
+ db.exec("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)");
54
+ const save = (kind, value) => {
55
+ const existing = db.query("SELECT id FROM zhivex_workspace_checkpoints WHERE id=?").get(value.id);
56
+ const count = db.query("SELECT COUNT(*) AS count FROM zhivex_workspace_checkpoints WHERE workspace_key=? AND scope_key=?").get(sessions.workspaceKey, sessions.scopeKey)?.count ?? 0;
57
+ if (!existing && count >= 100)
58
+ throw new Error("Checkpoint scope storage limit reached (100 records).");
59
+ const body = JSON.stringify(value);
60
+ if (Buffer.byteLength(body) > 2 * 1024 * 1024)
61
+ throw new Error("Checkpoint exceeds storage limit.");
62
+ db.query("INSERT INTO zhivex_workspace_checkpoints(id, workspace_key, scope_key, kind, body) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET body=excluded.body WHERE workspace_key=excluded.workspace_key AND scope_key=excluded.scope_key AND kind=excluded.kind").run(value.id, sessions.workspaceKey, sessions.scopeKey, kind, body);
63
+ };
64
+ const load = (kind, id) => {
65
+ const row = db.query("SELECT body FROM zhivex_workspace_checkpoints WHERE id=? AND workspace_key=? AND scope_key=? AND kind=?").get(id, sessions.workspaceKey, sessions.scopeKey, kind);
66
+ if (!row)
67
+ throw new Error("Unknown checkpoint or restore operation in this scope.");
68
+ return JSON.parse(row.body);
69
+ };
70
+ const read = async (target) => {
71
+ const authorized = await workspace.readFile(target, 1, 1);
72
+ const { contents, stat } = await readRegularFileNoFollow(path.join(workspace.root, target), {
73
+ label: "Checkpoint target",
74
+ maxBytes: 65536,
75
+ requireSingleLink: true
76
+ });
77
+ const content = new TextDecoder("utf-8", { fatal: true }).decode(contents);
78
+ if (contents.includes(0) || digest(contents) !== authorized.digest)
79
+ throw new Error("Checkpoint file changed while reading.");
80
+ return fileSchema.parse({ path: target, digest: digest(contents), content, mode: stat.mode & 511 });
81
+ };
82
+ const validateTurn = async (checkpoint) => {
83
+ const session = await sessions.get(checkpoint.sessionId);
84
+ const run = session?.runs.find((entry) => entry.turnId === checkpoint.turnId);
85
+ if (!session || !run || !TERMINAL_SESSION_RUN_STATUSES.includes(run.status)) {
86
+ throw new Error("Checkpoint requires an existing terminal conversation turn.");
87
+ }
88
+ return session;
89
+ };
90
+ const proposal = (operation) => {
91
+ if (new Set(operation.checkpoint.files.map((file) => file.path)).size !== operation.checkpoint.files.length || operation.checkpoint.files.some((file) => digest(file.content) !== file.digest))
92
+ throw new Error("Checkpoint content identity is corrupt.");
93
+ const changes = operation.checkpoint.files.map((file) => ({ path: file.path, expectedDigest: operation.expected[file.path], content: file.content }));
94
+ const result = createEditProposal({ changes });
95
+ if (result.proposalId !== operation.proposalId)
96
+ throw new Error("Restore proposal identity is corrupt.");
97
+ return { proposalId: result.proposalId, changes };
98
+ };
99
+ const getOperation = (id) => workspaceRestoreOperationSchema.parse(load("restore", id));
100
+ const list = (kind) => db.query("SELECT body FROM zhivex_workspace_checkpoints WHERE workspace_key=? AND scope_key=? AND kind=? ORDER BY id").all(sessions.workspaceKey, sessions.scopeKey, kind).map((row) => JSON.parse(row.body));
101
+ const summary = (checkpoint) => ({
102
+ id: checkpoint.id,
103
+ sessionId: checkpoint.sessionId,
104
+ turnId: checkpoint.turnId,
105
+ files: checkpoint.files.map((file) => ({
106
+ path: file.path,
107
+ digest: file.digest,
108
+ bytes: Buffer.byteLength(file.content),
109
+ mode: file.mode
110
+ }))
111
+ });
112
+ const inventory = () => db.query("SELECT id,kind,body FROM zhivex_workspace_checkpoints WHERE workspace_key=? AND scope_key=? ORDER BY id").all(sessions.workspaceKey, sessions.scopeKey).map((row) => {
113
+ const value = row.kind === "checkpoint" ? workspaceCheckpointSchema.parse(JSON.parse(row.body)) : row.kind === "restore" ? workspaceRestoreOperationSchema.parse(JSON.parse(row.body)) : undefined;
114
+ if (!value || value.id !== row.id)
115
+ throw new Error("Invalid checkpoint inventory.");
116
+ return { ...row, value };
117
+ });
118
+ const retentionPlan = (input) => {
119
+ const selection = retentionSelection.parse(input);
120
+ const rows = inventory();
121
+ const selected = new Set([...selection.checkpointIds, ...selection.completedRestoreIds]);
122
+ for (const id of selection.checkpointIds) {
123
+ if (!rows.some((row) => row.id === id && row.kind === "checkpoint"))
124
+ throw new Error("Unknown checkpoint selection in this scope.");
125
+ }
126
+ for (const id of selection.completedRestoreIds) {
127
+ const row = rows.find((row2) => row2.id === id && row2.kind === "restore");
128
+ if (!row || !("stage" in row.value) || row.value.stage !== "completed")
129
+ throw new Error("Retention cannot remove pending or unknown restore evidence.");
130
+ }
131
+ for (const row of rows) {
132
+ if ("checkpoint" in row.value && selection.checkpointIds.includes(row.value.checkpoint.id) && !selected.has(row.id)) {
133
+ throw new Error("Checkpoint is still referenced by a retained restore operation.");
134
+ }
135
+ }
136
+ const records = rows.filter((row) => selected.has(row.id)).map((row) => ({
137
+ id: row.id,
138
+ kind: row.kind,
139
+ sessionId: "checkpoint" in row.value ? row.value.checkpoint.sessionId : row.value.sessionId,
140
+ bytes: Buffer.byteLength(row.body)
141
+ }));
142
+ const planId = digest(JSON.stringify({
143
+ workspace: sessions.workspaceKey,
144
+ scope: sessions.scopeKey,
145
+ selection: [...selected].sort(),
146
+ inventory: rows.map((row) => [row.id, row.kind, digest(row.body)])
147
+ }));
148
+ return {
149
+ planId,
150
+ selection: { checkpointIds: [...selection.checkpointIds].sort(), completedRestoreIds: [...selection.completedRestoreIds].sort() },
151
+ records,
152
+ recordsBefore: rows.length,
153
+ recordsAfter: rows.length - records.length,
154
+ bytesRemoved: records.reduce((sum, row) => sum + row.bytes, 0),
155
+ workspaceFilesChanged: false,
156
+ conversationsChanged: false
157
+ };
158
+ };
159
+ return {
160
+ close: () => db.close(),
161
+ getCheckpoint: (id) => workspaceCheckpointSchema.parse(load("checkpoint", id)),
162
+ getOperation,
163
+ storageStatus: () => {
164
+ const rows = inventory();
165
+ const operations = rows.filter((row) => ("stage" in row.value));
166
+ const referenced = new Set(operations.map((row) => row.value.checkpoint.id));
167
+ return {
168
+ policy: "explicit-reviewed-removal",
169
+ automaticEviction: false,
170
+ maxRecords: 100,
171
+ maxRecordBytes: 2 * 1024 * 1024,
172
+ maxFilesPerCheckpoint: 20,
173
+ maxFileBytes: 65536,
174
+ records: rows.length,
175
+ availableRecords: Math.max(0, 100 - rows.length),
176
+ serializedBytes: rows.reduce((sum, row) => sum + Buffer.byteLength(row.body), 0),
177
+ pendingRestores: operations.filter((row) => row.value.stage !== "completed").length,
178
+ checkpoints: rows.filter((row) => row.kind === "checkpoint").map((row) => ({ id: row.id, referenced: referenced.has(row.id) }))
179
+ };
180
+ },
181
+ prepareRetention: retentionPlan,
182
+ applyRetention: async (selection, reviewedPlanId) => withWorkspaceMutation(workspace.root, async () => {
183
+ db.exec("BEGIN IMMEDIATE");
184
+ try {
185
+ const plan = retentionPlan(selection);
186
+ if (plan.planId !== reviewedPlanId)
187
+ throw new Error("Retention inventory changed or was not reviewed.");
188
+ for (const row of plan.records)
189
+ db.query("DELETE FROM zhivex_workspace_checkpoints WHERE id=? AND workspace_key=? AND scope_key=? AND kind=?").run(row.id, sessions.workspaceKey, sessions.scopeKey, row.kind);
190
+ db.exec("COMMIT");
191
+ return plan;
192
+ } catch (error) {
193
+ db.exec("ROLLBACK");
194
+ throw error;
195
+ }
196
+ }),
197
+ listCheckpoints: (sessionId) => list("checkpoint").map((value) => workspaceCheckpointSchema.parse(value)).filter((checkpoint) => checkpoint.sessionId === sessionId).map(summary),
198
+ listRestores: (sessionId) => list("restore").map((value) => workspaceRestoreOperationSchema.parse(value)).filter((operation) => operation.checkpoint.sessionId === sessionId).map((operation) => ({
199
+ id: operation.id,
200
+ checkpointId: operation.checkpoint.id,
201
+ proposalId: operation.proposalId,
202
+ stage: operation.stage,
203
+ ...operation.forkSessionId ? { forkSessionId: operation.forkSessionId } : {}
204
+ })),
205
+ inspectCheckpoint: async (id) => {
206
+ const checkpoint = workspaceCheckpointSchema.parse(load("checkpoint", id));
207
+ const files = await Promise.all(checkpoint.files.map(async (file) => {
208
+ try {
209
+ const current = await read(file.path);
210
+ if (current.mode !== file.mode)
211
+ return { path: file.path, status: "mode-conflict" };
212
+ return {
213
+ path: file.path,
214
+ status: "available",
215
+ before: current.content,
216
+ after: file.content,
217
+ expectedDigest: current.digest,
218
+ afterDigest: file.digest,
219
+ mode: file.mode
220
+ };
221
+ } catch {
222
+ return { path: file.path, status: "unavailable" };
223
+ }
224
+ }));
225
+ return {
226
+ checkpoint: summary(checkpoint),
227
+ files,
228
+ coverage: {
229
+ existingTextOnly: true,
230
+ maxFiles: 20,
231
+ maxFileBytes: 65536,
232
+ creation: false,
233
+ deletion: false,
234
+ binary: false,
235
+ modeRollback: false,
236
+ fullWorkspaceSnapshot: false
237
+ }
238
+ };
239
+ },
240
+ previewRestore: async (id) => {
241
+ const operation = getOperation(id);
242
+ for (const file of operation.checkpoint.files) {
243
+ if ((await read(file.path)).mode !== file.mode)
244
+ throw new Error(`Restore mode conflict: ${file.path}.`);
245
+ }
246
+ return workspace.previewPatch(proposal(operation));
247
+ },
248
+ capture: async (input) => withWorkspaceMutation(workspace.root, async () => {
249
+ await validateTurn(input);
250
+ if (input.paths.length === 0 || input.paths.length > 20 || new Set(input.paths).size !== input.paths.length)
251
+ throw new Error("Capture requires 1 to 20 unique existing text files.");
252
+ const files = [];
253
+ for (const target of input.paths)
254
+ files.push(await read(target));
255
+ const checkpoint = workspaceCheckpointSchema.parse({ id: randomUUID(), sessionId: input.sessionId, turnId: input.turnId, files });
256
+ save("checkpoint", checkpoint);
257
+ return checkpoint;
258
+ }),
259
+ prepareRestore: async (checkpointId, expectedCurrent) => withWorkspaceMutation(workspace.root, async () => {
260
+ const checkpoint = workspaceCheckpointSchema.parse(load("checkpoint", checkpointId));
261
+ await validateTurn(checkpoint);
262
+ if (Object.keys(expectedCurrent).length !== checkpoint.files.length)
263
+ throw new Error("Provide exact post-agent digests for every captured file.");
264
+ for (const file of checkpoint.files) {
265
+ const current = await read(file.path);
266
+ if (current.digest !== expectedCurrent[file.path] || current.mode !== file.mode)
267
+ throw new Error(`Restore conflicts with current content or mode: ${file.path}.`);
268
+ }
269
+ const changes = checkpoint.files.map((file) => ({ path: file.path, expectedDigest: expectedCurrent[file.path], content: file.content }));
270
+ const preview = createEditProposal({ changes });
271
+ const operation = workspaceRestoreOperationSchema.parse({
272
+ id: randomUUID(),
273
+ checkpoint,
274
+ proposalId: preview.proposalId,
275
+ expected: expectedCurrent,
276
+ stage: "prepared"
277
+ });
278
+ save("restore", operation);
279
+ return { operation, proposal: { ...preview, changes } };
280
+ }),
281
+ applyRestore: async (id, reviewedProposalId) => withWorkspaceMutation(workspace.root, async () => {
282
+ const operation = getOperation(id);
283
+ if (operation.proposalId !== reviewedProposalId)
284
+ throw new Error("Restore requires the reviewed proposal identity.");
285
+ const patch = proposal(operation);
286
+ if (operation.stage === "completed")
287
+ return operation;
288
+ if (operation.stage === "forking")
289
+ throw new Error(`Fork outcome uncertain. Inspect child sessions titled restore:${operation.id} and call recoverFork with the exact child ID; no files changed.`);
290
+ const current = [];
291
+ for (const file of operation.checkpoint.files) {
292
+ const found = await read(file.path);
293
+ if (found.mode !== file.mode)
294
+ throw new Error(`Restore mode conflict: ${file.path}.`);
295
+ current.push(found);
296
+ }
297
+ const beforeApply = current.every((file) => file.digest === operation.expected[file.path]);
298
+ const afterApply = current.every((file) => file.digest === operation.checkpoint.files.find((entry) => entry.path === file.path)?.digest);
299
+ if (["applying", "applied"].includes(operation.stage) && afterApply) {
300
+ operation.stage = "completed";
301
+ save("restore", operation);
302
+ return operation;
303
+ }
304
+ if (!beforeApply || operation.stage === "applied")
305
+ throw new Error("Restore conflicts with subsequent edits or a partially applied filesystem operation; manual recovery required.");
306
+ if (operation.stage === "prepared") {
307
+ const source = await validateTurn(operation.checkpoint);
308
+ operation.stage = "forking";
309
+ save("restore", operation);
310
+ const fork = await sessions.fork(source.sessionId, {
311
+ atTurnId: operation.checkpoint.turnId,
312
+ expectedRevision: source.revision,
313
+ title: `restore:${operation.id}`
314
+ });
315
+ operation.forkSessionId = fork.sessionId;
316
+ operation.stage = "forked";
317
+ save("restore", operation);
318
+ }
319
+ if (!operation.forkSessionId)
320
+ throw new Error("Restore has no recorded conversation fork.");
321
+ operation.stage = "applying";
322
+ save("restore", operation);
323
+ await workspace.applyPatchWithModes(patch, new Map(operation.checkpoint.files.map((file) => [file.path, { beforeMode: file.mode, afterMode: file.mode }])));
324
+ operation.stage = "applied";
325
+ save("restore", operation);
326
+ operation.stage = "completed";
327
+ save("restore", operation);
328
+ return operation;
329
+ }),
330
+ recoverFork: async (id, forkSessionId) => withWorkspaceMutation(workspace.root, async () => {
331
+ const operation = getOperation(id);
332
+ const fork = await sessions.get(forkSessionId);
333
+ if (operation.stage !== "forking" || !fork || fork.parentSessionId !== operation.checkpoint.sessionId || fork.forkedFromTurnId !== operation.checkpoint.turnId || fork.title !== `restore:${operation.id}`)
334
+ throw new Error("Fork does not match this interrupted restore.");
335
+ operation.forkSessionId = forkSessionId;
336
+ operation.stage = "forked";
337
+ save("restore", operation);
338
+ return operation;
339
+ })
340
+ };
341
+ };
342
+
343
+ export { workspaceCheckpointSchema, workspaceRestoreOperationSchema, openWorkspaceCheckpointStore };