@zhivex-ai/harness 1.3.0-rc.3 → 1.3.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.
- package/CHANGELOG.md +53 -0
- package/README.md +12 -10
- package/SECURITY.md +1 -1
- package/SUPPORT.md +4 -4
- package/contracts/engine-api.json +334 -0
- package/contracts/public-api.json +267 -1
- package/contracts/stable-api-signatures.json +43 -43
- package/dist/acp-cli.js +54 -18
- package/dist/approvals/approval-history.d.ts +22 -0
- package/dist/approvals/explicit-review.d.ts +24 -0
- package/dist/approvals/host-policy-identity.d.ts +3 -0
- package/dist/cli/arguments.d.ts +6 -2
- package/dist/cli/checkpoints.d.ts +230 -0
- package/dist/cli/cli-profiles.d.ts +4 -2
- package/dist/cli/governance-report.d.ts +5 -0
- package/dist/cli/policy-decisions.d.ts +9 -0
- package/dist/cli/policy.d.ts +5 -0
- package/dist/cli/presentation.d.ts +3 -1
- package/dist/cli/terminal/terminal-ui.d.ts +4 -0
- package/dist/{cli-entry-xmf48p6a.js → cli-entry-1gfg3brj.js} +14921 -10692
- package/dist/cli-entry-bx3vjw7h.js +656 -0
- package/dist/{index-fvca1xea.js → cli-entry-d5788aay.js} +339 -58
- package/dist/{cli-qgwvtk0w.js → cli-etsmtst3.js} +918 -2222
- package/dist/cli.js +3 -5
- package/dist/client/acp-mcp-admission.d.ts +32 -0
- package/dist/client/acp-mcp-host.d.ts +19 -0
- package/dist/client/acp-mcp-session.d.ts +14 -0
- package/dist/client/acp-stdio.d.ts +2 -0
- package/dist/client/acp.d.ts +4 -1
- package/dist/client/adapter.d.ts +13 -1
- package/dist/client/json-contracts.d.ts +107 -4
- package/dist/client/local-service.d.ts +2 -0
- package/dist/client/protocol.d.ts +127 -1
- package/dist/client/service-events.d.ts +4 -0
- package/dist/compat/cli-options.d.ts +13 -1
- package/dist/compat/code-support.js +23 -87
- package/dist/{config-q32jm1e6.js → config-c002f06f.js} +1 -1
- package/dist/{config-6gtpxvjv.js → config-j3jga5wh.js} +3 -1
- package/dist/context/adaptive-compaction.d.ts +1 -0
- package/dist/context/compaction.d.ts +1 -1
- package/dist/context/request-measurements.d.ts +17 -0
- package/dist/context/request-projection.d.ts +10 -0
- package/dist/context/task-memory.d.ts +1 -0
- package/dist/engine/acp.d.ts +2 -0
- package/dist/engine/acp.js +197 -1
- package/dist/engine/client.js +11 -4
- package/dist/engine/desktop-providers-v1.d.ts +4 -0
- package/dist/engine/desktop-providers-v1.js +18 -0
- package/dist/engine/desktop-state-v1.d.ts +10 -0
- package/dist/engine/desktop-state-v1.js +168 -0
- package/dist/engine/index.d.ts +12 -0
- package/dist/engine/index.js +571 -71
- package/dist/engine/mcp-stdio-v1.d.ts +11 -0
- package/dist/engine/mcp-stdio-v1.js +108 -0
- package/dist/engine/models.d.ts +3 -0
- package/dist/engine/models.js +894 -0
- package/dist/engine/protocol.d.ts +1 -0
- package/dist/engine/protocol.js +24 -0
- package/dist/engine/service.js +12 -5
- package/dist/execution/acceptance-snapshot.d.ts +2 -0
- package/dist/execution/execution-environment.d.ts +10 -0
- package/dist/execution/mcp-oci-channel.d.ts +11 -0
- package/dist/execution/mcp-oci-server.d.ts +38 -0
- package/dist/execution/mcp-oci-workspace.d.ts +23 -0
- package/dist/execution/mcp-resource-recovery.d.ts +19 -0
- package/dist/execution/mcp-workspace-snapshot.d.ts +20 -0
- package/dist/hostile-repository-demo.js +3169 -1163
- package/dist/{index-f4sb0e2q.js → index-2jrdcj6b.js} +17 -2
- package/dist/{index-77h9qr5h.js → index-3ah1f369.js} +40 -10
- package/dist/{index-pgqz45fj.js → index-42gp2hy8.js} +197 -1091
- package/dist/{index-41fkzt91.js → index-4jw3tvy8.js} +10 -2
- package/dist/index-6m08wvab.js +1768 -0
- package/dist/index-8705zxpr.js +145 -0
- package/dist/{index-d0wrghff.js → index-8vm2m34s.js} +225 -1416
- package/dist/index-8zzhggxv.js +477 -0
- package/dist/index-a97bezbz.js +1073 -0
- package/dist/index-azmwjp95.js +1004 -0
- package/dist/{cli-entry-0mmeag20.js → index-gq9xagx2.js} +122 -836
- package/dist/index-h0mqx2ed.js +1373 -0
- package/dist/index-hnaxfpb5.js +5589 -0
- package/dist/{index-2aymdsqc.js → index-qfpz5y2v.js} +24 -0
- package/dist/{index-kfgt9yqq.js → index-w2sj0wkw.js} +33 -10
- package/dist/index-wg1sd3at.js +80 -0
- package/dist/index-xqxeypt5.js +888 -0
- package/dist/index-yb239ghk.js +961 -0
- package/dist/index-z0qxmkm2.js +3389 -0
- package/dist/index.js +85 -72
- package/dist/integrations/mcp-host-session.d.ts +26 -0
- package/dist/integrations/mcp-stdio-admission.d.ts +54 -0
- package/dist/integrations/mcp-stdio-client.d.ts +44 -0
- package/dist/integrations/mcp-stdio-framing.d.ts +18 -0
- package/dist/integrations/mcp-stdio-launch.d.ts +20 -0
- package/dist/integrations/mcp-stdio-session.d.ts +7 -0
- package/dist/integrations/mcp-stdio-tools.d.ts +12 -0
- package/dist/internal/desktop/catalog.d.ts +2 -0
- package/dist/internal/desktop/persistence.d.ts +2 -3
- package/dist/internal/desktop/providers.d.ts +1 -0
- package/dist/models/catalog.d.ts +2 -0
- package/dist/persistence/governance-context.d.ts +126 -0
- package/dist/persistence/governance-report.d.ts +299 -0
- package/dist/persistence/host-database-backup.d.ts +28 -0
- package/dist/persistence/mcp-execution-journal.d.ts +19 -0
- package/dist/persistence/mcp-reconciliation.d.ts +33 -0
- package/dist/persistence/mcp-resource-journal.d.ts +54 -0
- package/dist/persistence/workspace-checkpoints.d.ts +138 -0
- package/dist/providers/anthropic-continuation.d.ts +5 -0
- package/dist/providers/providers.d.ts +1 -1
- package/dist/providers/vertex-auth.d.ts +27 -0
- package/dist/runtime/delegation-contracts.d.ts +9 -0
- package/dist/runtime/delegation-correction.d.ts +9 -0
- package/dist/runtime/delegation-result.d.ts +73 -0
- package/dist/runtime/error-diagnostics.d.ts +86 -0
- package/dist/runtime/harness.d.ts +11 -1
- package/dist/runtime/policy-decisions.d.ts +66 -0
- package/dist/runtime/policy-inspection.d.ts +52 -0
- package/dist/runtime/provider-diagnostics.d.ts +1 -0
- package/dist/runtime/runtime-instructions.d.ts +8 -0
- package/dist/runtime/task-acceptance-checks.d.ts +103 -0
- package/dist/runtime/task-acceptance-delivery.d.ts +107 -0
- package/dist/runtime/task-acceptance-host.d.ts +20 -0
- package/dist/runtime/task-acceptance-record.d.ts +107 -0
- package/dist/runtime/task-acceptance-validation.d.ts +25 -0
- package/dist/runtime/task-acceptance.d.ts +47 -0
- package/dist/runtime/time-to-safe-fix.d.ts +83 -0
- package/dist/runtime/tool-policy-file.d.ts +23 -0
- package/dist/runtime/tool-policy-paths.d.ts +5 -0
- package/dist/runtime/tool-policy.d.ts +6 -0
- package/dist/service-cli.js +14 -7
- package/dist/{service-client-cli-gs78wrxe.js → service-client-cli-3365rfav.js} +307 -248
- package/dist/verify-historical-migrations.js +3134 -1129
- package/dist/workspace/path-policy.d.ts +4 -0
- package/dist/zhx.js +3 -5
- package/docs/ACP.md +14 -2
- package/docs/ANTHROPIC.md +33 -0
- package/docs/CLI.md +132 -0
- package/docs/CLIENT_PROTOCOL.md +45 -0
- package/docs/CODE_RELEASE.md +35 -18
- package/docs/ENGINE_API.md +216 -7
- package/docs/EXTENSIBILITY.md +46 -3
- package/docs/FIRST_USE.md +6 -6
- package/docs/LOCAL_SERVICE.md +13 -0
- package/docs/MCP_STDIO.md +20 -0
- package/docs/README.md +1 -0
- package/docs/STABILITY.md +4 -2
- package/docs/SUPPORT_MATRIX.md +1 -1
- package/docs/TOOL_POLICY.md +81 -0
- package/docs/VERTEX.md +50 -0
- package/docs/adr/0002-isolated-mcp-stdio.md +135 -0
- package/docs/fixtures/mcp-stdio-contract-v1.json +189 -0
- package/docs/support-matrix.json +2 -0
- package/evaluations/representative-assembly-matrix.json +46 -1
- package/evaluations/task-acceptance-routes.json +7 -0
- package/examples/README.md +3 -3
- package/package.json +43 -12
- package/dist/cli-entry-64pdp3k5.js +0 -17
- package/dist/cli-entry-7h9savv5.js +0 -375
- package/dist/index-5wjwe91x.js +0 -9434
|
@@ -0,0 +1,1373 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MODEL_BUDGET_KEY,
|
|
3
|
+
TERMINAL_SESSION_RUN_STATUSES,
|
|
4
|
+
USAGE_LEDGER_KEY,
|
|
5
|
+
estimateRequestTokens,
|
|
6
|
+
harnessToolExecution,
|
|
7
|
+
inspectRuntimeDiagnostics,
|
|
8
|
+
inspectRuntimeManifest,
|
|
9
|
+
inspectUsageLedger,
|
|
10
|
+
protectStateFromGit,
|
|
11
|
+
taskSources,
|
|
12
|
+
validateStateDirectory,
|
|
13
|
+
verifierSchema
|
|
14
|
+
} from "./index-42gp2hy8.js";
|
|
15
|
+
import {
|
|
16
|
+
harnessExecutionSession,
|
|
17
|
+
withWorkspaceMutation
|
|
18
|
+
} from "./index-z0qxmkm2.js";
|
|
19
|
+
import {
|
|
20
|
+
SqliteDatabase,
|
|
21
|
+
createEditProposal,
|
|
22
|
+
fileDigestSchema,
|
|
23
|
+
readRegularFileNoFollow,
|
|
24
|
+
statRegularFileNoFollow,
|
|
25
|
+
workspaceFilePathSchema
|
|
26
|
+
} from "./index-yb239ghk.js";
|
|
27
|
+
import {
|
|
28
|
+
HarnessStateConflictError,
|
|
29
|
+
HarnessWorkspaceError,
|
|
30
|
+
defaultHarnessNamespace
|
|
31
|
+
} from "./index-gq9xagx2.js";
|
|
32
|
+
|
|
33
|
+
// src/persistence/workspace-checkpoints.ts
|
|
34
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
35
|
+
import path from "node:path";
|
|
36
|
+
import { z } from "zod";
|
|
37
|
+
var fileSchema = z.strictObject({
|
|
38
|
+
path: workspaceFilePathSchema,
|
|
39
|
+
digest: fileDigestSchema,
|
|
40
|
+
content: z.string().max(65536),
|
|
41
|
+
mode: z.number().int().min(0).max(511)
|
|
42
|
+
}).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.");
|
|
43
|
+
var workspaceCheckpointSchema = z.strictObject({
|
|
44
|
+
id: z.string().uuid(),
|
|
45
|
+
sessionId: z.string(),
|
|
46
|
+
turnId: z.string(),
|
|
47
|
+
files: z.array(fileSchema).min(1).max(20)
|
|
48
|
+
});
|
|
49
|
+
var workspaceRestoreOperationSchema = z.strictObject({
|
|
50
|
+
id: z.string().uuid(),
|
|
51
|
+
checkpoint: workspaceCheckpointSchema,
|
|
52
|
+
proposalId: fileDigestSchema,
|
|
53
|
+
expected: z.record(z.string(), fileDigestSchema),
|
|
54
|
+
stage: z.enum(["prepared", "forking", "forked", "applying", "applied", "completed"]),
|
|
55
|
+
forkSessionId: z.string().optional()
|
|
56
|
+
});
|
|
57
|
+
var digest = (content) => `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
58
|
+
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.");
|
|
59
|
+
var openWorkspaceCheckpointStore = async (workspace, sessions) => {
|
|
60
|
+
const key = createHash("sha256").update("workspace\x00").update(workspace.root).digest("hex");
|
|
61
|
+
if (key !== sessions.workspaceKey)
|
|
62
|
+
throw new Error("Checkpoint session store belongs to a different workspace.");
|
|
63
|
+
const before = await statRegularFileNoFollow(sessions.databasePath, { label: "Checkpoint database", requireSingleLink: true });
|
|
64
|
+
if (before.uid !== process.getuid?.() || before.mode & 63)
|
|
65
|
+
throw new Error("Checkpoint database must be private.");
|
|
66
|
+
const db = new SqliteDatabase(sessions.databasePath, { create: false });
|
|
67
|
+
const after = await statRegularFileNoFollow(sessions.databasePath, { label: "Checkpoint database", requireSingleLink: true });
|
|
68
|
+
if (before.dev !== after.dev || before.ino !== after.ino) {
|
|
69
|
+
db.close();
|
|
70
|
+
throw new Error("Checkpoint database changed.");
|
|
71
|
+
}
|
|
72
|
+
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)");
|
|
73
|
+
const save = (kind, value) => {
|
|
74
|
+
const existing = db.query("SELECT id FROM zhivex_workspace_checkpoints WHERE id=?").get(value.id);
|
|
75
|
+
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;
|
|
76
|
+
if (!existing && count >= 100)
|
|
77
|
+
throw new Error("Checkpoint scope storage limit reached (100 records).");
|
|
78
|
+
const body = JSON.stringify(value);
|
|
79
|
+
if (Buffer.byteLength(body) > 2 * 1024 * 1024)
|
|
80
|
+
throw new Error("Checkpoint exceeds storage limit.");
|
|
81
|
+
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);
|
|
82
|
+
};
|
|
83
|
+
const load = (kind, id) => {
|
|
84
|
+
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);
|
|
85
|
+
if (!row)
|
|
86
|
+
throw new Error("Unknown checkpoint or restore operation in this scope.");
|
|
87
|
+
return JSON.parse(row.body);
|
|
88
|
+
};
|
|
89
|
+
const read = async (target) => {
|
|
90
|
+
const authorized = await workspace.readFile(target, 1, 1);
|
|
91
|
+
const { contents, stat } = await readRegularFileNoFollow(path.join(workspace.root, target), {
|
|
92
|
+
label: "Checkpoint target",
|
|
93
|
+
maxBytes: 65536,
|
|
94
|
+
requireSingleLink: true
|
|
95
|
+
});
|
|
96
|
+
const content = new TextDecoder("utf-8", { fatal: true }).decode(contents);
|
|
97
|
+
if (contents.includes(0) || digest(contents) !== authorized.digest)
|
|
98
|
+
throw new Error("Checkpoint file changed while reading.");
|
|
99
|
+
return fileSchema.parse({ path: target, digest: digest(contents), content, mode: stat.mode & 511 });
|
|
100
|
+
};
|
|
101
|
+
const validateTurn = async (checkpoint) => {
|
|
102
|
+
const session = await sessions.get(checkpoint.sessionId);
|
|
103
|
+
const run = session?.runs.find((entry) => entry.turnId === checkpoint.turnId);
|
|
104
|
+
if (!session || !run || !TERMINAL_SESSION_RUN_STATUSES.includes(run.status)) {
|
|
105
|
+
throw new Error("Checkpoint requires an existing terminal conversation turn.");
|
|
106
|
+
}
|
|
107
|
+
return session;
|
|
108
|
+
};
|
|
109
|
+
const proposal = (operation) => {
|
|
110
|
+
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))
|
|
111
|
+
throw new Error("Checkpoint content identity is corrupt.");
|
|
112
|
+
const changes = operation.checkpoint.files.map((file) => ({ path: file.path, expectedDigest: operation.expected[file.path], content: file.content }));
|
|
113
|
+
const result = createEditProposal({ changes });
|
|
114
|
+
if (result.proposalId !== operation.proposalId)
|
|
115
|
+
throw new Error("Restore proposal identity is corrupt.");
|
|
116
|
+
return { proposalId: result.proposalId, changes };
|
|
117
|
+
};
|
|
118
|
+
const getOperation = (id) => workspaceRestoreOperationSchema.parse(load("restore", id));
|
|
119
|
+
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));
|
|
120
|
+
const summary = (checkpoint) => ({
|
|
121
|
+
id: checkpoint.id,
|
|
122
|
+
sessionId: checkpoint.sessionId,
|
|
123
|
+
turnId: checkpoint.turnId,
|
|
124
|
+
files: checkpoint.files.map((file) => ({
|
|
125
|
+
path: file.path,
|
|
126
|
+
digest: file.digest,
|
|
127
|
+
bytes: Buffer.byteLength(file.content),
|
|
128
|
+
mode: file.mode
|
|
129
|
+
}))
|
|
130
|
+
});
|
|
131
|
+
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) => {
|
|
132
|
+
const value = row.kind === "checkpoint" ? workspaceCheckpointSchema.parse(JSON.parse(row.body)) : row.kind === "restore" ? workspaceRestoreOperationSchema.parse(JSON.parse(row.body)) : undefined;
|
|
133
|
+
if (!value || value.id !== row.id)
|
|
134
|
+
throw new Error("Invalid checkpoint inventory.");
|
|
135
|
+
return { ...row, value };
|
|
136
|
+
});
|
|
137
|
+
const retentionPlan = (input) => {
|
|
138
|
+
const selection = retentionSelection.parse(input);
|
|
139
|
+
const rows = inventory();
|
|
140
|
+
const selected = new Set([...selection.checkpointIds, ...selection.completedRestoreIds]);
|
|
141
|
+
for (const id of selection.checkpointIds) {
|
|
142
|
+
if (!rows.some((row) => row.id === id && row.kind === "checkpoint"))
|
|
143
|
+
throw new Error("Unknown checkpoint selection in this scope.");
|
|
144
|
+
}
|
|
145
|
+
for (const id of selection.completedRestoreIds) {
|
|
146
|
+
const row = rows.find((row2) => row2.id === id && row2.kind === "restore");
|
|
147
|
+
if (!row || !("stage" in row.value) || row.value.stage !== "completed")
|
|
148
|
+
throw new Error("Retention cannot remove pending or unknown restore evidence.");
|
|
149
|
+
}
|
|
150
|
+
for (const row of rows) {
|
|
151
|
+
if ("checkpoint" in row.value && selection.checkpointIds.includes(row.value.checkpoint.id) && !selected.has(row.id)) {
|
|
152
|
+
throw new Error("Checkpoint is still referenced by a retained restore operation.");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const records = rows.filter((row) => selected.has(row.id)).map((row) => ({
|
|
156
|
+
id: row.id,
|
|
157
|
+
kind: row.kind,
|
|
158
|
+
sessionId: "checkpoint" in row.value ? row.value.checkpoint.sessionId : row.value.sessionId,
|
|
159
|
+
bytes: Buffer.byteLength(row.body)
|
|
160
|
+
}));
|
|
161
|
+
const planId = digest(JSON.stringify({
|
|
162
|
+
workspace: sessions.workspaceKey,
|
|
163
|
+
scope: sessions.scopeKey,
|
|
164
|
+
selection: [...selected].sort(),
|
|
165
|
+
inventory: rows.map((row) => [row.id, row.kind, digest(row.body)])
|
|
166
|
+
}));
|
|
167
|
+
return {
|
|
168
|
+
planId,
|
|
169
|
+
selection: { checkpointIds: [...selection.checkpointIds].sort(), completedRestoreIds: [...selection.completedRestoreIds].sort() },
|
|
170
|
+
records,
|
|
171
|
+
recordsBefore: rows.length,
|
|
172
|
+
recordsAfter: rows.length - records.length,
|
|
173
|
+
bytesRemoved: records.reduce((sum, row) => sum + row.bytes, 0),
|
|
174
|
+
workspaceFilesChanged: false,
|
|
175
|
+
conversationsChanged: false
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
close: () => db.close(),
|
|
180
|
+
getCheckpoint: (id) => workspaceCheckpointSchema.parse(load("checkpoint", id)),
|
|
181
|
+
getOperation,
|
|
182
|
+
storageStatus: () => {
|
|
183
|
+
const rows = inventory();
|
|
184
|
+
const operations = rows.filter((row) => ("stage" in row.value));
|
|
185
|
+
const referenced = new Set(operations.map((row) => row.value.checkpoint.id));
|
|
186
|
+
return {
|
|
187
|
+
policy: "explicit-reviewed-removal",
|
|
188
|
+
automaticEviction: false,
|
|
189
|
+
maxRecords: 100,
|
|
190
|
+
maxRecordBytes: 2 * 1024 * 1024,
|
|
191
|
+
maxFilesPerCheckpoint: 20,
|
|
192
|
+
maxFileBytes: 65536,
|
|
193
|
+
records: rows.length,
|
|
194
|
+
availableRecords: Math.max(0, 100 - rows.length),
|
|
195
|
+
serializedBytes: rows.reduce((sum, row) => sum + Buffer.byteLength(row.body), 0),
|
|
196
|
+
pendingRestores: operations.filter((row) => row.value.stage !== "completed").length,
|
|
197
|
+
checkpoints: rows.filter((row) => row.kind === "checkpoint").map((row) => ({ id: row.id, referenced: referenced.has(row.id) }))
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
prepareRetention: retentionPlan,
|
|
201
|
+
applyRetention: async (selection, reviewedPlanId) => withWorkspaceMutation(workspace.root, async () => {
|
|
202
|
+
db.exec("BEGIN IMMEDIATE");
|
|
203
|
+
try {
|
|
204
|
+
const plan = retentionPlan(selection);
|
|
205
|
+
if (plan.planId !== reviewedPlanId)
|
|
206
|
+
throw new Error("Retention inventory changed or was not reviewed.");
|
|
207
|
+
for (const row of plan.records)
|
|
208
|
+
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);
|
|
209
|
+
db.exec("COMMIT");
|
|
210
|
+
return plan;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
db.exec("ROLLBACK");
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}),
|
|
216
|
+
listCheckpoints: (sessionId) => list("checkpoint").map((value) => workspaceCheckpointSchema.parse(value)).filter((checkpoint) => checkpoint.sessionId === sessionId).map(summary),
|
|
217
|
+
listRestores: (sessionId) => list("restore").map((value) => workspaceRestoreOperationSchema.parse(value)).filter((operation) => operation.checkpoint.sessionId === sessionId).map((operation) => ({
|
|
218
|
+
id: operation.id,
|
|
219
|
+
checkpointId: operation.checkpoint.id,
|
|
220
|
+
proposalId: operation.proposalId,
|
|
221
|
+
stage: operation.stage,
|
|
222
|
+
...operation.forkSessionId ? { forkSessionId: operation.forkSessionId } : {}
|
|
223
|
+
})),
|
|
224
|
+
inspectCheckpoint: async (id) => {
|
|
225
|
+
const checkpoint = workspaceCheckpointSchema.parse(load("checkpoint", id));
|
|
226
|
+
const files = await Promise.all(checkpoint.files.map(async (file) => {
|
|
227
|
+
try {
|
|
228
|
+
const current = await read(file.path);
|
|
229
|
+
if (current.mode !== file.mode)
|
|
230
|
+
return { path: file.path, status: "mode-conflict" };
|
|
231
|
+
return {
|
|
232
|
+
path: file.path,
|
|
233
|
+
status: "available",
|
|
234
|
+
before: current.content,
|
|
235
|
+
after: file.content,
|
|
236
|
+
expectedDigest: current.digest,
|
|
237
|
+
afterDigest: file.digest,
|
|
238
|
+
mode: file.mode
|
|
239
|
+
};
|
|
240
|
+
} catch {
|
|
241
|
+
return { path: file.path, status: "unavailable" };
|
|
242
|
+
}
|
|
243
|
+
}));
|
|
244
|
+
return {
|
|
245
|
+
checkpoint: summary(checkpoint),
|
|
246
|
+
files,
|
|
247
|
+
coverage: {
|
|
248
|
+
existingTextOnly: true,
|
|
249
|
+
maxFiles: 20,
|
|
250
|
+
maxFileBytes: 65536,
|
|
251
|
+
creation: false,
|
|
252
|
+
deletion: false,
|
|
253
|
+
binary: false,
|
|
254
|
+
modeRollback: false,
|
|
255
|
+
fullWorkspaceSnapshot: false
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
previewRestore: async (id) => {
|
|
260
|
+
const operation = getOperation(id);
|
|
261
|
+
for (const file of operation.checkpoint.files) {
|
|
262
|
+
if ((await read(file.path)).mode !== file.mode)
|
|
263
|
+
throw new Error(`Restore mode conflict: ${file.path}.`);
|
|
264
|
+
}
|
|
265
|
+
return workspace.previewPatch(proposal(operation));
|
|
266
|
+
},
|
|
267
|
+
capture: async (input) => withWorkspaceMutation(workspace.root, async () => {
|
|
268
|
+
await validateTurn(input);
|
|
269
|
+
if (input.paths.length === 0 || input.paths.length > 20 || new Set(input.paths).size !== input.paths.length)
|
|
270
|
+
throw new Error("Capture requires 1 to 20 unique existing text files.");
|
|
271
|
+
const files = [];
|
|
272
|
+
for (const target of input.paths)
|
|
273
|
+
files.push(await read(target));
|
|
274
|
+
const checkpoint = workspaceCheckpointSchema.parse({ id: randomUUID(), sessionId: input.sessionId, turnId: input.turnId, files });
|
|
275
|
+
save("checkpoint", checkpoint);
|
|
276
|
+
return checkpoint;
|
|
277
|
+
}),
|
|
278
|
+
prepareRestore: async (checkpointId, expectedCurrent) => withWorkspaceMutation(workspace.root, async () => {
|
|
279
|
+
const checkpoint = workspaceCheckpointSchema.parse(load("checkpoint", checkpointId));
|
|
280
|
+
await validateTurn(checkpoint);
|
|
281
|
+
if (Object.keys(expectedCurrent).length !== checkpoint.files.length)
|
|
282
|
+
throw new Error("Provide exact post-agent digests for every captured file.");
|
|
283
|
+
for (const file of checkpoint.files) {
|
|
284
|
+
const current = await read(file.path);
|
|
285
|
+
if (current.digest !== expectedCurrent[file.path] || current.mode !== file.mode)
|
|
286
|
+
throw new Error(`Restore conflicts with current content or mode: ${file.path}.`);
|
|
287
|
+
}
|
|
288
|
+
const changes = checkpoint.files.map((file) => ({ path: file.path, expectedDigest: expectedCurrent[file.path], content: file.content }));
|
|
289
|
+
const preview = createEditProposal({ changes });
|
|
290
|
+
const operation = workspaceRestoreOperationSchema.parse({
|
|
291
|
+
id: randomUUID(),
|
|
292
|
+
checkpoint,
|
|
293
|
+
proposalId: preview.proposalId,
|
|
294
|
+
expected: expectedCurrent,
|
|
295
|
+
stage: "prepared"
|
|
296
|
+
});
|
|
297
|
+
save("restore", operation);
|
|
298
|
+
return { operation, proposal: { ...preview, changes } };
|
|
299
|
+
}),
|
|
300
|
+
applyRestore: async (id, reviewedProposalId) => withWorkspaceMutation(workspace.root, async () => {
|
|
301
|
+
const operation = getOperation(id);
|
|
302
|
+
if (operation.proposalId !== reviewedProposalId)
|
|
303
|
+
throw new Error("Restore requires the reviewed proposal identity.");
|
|
304
|
+
const patch = proposal(operation);
|
|
305
|
+
if (operation.stage === "completed")
|
|
306
|
+
return operation;
|
|
307
|
+
if (operation.stage === "forking")
|
|
308
|
+
throw new Error(`Fork outcome uncertain. Inspect child sessions titled restore:${operation.id} and call recoverFork with the exact child ID; no files changed.`);
|
|
309
|
+
const current = [];
|
|
310
|
+
for (const file of operation.checkpoint.files) {
|
|
311
|
+
const found = await read(file.path);
|
|
312
|
+
if (found.mode !== file.mode)
|
|
313
|
+
throw new Error(`Restore mode conflict: ${file.path}.`);
|
|
314
|
+
current.push(found);
|
|
315
|
+
}
|
|
316
|
+
const beforeApply = current.every((file) => file.digest === operation.expected[file.path]);
|
|
317
|
+
const afterApply = current.every((file) => file.digest === operation.checkpoint.files.find((entry) => entry.path === file.path)?.digest);
|
|
318
|
+
if (["applying", "applied"].includes(operation.stage) && afterApply) {
|
|
319
|
+
operation.stage = "completed";
|
|
320
|
+
save("restore", operation);
|
|
321
|
+
return operation;
|
|
322
|
+
}
|
|
323
|
+
if (!beforeApply || operation.stage === "applied")
|
|
324
|
+
throw new Error("Restore conflicts with subsequent edits or a partially applied filesystem operation; manual recovery required.");
|
|
325
|
+
if (operation.stage === "prepared") {
|
|
326
|
+
const source = await validateTurn(operation.checkpoint);
|
|
327
|
+
operation.stage = "forking";
|
|
328
|
+
save("restore", operation);
|
|
329
|
+
const fork = await sessions.fork(source.sessionId, {
|
|
330
|
+
atTurnId: operation.checkpoint.turnId,
|
|
331
|
+
expectedRevision: source.revision,
|
|
332
|
+
title: `restore:${operation.id}`
|
|
333
|
+
});
|
|
334
|
+
operation.forkSessionId = fork.sessionId;
|
|
335
|
+
operation.stage = "forked";
|
|
336
|
+
save("restore", operation);
|
|
337
|
+
}
|
|
338
|
+
if (!operation.forkSessionId)
|
|
339
|
+
throw new Error("Restore has no recorded conversation fork.");
|
|
340
|
+
operation.stage = "applying";
|
|
341
|
+
save("restore", operation);
|
|
342
|
+
await workspace.applyPatchWithModes(patch, new Map(operation.checkpoint.files.map((file) => [file.path, { beforeMode: file.mode, afterMode: file.mode }])));
|
|
343
|
+
operation.stage = "applied";
|
|
344
|
+
save("restore", operation);
|
|
345
|
+
operation.stage = "completed";
|
|
346
|
+
save("restore", operation);
|
|
347
|
+
return operation;
|
|
348
|
+
}),
|
|
349
|
+
recoverFork: async (id, forkSessionId) => withWorkspaceMutation(workspace.root, async () => {
|
|
350
|
+
const operation = getOperation(id);
|
|
351
|
+
const fork = await sessions.get(forkSessionId);
|
|
352
|
+
if (operation.stage !== "forking" || !fork || fork.parentSessionId !== operation.checkpoint.sessionId || fork.forkedFromTurnId !== operation.checkpoint.turnId || fork.title !== `restore:${operation.id}`)
|
|
353
|
+
throw new Error("Fork does not match this interrupted restore.");
|
|
354
|
+
operation.forkSessionId = forkSessionId;
|
|
355
|
+
operation.stage = "forked";
|
|
356
|
+
save("restore", operation);
|
|
357
|
+
return operation;
|
|
358
|
+
})
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/runtime/runtime-instructions.ts
|
|
363
|
+
var withRuntimeInstruction = (messages, text) => {
|
|
364
|
+
const firstConversation = messages.findIndex((message) => message.role !== "system");
|
|
365
|
+
const at = firstConversation === -1 ? messages.length : firstConversation;
|
|
366
|
+
return [...messages.slice(0, at), { role: "system", parts: [{ type: "text", text }] }, ...messages.slice(at)];
|
|
367
|
+
};
|
|
368
|
+
var withFreshSystemInstructions = (input, hostInstructions) => {
|
|
369
|
+
if ("state" in input || !input.messages?.length)
|
|
370
|
+
return input;
|
|
371
|
+
const firstConversation = input.messages.findIndex((message) => message.role !== "system");
|
|
372
|
+
const count = firstConversation === -1 ? input.messages.length : firstConversation;
|
|
373
|
+
const leading = input.messages.slice(0, count);
|
|
374
|
+
if (!count || leading.some((message) => message.parts.some((part) => part.type !== "text")))
|
|
375
|
+
return input;
|
|
376
|
+
let text = leading.flatMap((message) => message.parts.flatMap((part) => part.type === "text" ? [part.text] : [])).join(`
|
|
377
|
+
|
|
378
|
+
`);
|
|
379
|
+
const known = hostInstructions?.trim();
|
|
380
|
+
if (known && (text === known || text.startsWith(known + `
|
|
381
|
+
|
|
382
|
+
`)))
|
|
383
|
+
text = text.slice(known.length).replace(/^\n\n/, "");
|
|
384
|
+
return { ...input, system: [input.system, text].filter((value) => value !== undefined && value !== "").join(`
|
|
385
|
+
|
|
386
|
+
`), messages: input.messages.slice(count) };
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/runtime/runtime-policy.ts
|
|
390
|
+
import { createBudgetGuard, createProductionSafetyPolicy } from "@zhivex-ai/agents";
|
|
391
|
+
var effectiveRuntimeBudget = (budget) => {
|
|
392
|
+
const { unlimitedTokens, maxInputTokens, maxOutputTokens, maxTotalTokens, ...nonTokenBudget } = budget;
|
|
393
|
+
return unlimitedTokens ? nonTokenBudget : { ...nonTokenBudget, maxInputTokens, maxOutputTokens, maxTotalTokens };
|
|
394
|
+
};
|
|
395
|
+
var createRuntimeBudget = (budget, transportTokens) => {
|
|
396
|
+
const durable = createBudgetGuard(effectiveRuntimeBudget(budget));
|
|
397
|
+
const transport = transportTokens ? durable : createBudgetGuard({
|
|
398
|
+
maxSteps: budget.maxSteps,
|
|
399
|
+
maxToolCalls: budget.maxToolCalls,
|
|
400
|
+
maxToolErrors: budget.maxToolErrors,
|
|
401
|
+
includeChildRuns: budget.includeChildRuns
|
|
402
|
+
});
|
|
403
|
+
return { ...transport, inputGuardrail: durable.inputGuardrail, outputGuardrail: durable.outputGuardrail };
|
|
404
|
+
};
|
|
405
|
+
var runtimeManifest = (config, tools, role = "primary") => ({
|
|
406
|
+
schemaVersion: 2,
|
|
407
|
+
policyVersion: "assistant-v3-durable-closure",
|
|
408
|
+
role,
|
|
409
|
+
requireVerifiedDelivery: role === "primary" && config.requireVerifiedDelivery,
|
|
410
|
+
...role === "primary" && config.reasoningEffort ? { reasoningEffort: config.reasoningEffort } : {},
|
|
411
|
+
backend: config.execution.backend,
|
|
412
|
+
tools: [...tools].sort(),
|
|
413
|
+
budget: { ...role === "primary" ? config.budget : config.orchestration.childBudget },
|
|
414
|
+
timeoutMs: role === "primary" ? config.timeoutMs : config.orchestration.childTimeoutMs,
|
|
415
|
+
closureController: role === "primary" && config.requireVerifiedDelivery,
|
|
416
|
+
contextEnabled: config.context.enabled
|
|
417
|
+
});
|
|
418
|
+
var childRuntimeSafety = (config) => createProductionSafetyPolicy({
|
|
419
|
+
budget: createRuntimeBudget(config.orchestration.childBudget, false),
|
|
420
|
+
toolExecution: { ...harnessToolExecution, parallel: false }
|
|
421
|
+
});
|
|
422
|
+
var createCheckpointTokenCap = (limits, usage, transportTokens = true, options = {}) => {
|
|
423
|
+
const observed = {
|
|
424
|
+
inputTokens: options.initialUsage?.inputTokens ?? 0,
|
|
425
|
+
outputTokens: options.initialUsage?.outputTokens ?? 0,
|
|
426
|
+
totalTokens: options.initialUsage?.totalTokens ?? (options.initialUsage?.inputTokens ?? 0) + (options.initialUsage?.outputTokens ?? 0)
|
|
427
|
+
};
|
|
428
|
+
const cap = async (input, auxiliary = false) => {
|
|
429
|
+
if (limits.unlimitedTokens)
|
|
430
|
+
return;
|
|
431
|
+
const persisted = await usage();
|
|
432
|
+
for (const key of ["inputTokens", "outputTokens", "totalTokens"]) {
|
|
433
|
+
const value = persisted?.[key] ?? (key === "totalTokens" ? (persisted?.inputTokens ?? 0) + (persisted?.outputTokens ?? 0) : 0);
|
|
434
|
+
observed[key] = Math.max(observed[key], value);
|
|
435
|
+
}
|
|
436
|
+
const additional = await options.additionalUsage?.();
|
|
437
|
+
const remainingOutput = limits.maxOutputTokens - observed.outputTokens - (additional?.outputTokens ?? 0);
|
|
438
|
+
const remainingInput = limits.maxInputTokens - observed.inputTokens - (additional?.inputTokens ?? 0);
|
|
439
|
+
const remainingTotal = limits.maxTotalTokens - observed.totalTokens - (additional?.totalTokens ?? 0);
|
|
440
|
+
if (remainingInput <= 0)
|
|
441
|
+
throw new Error("maxInputTokens budget exhausted");
|
|
442
|
+
if (remainingOutput <= 0)
|
|
443
|
+
throw new Error("maxOutputTokens budget exhausted");
|
|
444
|
+
if (remainingTotal <= 0)
|
|
445
|
+
throw new Error("maxTotalTokens budget exhausted");
|
|
446
|
+
let predicted = estimateRequestTokens(input);
|
|
447
|
+
if (!auxiliary && options.closeOnBudget && (observed.inputTokens + predicted > limits.maxInputTokens * 0.7 || observed.outputTokens >= limits.maxOutputTokens * 0.7 || observed.totalTokens + predicted > limits.maxTotalTokens * 0.7)) {
|
|
448
|
+
delete input.tools;
|
|
449
|
+
delete input.toolChoice;
|
|
450
|
+
input.messages = withRuntimeInstruction(input.messages, "The run is approaching its cumulative token budget. Finish now using only the evidence already collected. State what was established and what remains unverified; do not claim that unfinished work is complete. No further tools are available.");
|
|
451
|
+
predicted = estimateRequestTokens(input);
|
|
452
|
+
}
|
|
453
|
+
if (predicted > remainingInput)
|
|
454
|
+
throw new Error("maxInputTokens budget exhausted");
|
|
455
|
+
if (predicted >= remainingTotal)
|
|
456
|
+
throw new Error("maxTotalTokens budget exhausted");
|
|
457
|
+
if (transportTokens || auxiliary)
|
|
458
|
+
input.maxTokens = Math.min(input.maxTokens ?? remainingOutput, remainingOutput, remainingTotal - predicted);
|
|
459
|
+
};
|
|
460
|
+
const record = (reported, enforce = true) => {
|
|
461
|
+
observed.inputTokens += reported?.inputTokens ?? 0;
|
|
462
|
+
observed.outputTokens += reported?.outputTokens ?? 0;
|
|
463
|
+
const sum = (reported?.inputTokens ?? 0) + (reported?.outputTokens ?? 0);
|
|
464
|
+
observed.totalTokens += Number.isSafeInteger(reported?.totalTokens) && reported.totalTokens >= 0 ? Math.max(reported.totalTokens, sum) : sum;
|
|
465
|
+
if (limits.unlimitedTokens || !enforce)
|
|
466
|
+
return;
|
|
467
|
+
if (observed.inputTokens > limits.maxInputTokens)
|
|
468
|
+
throw new Error("maxInputTokens budget exceeded");
|
|
469
|
+
if (observed.outputTokens > limits.maxOutputTokens)
|
|
470
|
+
throw new Error("maxOutputTokens budget exceeded");
|
|
471
|
+
if (observed.totalTokens > limits.maxTotalTokens)
|
|
472
|
+
throw new Error("maxTotalTokens budget exceeded");
|
|
473
|
+
};
|
|
474
|
+
return {
|
|
475
|
+
name: "harness-checkpoint-token-cap",
|
|
476
|
+
observed,
|
|
477
|
+
auxiliary: () => ({ name: "harness-utility-token-cap", async wrapGenerate(context, next) {
|
|
478
|
+
await cap(context.input, true);
|
|
479
|
+
const result = await next();
|
|
480
|
+
record(result.usage, false);
|
|
481
|
+
return result;
|
|
482
|
+
} }),
|
|
483
|
+
async wrapGenerate(context, next) {
|
|
484
|
+
await cap(context.input);
|
|
485
|
+
const result = await next();
|
|
486
|
+
record(result.usage);
|
|
487
|
+
return result;
|
|
488
|
+
},
|
|
489
|
+
async wrapStream(context, next) {
|
|
490
|
+
await cap(context.input);
|
|
491
|
+
const stream = await next();
|
|
492
|
+
return async function* () {
|
|
493
|
+
let recorded = false;
|
|
494
|
+
for await (const event of stream) {
|
|
495
|
+
if (event.type === "finish" && !recorded) {
|
|
496
|
+
record(event.usage);
|
|
497
|
+
recorded = true;
|
|
498
|
+
}
|
|
499
|
+
yield event;
|
|
500
|
+
}
|
|
501
|
+
}();
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
// src/runtime/repair-progress.ts
|
|
507
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
508
|
+
import { posix } from "node:path";
|
|
509
|
+
import { z as z2 } from "zod";
|
|
510
|
+
var REPAIR_PROGRESS_KEY = "zhivexRepairProgress";
|
|
511
|
+
var CLOSURE_READ_LIMIT = 4;
|
|
512
|
+
var CLOSURE_COMMAND_LIMIT = 3;
|
|
513
|
+
var savedProgress = z2.object({
|
|
514
|
+
closureReads: z2.number().int().min(0),
|
|
515
|
+
closureCommands: z2.number().int().min(0),
|
|
516
|
+
enteredClosure: z2.boolean(),
|
|
517
|
+
plannedPaths: z2.array(z2.string().max(240)).max(8),
|
|
518
|
+
seen: z2.array(z2.tuple([z2.string().max(128), z2.object({ output: z2.string().length(64), count: z2.number().int().min(1) })])).max(128),
|
|
519
|
+
lines: z2.array(z2.tuple([z2.string().length(64), z2.number().int().min(1)])).max(256).default([])
|
|
520
|
+
});
|
|
521
|
+
var observations = new Set(["inspect_environment_patch", "mutation_audit", "git_diff", "read_task", "environment_status", "load_skill", "propose_edits"]);
|
|
522
|
+
var reads = new Set(["list_files", "read_file", "read_files", "search_files", "search_many"]);
|
|
523
|
+
var canonical = (value) => {
|
|
524
|
+
if (Array.isArray(value))
|
|
525
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
526
|
+
if (value && typeof value === "object")
|
|
527
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
|
528
|
+
return JSON.stringify(value) ?? "null";
|
|
529
|
+
};
|
|
530
|
+
var hash = (value) => createHash2("sha256").update(canonical(value)).digest("hex");
|
|
531
|
+
var createRepairProgress = (usage, limits, metadata) => {
|
|
532
|
+
const restored = metadata?.[REPAIR_PROGRESS_KEY] === undefined ? undefined : savedProgress.parse(metadata[REPAIR_PROGRESS_KEY]);
|
|
533
|
+
let closureReads = restored?.closureReads ?? 0, closureCommands = restored?.closureCommands ?? 0;
|
|
534
|
+
let plannedPaths = restored?.plannedPaths ?? [];
|
|
535
|
+
let outputStep = -1, outputCharacters = 0;
|
|
536
|
+
const seen = new Map(restored?.seen);
|
|
537
|
+
const linesSeen = new Map(restored?.lines);
|
|
538
|
+
const stats = { repeatedResults: 0, suppressedResults: 0, blockedBroadCalls: 0, enteredClosure: restored?.enteredClosure ?? false, phase: plannedPaths.length ? "repair" : "explore", blockedPhaseCalls: 0, planRecorded: !!plannedPaths.length };
|
|
539
|
+
const snapshot = () => ({ closureReads, closureCommands, enteredClosure: stats.enteredClosure, plannedPaths: [...plannedPaths], seen: [...seen], lines: [...linesSeen] });
|
|
540
|
+
const workingContext = () => ({
|
|
541
|
+
plannedPaths: plannedPaths.map((value) => posix.normalize(value)),
|
|
542
|
+
closureReadsRemaining: Math.max(0, CLOSURE_READ_LIMIT - closureReads),
|
|
543
|
+
closureCommandsRemaining: Math.max(0, CLOSURE_COMMAND_LIMIT - closureCommands)
|
|
544
|
+
});
|
|
545
|
+
const closing = () => {
|
|
546
|
+
const used = usage();
|
|
547
|
+
stats.enteredClosure ||= used.inputTokens + (used.predictedInputTokens ?? 0) >= limits.inputTokens * 0.7 || used.outputTokens >= limits.outputTokens * 0.7;
|
|
548
|
+
return stats.enteredClosure;
|
|
549
|
+
};
|
|
550
|
+
const wrapTools = (tools) => Object.fromEntries(Object.entries(tools).map(([name, definition]) => {
|
|
551
|
+
if (!("execute" in definition))
|
|
552
|
+
return [name, definition];
|
|
553
|
+
return [name, { ...definition, async execute(input, context) {
|
|
554
|
+
try {
|
|
555
|
+
if (name === "repair_plan") {
|
|
556
|
+
const plan = input;
|
|
557
|
+
const result = await definition.execute(input, context);
|
|
558
|
+
plannedPaths = plan.paths ?? [];
|
|
559
|
+
stats.planRecorded = true;
|
|
560
|
+
stats.phase = "explore";
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
if (observations.has(name))
|
|
564
|
+
return definition.execute(input, context);
|
|
565
|
+
if (name.startsWith("verify_"))
|
|
566
|
+
stats.phase = "verify";
|
|
567
|
+
if (!reads.has(name)) {
|
|
568
|
+
if (closing() && name.startsWith("run_environment") && ++closureCommands > CLOSURE_COMMAND_LIMIT) {
|
|
569
|
+
stats.blockedPhaseCalls++;
|
|
570
|
+
throw new Error("REPAIR_PHASE_BUDGET: the closure command allowance is exhausted. Use an inspected patch and the approved verifier, or report the incomplete repair.");
|
|
571
|
+
}
|
|
572
|
+
seen.clear();
|
|
573
|
+
linesSeen.clear();
|
|
574
|
+
return definition.execute(input, context);
|
|
575
|
+
}
|
|
576
|
+
const requestedPath = input && typeof input === "object" ? input.path : undefined;
|
|
577
|
+
const pathInput = input && typeof input === "object" ? input : {};
|
|
578
|
+
const requestedPaths = name === "read_files" && Array.isArray(pathInput.files) ? pathInput.files.map((file) => file.path) : [requestedPath];
|
|
579
|
+
const normalizedPaths = requestedPaths.filter((value) => typeof value === "string").map((value) => posix.normalize(value));
|
|
580
|
+
const normalizedPlan = plannedPaths.map((value) => posix.normalize(value));
|
|
581
|
+
const globalSearch = requestedPath === undefined || typeof requestedPath === "string" && posix.normalize(requestedPath || ".") === ".";
|
|
582
|
+
if (closing() && (name === "list_files" || ["search_files", "search_many"].includes(name) && globalSearch)) {
|
|
583
|
+
stats.blockedBroadCalls++;
|
|
584
|
+
throw new Error("REPAIR_CLOSURE: broad discovery is paused to preserve the remaining budget. Use known file paths for focused reads/searches, reproduce the issue, repair and verify. If context is insufficient, report the limitation; do not guess a patch.");
|
|
585
|
+
}
|
|
586
|
+
if (closing() && plannedPaths.length && normalizedPaths.some((value) => !normalizedPlan.includes(value))) {
|
|
587
|
+
stats.blockedPhaseCalls++;
|
|
588
|
+
throw new Error("REPAIR_PLAN_SCOPE: use a file recorded in repair_plan or explicitly revise the hypothesis first.");
|
|
589
|
+
}
|
|
590
|
+
if (closing() && closureReads >= CLOSURE_READ_LIMIT) {
|
|
591
|
+
stats.blockedPhaseCalls++;
|
|
592
|
+
throw new Error("REPAIR_PHASE_BUDGET: the closure read allowance is exhausted. Recover task constraints with read_task, repair known files, and verify or report the limitation.");
|
|
593
|
+
}
|
|
594
|
+
if (closing())
|
|
595
|
+
closureReads++;
|
|
596
|
+
const output = await definition.execute(input, context);
|
|
597
|
+
if (outputStep !== context.step) {
|
|
598
|
+
outputStep = context.step;
|
|
599
|
+
outputCharacters = 0;
|
|
600
|
+
}
|
|
601
|
+
outputCharacters += JSON.stringify(output).length;
|
|
602
|
+
if (outputCharacters > 32000)
|
|
603
|
+
throw new Error("TOOL_OUTPUT_BUDGET: this turn's read output is full. Request a narrower slice on a new turn.");
|
|
604
|
+
const data = output && typeof output === "object" ? output : {};
|
|
605
|
+
const evidence = Array.isArray(data.matches) && data.matches.length ? data.matches : Array.isArray(data.results) ? data.results.flatMap((r) => r.matches ?? []) : undefined;
|
|
606
|
+
const key = evidence?.length ? hash({ evidence }) : hash({ name, input });
|
|
607
|
+
const fingerprint = hash(evidence?.length ? evidence : output);
|
|
608
|
+
const prior = seen.get(key);
|
|
609
|
+
const count = prior?.output === fingerprint ? prior.count + 1 : 1;
|
|
610
|
+
seen.delete(key);
|
|
611
|
+
seen.set(key, { output: fingerprint, count });
|
|
612
|
+
if (seen.size > 128)
|
|
613
|
+
seen.delete(seen.keys().next().value);
|
|
614
|
+
if (count > 1)
|
|
615
|
+
stats.repeatedResults++;
|
|
616
|
+
const files = Array.isArray(data.files) ? data.files : [data];
|
|
617
|
+
const lineKeys = files.flatMap((file) => typeof file.path === "string" && typeof file.digest === "string" && typeof file.content === "string" ? file.content.split(`
|
|
618
|
+
`).map((line) => hash({ path: file.path, digest: file.digest, line })) : []);
|
|
619
|
+
const overlapping = lineKeys.length > 0 && lineKeys.length <= 256 && lineKeys.every((key2) => (linesSeen.get(key2) ?? 0) >= 2);
|
|
620
|
+
for (const key2 of new Set(lineKeys.slice(0, 256))) {
|
|
621
|
+
const count2 = (linesSeen.get(key2) ?? 0) + 1;
|
|
622
|
+
linesSeen.delete(key2);
|
|
623
|
+
linesSeen.set(key2, count2);
|
|
624
|
+
if (linesSeen.size > 256)
|
|
625
|
+
linesSeen.delete(linesSeen.keys().next().value);
|
|
626
|
+
}
|
|
627
|
+
if (count > 2 || overlapping) {
|
|
628
|
+
stats.suppressedResults++;
|
|
629
|
+
throw new Error("REPEATED_EXPLORATION: this request returned unchanged or fully overlapping evidence at least three times. Its repeated payload was omitted. Request new evidence or proceed to reproduction and verification. No cached source was substituted.");
|
|
630
|
+
}
|
|
631
|
+
return output;
|
|
632
|
+
} finally {
|
|
633
|
+
if (context.metadata)
|
|
634
|
+
context.metadata[REPAIR_PROGRESS_KEY] = snapshot();
|
|
635
|
+
}
|
|
636
|
+
} }];
|
|
637
|
+
}));
|
|
638
|
+
return { stats, closing, wrapTools, snapshot, workingContext };
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// src/runtime/repair-controller.ts
|
|
642
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "node:crypto";
|
|
643
|
+
import { z as z3 } from "zod";
|
|
644
|
+
import { createRedactionPolicy } from "@zhivex-ai/agents";
|
|
645
|
+
var REPAIR_CONTROLLER_KEY = "zhivexRepairController";
|
|
646
|
+
var digestSchema = z3.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
647
|
+
var stateSchema = z3.object({
|
|
648
|
+
schemaVersion: z3.literal(1),
|
|
649
|
+
phase: z3.enum(["explore", "reproduce", "candidate", "verify", "recover", "delivered", "incomplete"]),
|
|
650
|
+
candidate: digestSchema.nullable(),
|
|
651
|
+
revision: z3.number().int().nonnegative(),
|
|
652
|
+
verifier: verifierSchema.nullable(),
|
|
653
|
+
verifierRequests: z3.number().int().nonnegative(),
|
|
654
|
+
verificationFailures: z3.number().int().nonnegative(),
|
|
655
|
+
completionReminders: z3.number().int().min(0).max(1).default(0),
|
|
656
|
+
planRequired: z3.boolean().default(false),
|
|
657
|
+
workBudgetClosure: z3.boolean().default(false),
|
|
658
|
+
requireVerifiedDelivery: z3.boolean().default(false),
|
|
659
|
+
hypothesis: z3.string().max(1000),
|
|
660
|
+
nextCheck: z3.string().max(500),
|
|
661
|
+
receipts: z3.array(z3.object({
|
|
662
|
+
commandId: z3.string().max(128),
|
|
663
|
+
purpose: z3.string().max(500),
|
|
664
|
+
argvDigest: digestSchema,
|
|
665
|
+
candidate: digestSchema.nullable(),
|
|
666
|
+
exitCode: z3.number().int(),
|
|
667
|
+
verified: z3.boolean()
|
|
668
|
+
})).max(8)
|
|
669
|
+
});
|
|
670
|
+
var mutations = new Set(["apply_reviewed_edits", "apply_reviewed_replacement", "apply_patch", "move_file", "quarantine_file", "restore_file"]);
|
|
671
|
+
var commands = new Set(["run_environment_command", "run_environment_shell", "run_environment_batch"]);
|
|
672
|
+
var terminal = new Set(["verify_and_apply_environment_patch", "verify_and_apply_reviewed_edits"]);
|
|
673
|
+
var redact = createRedactionPolicy({ includeEmails: true });
|
|
674
|
+
var sha = (value) => `sha256:${createHash3("sha256").update(JSON.stringify(value)).digest("hex")}`;
|
|
675
|
+
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
676
|
+
var createRepairController = (metadata, oci, options = {}) => {
|
|
677
|
+
const state = metadata[REPAIR_CONTROLLER_KEY] === undefined ? stateSchema.parse({
|
|
678
|
+
schemaVersion: 1,
|
|
679
|
+
phase: "explore",
|
|
680
|
+
candidate: null,
|
|
681
|
+
revision: 0,
|
|
682
|
+
verifier: null,
|
|
683
|
+
verifierRequests: 0,
|
|
684
|
+
verificationFailures: 0,
|
|
685
|
+
hypothesis: "",
|
|
686
|
+
nextCheck: "",
|
|
687
|
+
receipts: []
|
|
688
|
+
}) : stateSchema.parse(metadata[REPAIR_CONTROLLER_KEY]);
|
|
689
|
+
state.requireVerifiedDelivery ||= options.requireVerifiedDelivery === true;
|
|
690
|
+
const snapshot = () => structuredClone(state);
|
|
691
|
+
const pending = () => state.candidate !== null && state.phase !== "delivered";
|
|
692
|
+
const closure = () => state.workBudgetClosure || state.planRequired || pending() || state.phase === "recover";
|
|
693
|
+
const completionPending = () => state.requireVerifiedDelivery && state.phase !== "delivered" || state.planRequired || pending() || state.verifier !== null && state.phase !== "delivered";
|
|
694
|
+
const markIncomplete = () => {
|
|
695
|
+
if (completionPending())
|
|
696
|
+
state.phase = "incomplete";
|
|
697
|
+
};
|
|
698
|
+
const verificationFailed = (attemptedVerification = true) => {
|
|
699
|
+
if (attemptedVerification)
|
|
700
|
+
state.verificationFailures++;
|
|
701
|
+
state.phase = "recover";
|
|
702
|
+
state.verifier = null;
|
|
703
|
+
state.verifierRequests = 0;
|
|
704
|
+
};
|
|
705
|
+
const wrapTools = (tools) => Object.fromEntries(Object.entries(tools).map(([name, tool]) => {
|
|
706
|
+
if (!("execute" in tool))
|
|
707
|
+
return [name, tool];
|
|
708
|
+
return [name, { ...tool, async execute(input, context) {
|
|
709
|
+
let executed = false;
|
|
710
|
+
try {
|
|
711
|
+
if (oci && mutations.has(name) && !state.verifier)
|
|
712
|
+
state.planRequired = true;
|
|
713
|
+
if (state.planRequired && !state.verifier && !["repair_plan", "read_task", "environment_status", "mutation_audit"].includes(name)) {
|
|
714
|
+
throw new Error("REPAIR_PLAN_REQUIRED: call repair_plan with exact verifier command, args and purpose before editing. No edit was executed. The verifier must assert the requested behavior; recording it is not approval.");
|
|
715
|
+
}
|
|
716
|
+
if (pending() && state.phase !== "recover" && !terminal.has(name) && !["repair_plan", "read_task", "inspect_environment_patch", "environment_status", "mutation_audit", "run_check"].includes(name)) {
|
|
717
|
+
throw new Error("REPAIR_VERIFICATION_REQUIRED: verify the existing candidate before further exploration; revise the plan after a failed check.");
|
|
718
|
+
}
|
|
719
|
+
executed = true;
|
|
720
|
+
const value = await tool.execute(input, context);
|
|
721
|
+
const output = record(value);
|
|
722
|
+
if (name === "repair_plan") {
|
|
723
|
+
const plan = record(input);
|
|
724
|
+
state.hypothesis = redact.redactText(String(plan.hypothesis ?? "")).slice(0, 1000);
|
|
725
|
+
state.nextCheck = redact.redactText(String(plan.nextCheck ?? "")).slice(0, 500);
|
|
726
|
+
if (plan.verifier !== undefined) {
|
|
727
|
+
state.verifier = verifierSchema.parse(plan.verifier);
|
|
728
|
+
if (state.planRequired) {
|
|
729
|
+
state.planRequired = false;
|
|
730
|
+
state.verifierRequests = 0;
|
|
731
|
+
}
|
|
732
|
+
if (pending())
|
|
733
|
+
state.phase = "candidate";
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (mutations.has(name) || commands.has(name)) {
|
|
737
|
+
const session = harnessExecutionSession(context);
|
|
738
|
+
const patch = session ? await session.inspectPatch() : undefined;
|
|
739
|
+
const candidate = patch ? patch.entries.length ? patch.patchId : null : mutations.has(name) ? sha(output) : state.candidate;
|
|
740
|
+
if (candidate !== state.candidate) {
|
|
741
|
+
state.revision++;
|
|
742
|
+
state.verifierRequests = 0;
|
|
743
|
+
}
|
|
744
|
+
state.candidate = candidate;
|
|
745
|
+
state.phase = candidate ? "candidate" : commands.has(name) ? "reproduce" : "explore";
|
|
746
|
+
}
|
|
747
|
+
if (name === "inspect_environment_patch" && Array.isArray(output.entries)) {
|
|
748
|
+
const candidate = output.entries.length ? digestSchema.parse(output.patchId) : null;
|
|
749
|
+
if (candidate !== state.candidate) {
|
|
750
|
+
state.revision++;
|
|
751
|
+
state.verifierRequests = 0;
|
|
752
|
+
}
|
|
753
|
+
state.candidate = candidate;
|
|
754
|
+
if (state.phase !== "recover")
|
|
755
|
+
state.phase = candidate ? "candidate" : "explore";
|
|
756
|
+
}
|
|
757
|
+
if (terminal.has(name) && typeof output.patchId === "string") {
|
|
758
|
+
const candidate = digestSchema.parse(output.patchId);
|
|
759
|
+
if (candidate !== state.candidate)
|
|
760
|
+
state.revision++;
|
|
761
|
+
state.candidate = candidate;
|
|
762
|
+
}
|
|
763
|
+
const verification = terminal.has(name) ? record(output.verification) : output;
|
|
764
|
+
if ((commands.has(name) || terminal.has(name) || name === "run_check") && Number.isSafeInteger(verification.exitCode)) {
|
|
765
|
+
const passed = verification.exitCode === 0 && verification.timedOut !== true;
|
|
766
|
+
state.receipts.push({
|
|
767
|
+
commandId: context?.toolCall?.id ?? `check_${randomUUID2()}`,
|
|
768
|
+
purpose: redact.redactText((state.verifier?.purpose ?? state.nextCheck) || "Command execution; requirement coverage not established").slice(0, 500),
|
|
769
|
+
argvDigest: sha(input),
|
|
770
|
+
candidate: state.candidate,
|
|
771
|
+
exitCode: verification.exitCode,
|
|
772
|
+
verified: passed && (terminal.has(name) || !oci && name === "run_check" && pending())
|
|
773
|
+
});
|
|
774
|
+
if (state.receipts.length > 8)
|
|
775
|
+
state.receipts.shift();
|
|
776
|
+
if (passed && (terminal.has(name) || !oci && name === "run_check" && pending()))
|
|
777
|
+
state.phase = "delivered";
|
|
778
|
+
else if (!passed && pending())
|
|
779
|
+
verificationFailed(terminal.has(name) || !oci && name === "run_check");
|
|
780
|
+
}
|
|
781
|
+
return value;
|
|
782
|
+
} catch (error) {
|
|
783
|
+
if (executed && (mutations.has(name) || commands.has(name) || terminal.has(name))) {
|
|
784
|
+
try {
|
|
785
|
+
const session = harnessExecutionSession(context);
|
|
786
|
+
const patch = session ? await session.inspectPatch() : undefined;
|
|
787
|
+
const candidate = patch ? patch.entries.length ? patch.patchId : null : sha({ failedTool: name, call: context?.toolCall?.id });
|
|
788
|
+
const changed = candidate !== state.candidate;
|
|
789
|
+
if (changed)
|
|
790
|
+
state.revision++;
|
|
791
|
+
state.candidate = candidate;
|
|
792
|
+
if (candidate !== null && (changed || pending())) {
|
|
793
|
+
const failure = record(record(error).verification);
|
|
794
|
+
if (Number.isSafeInteger(failure.exitCode)) {
|
|
795
|
+
state.receipts.push({
|
|
796
|
+
commandId: context?.toolCall?.id ?? `check_${randomUUID2()}`,
|
|
797
|
+
purpose: redact.redactText(state.verifier?.purpose ?? state.nextCheck).slice(0, 500),
|
|
798
|
+
argvDigest: sha(input),
|
|
799
|
+
candidate,
|
|
800
|
+
exitCode: failure.exitCode,
|
|
801
|
+
verified: false
|
|
802
|
+
});
|
|
803
|
+
if (state.receipts.length > 8)
|
|
804
|
+
state.receipts.shift();
|
|
805
|
+
}
|
|
806
|
+
verificationFailed(terminal.has(name));
|
|
807
|
+
}
|
|
808
|
+
} catch {
|
|
809
|
+
state.candidate ??= sha({ uninspectedEffect: name, call: context?.toolCall?.id });
|
|
810
|
+
state.phase = "incomplete";
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
throw error;
|
|
814
|
+
} finally {
|
|
815
|
+
if (context?.metadata)
|
|
816
|
+
context.metadata[REPAIR_CONTROLLER_KEY] = snapshot();
|
|
817
|
+
}
|
|
818
|
+
} }];
|
|
819
|
+
}));
|
|
820
|
+
const prepare = (input, provider) => {
|
|
821
|
+
input.abortSignal?.throwIfAborted();
|
|
822
|
+
const sources = taskSources(metadata);
|
|
823
|
+
const latest = sources.at(-1);
|
|
824
|
+
const projection = {
|
|
825
|
+
activeRequest: latest ? { id: latest.id, excerpt: latest.text.slice(0, 1600), totalCharacters: latest.text.length } : null,
|
|
826
|
+
previousRequestIds: sources.slice(-4, -1).map((source) => source.id),
|
|
827
|
+
phase: state.phase,
|
|
828
|
+
candidate: state.candidate,
|
|
829
|
+
revision: state.revision,
|
|
830
|
+
planRequired: state.planRequired,
|
|
831
|
+
requireVerifiedDelivery: state.requireVerifiedDelivery,
|
|
832
|
+
workBudgetClosure: state.workBudgetClosure,
|
|
833
|
+
hypothesis: state.hypothesis,
|
|
834
|
+
nextCheck: state.nextCheck,
|
|
835
|
+
checks: state.receipts.slice(-2),
|
|
836
|
+
progress: options.progressContext?.() ?? null,
|
|
837
|
+
completionReminder: state.completionReminders > 0 ? "A final answer did not fulfill the repair. Continue the focused repair and verification; do not claim delivery without a verified candidate." : null
|
|
838
|
+
};
|
|
839
|
+
const workingState = { type: "text", text: "" };
|
|
840
|
+
const renderState = () => {
|
|
841
|
+
workingState.text = `[Harness working state; not approval]
|
|
842
|
+
${JSON.stringify(projection)}
|
|
843
|
+
Use read_task for complete constraints if the excerpt is incomplete. A candidate must be verified before completion.`;
|
|
844
|
+
};
|
|
845
|
+
renderState();
|
|
846
|
+
input.messages = [...input.messages, { role: "user", parts: [workingState] }];
|
|
847
|
+
if (oci && state.requireVerifiedDelivery && !pending() && ["explore", "reproduce"].includes(state.phase) && options.workBudgetReached?.(input)) {
|
|
848
|
+
state.workBudgetClosure = true;
|
|
849
|
+
projection.workBudgetClosure = true;
|
|
850
|
+
if (!state.verifier)
|
|
851
|
+
state.planRequired = true;
|
|
852
|
+
projection.planRequired = state.planRequired;
|
|
853
|
+
renderState();
|
|
854
|
+
}
|
|
855
|
+
if (state.phase === "incomplete")
|
|
856
|
+
throw new Error("REPAIR_INCOMPLETE: authorization or verification is missing.");
|
|
857
|
+
if (pending() && state.verificationFailures > 2)
|
|
858
|
+
throw new Error("REPAIR_VERIFICATION_RETRIES_EXHAUSTED");
|
|
859
|
+
if (state.planRequired && !state.verifier) {
|
|
860
|
+
if (state.verifierRequests >= 2)
|
|
861
|
+
throw new Error("REPAIR_PLAN_MISSING: register a concrete verifier before editing.");
|
|
862
|
+
if (!input.tools?.repair_plan)
|
|
863
|
+
throw new Error("REPAIR_VERIFIER_UNAVAILABLE");
|
|
864
|
+
state.verifierRequests++;
|
|
865
|
+
input.tools = Object.fromEntries(Object.entries(input.tools).filter(([name]) => ["repair_plan", "read_task"].includes(name)));
|
|
866
|
+
const supported = provider !== "qwen" || input.reasoning?.effort === "none" || input.providerOptions?.enable_thinking === false;
|
|
867
|
+
input.toolChoice = supported ? { type: "tool", toolName: "repair_plan" } : "auto";
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (!pending()) {
|
|
871
|
+
if (completionPending()) {
|
|
872
|
+
const supported = provider !== "qwen" || input.reasoning?.effort === "none" || input.providerOptions?.enable_thinking === false;
|
|
873
|
+
input.toolChoice = supported ? "required" : "auto";
|
|
874
|
+
}
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (oci && state.verifier && state.phase !== "recover" && input.tools?.verify_and_apply_environment_patch) {
|
|
878
|
+
state.phase = "verify";
|
|
879
|
+
return {
|
|
880
|
+
id: `controller_${randomUUID2()}`,
|
|
881
|
+
name: "verify_and_apply_environment_patch",
|
|
882
|
+
input: { patchId: state.candidate, command: state.verifier.command, args: state.verifier.args }
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
if (state.phase === "recover") {
|
|
886
|
+
const requiredChoiceSupported = provider !== "qwen" || input.reasoning?.effort === "none" || input.providerOptions?.enable_thinking === false;
|
|
887
|
+
input.toolChoice = requiredChoiceSupported ? "required" : "auto";
|
|
888
|
+
}
|
|
889
|
+
if (state.phase !== "recover") {
|
|
890
|
+
if (++state.verifierRequests > 2)
|
|
891
|
+
throw new Error("REPAIR_VERIFIER_MISSING: candidate retained without verification.");
|
|
892
|
+
const required = oci ? "repair_plan" : "run_check";
|
|
893
|
+
if (!input.tools?.[required])
|
|
894
|
+
throw new Error("REPAIR_VERIFIER_UNAVAILABLE");
|
|
895
|
+
input.tools = Object.fromEntries(Object.entries(input.tools).filter(([name]) => [required, "read_task"].includes(name)));
|
|
896
|
+
const namedChoiceSupported = provider !== "qwen" || input.reasoning?.effort === "none" || input.providerOptions?.enable_thinking === false;
|
|
897
|
+
input.toolChoice = namedChoiceSupported ? { type: "tool", toolName: required } : "auto";
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
const synthetic = (call) => ({
|
|
901
|
+
message: { role: "assistant", parts: [{ type: "tool-call", toolCall: call }] },
|
|
902
|
+
finishReason: "tool-calls",
|
|
903
|
+
providerFinishReason: "harness-controller",
|
|
904
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
|
|
905
|
+
});
|
|
906
|
+
const remind = (input, reason, usage, sawTool) => {
|
|
907
|
+
input.abortSignal?.throwIfAborted();
|
|
908
|
+
if (reason !== "stop" || sawTool || !completionPending() || state.completionReminders >= 1 || !input.tools?.read_task || state.planRequired && state.verifierRequests >= 2 || pending() && state.phase !== "recover" && !state.verifier && state.verifierRequests >= 2 || !usage || ![usage.inputTokens, usage.outputTokens, usage.cachedInputTokens ?? 0].every((n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0))
|
|
909
|
+
return;
|
|
910
|
+
state.completionReminders++;
|
|
911
|
+
return { id: `controller_${randomUUID2()}`, name: "read_task", input: {} };
|
|
912
|
+
};
|
|
913
|
+
const middleware = {
|
|
914
|
+
name: "harness-repair-controller-v1",
|
|
915
|
+
async wrapGenerate(context, next) {
|
|
916
|
+
const call = prepare(context.input, context.model.provider);
|
|
917
|
+
if (call)
|
|
918
|
+
return synthetic(call);
|
|
919
|
+
const result = await next();
|
|
920
|
+
const messages = result.messages ?? (result.message ? [result.message] : []);
|
|
921
|
+
const reminder = remind(context.input, result.finishReason, result.usage, messages.some((m) => m.parts.some((p) => p.type === "tool-call" || p.type === "tool-result")));
|
|
922
|
+
if (!reminder)
|
|
923
|
+
return result;
|
|
924
|
+
const last = messages.at(-1);
|
|
925
|
+
const message = { role: "assistant", parts: [
|
|
926
|
+
...last?.role === "assistant" ? last.parts : [],
|
|
927
|
+
{ type: "tool-call", toolCall: reminder }
|
|
928
|
+
] };
|
|
929
|
+
return {
|
|
930
|
+
...result,
|
|
931
|
+
message,
|
|
932
|
+
messages: [...last?.role === "assistant" ? messages.slice(0, -1) : messages, message],
|
|
933
|
+
finishReason: "tool-calls",
|
|
934
|
+
providerFinishReason: "harness-completion-reminder"
|
|
935
|
+
};
|
|
936
|
+
},
|
|
937
|
+
async wrapStream(context, next) {
|
|
938
|
+
const call = prepare(context.input, context.model.provider);
|
|
939
|
+
if (!call) {
|
|
940
|
+
const stream = await next();
|
|
941
|
+
return async function* () {
|
|
942
|
+
let sawTool = false;
|
|
943
|
+
let finish;
|
|
944
|
+
for await (const event of stream) {
|
|
945
|
+
if (["tool-call", "tool-result", "tool-approval-request", "error"].includes(event.type))
|
|
946
|
+
sawTool = true;
|
|
947
|
+
if (event.type === "finish") {
|
|
948
|
+
if (finish)
|
|
949
|
+
throw new Error("REPAIR_AMBIGUOUS_STREAM: multiple terminal events.");
|
|
950
|
+
finish = event;
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
yield event;
|
|
954
|
+
}
|
|
955
|
+
if (finish) {
|
|
956
|
+
const reminder = remind(context.input, finish.finishReason, finish.usage, sawTool);
|
|
957
|
+
if (reminder)
|
|
958
|
+
yield { type: "tool-call", toolCall: reminder };
|
|
959
|
+
yield reminder ? { ...finish, finishReason: "tool-calls", providerFinishReason: "harness-completion-reminder" } : finish;
|
|
960
|
+
}
|
|
961
|
+
}();
|
|
962
|
+
}
|
|
963
|
+
return async function* () {
|
|
964
|
+
yield { type: "tool-call", toolCall: call };
|
|
965
|
+
yield { type: "finish", finishReason: "tool-calls", providerFinishReason: "harness-controller", usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 } };
|
|
966
|
+
}();
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
return { state, snapshot, pending, completionPending, closure, markIncomplete, verificationFailed, wrapTools, middleware };
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// src/runtime/runtime-checkpoints.ts
|
|
973
|
+
var RUNTIME_DIAGNOSTICS_KEY = "zhivexRuntimeDiagnostics";
|
|
974
|
+
var runtimeCheckpointStore = (store, runId, budget, progress, controller) => new Proxy(store, {
|
|
975
|
+
get(target, key) {
|
|
976
|
+
if (key === "save")
|
|
977
|
+
return async (...args) => {
|
|
978
|
+
const [state] = args;
|
|
979
|
+
if (state.runId === runId) {
|
|
980
|
+
if (state.status === "completed" && controller.completionPending()) {
|
|
981
|
+
controller.markIncomplete();
|
|
982
|
+
state.status = "failed";
|
|
983
|
+
state.outputText = "Repair incomplete: the candidate has not been verified and delivered.";
|
|
984
|
+
state.error = { message: "REPAIR_INCOMPLETE" };
|
|
985
|
+
}
|
|
986
|
+
state.metadata = {
|
|
987
|
+
...state.metadata,
|
|
988
|
+
[MODEL_BUDGET_KEY]: budget.snapshot(),
|
|
989
|
+
[REPAIR_PROGRESS_KEY]: progress.snapshot(),
|
|
990
|
+
[REPAIR_CONTROLLER_KEY]: controller.snapshot(),
|
|
991
|
+
[RUNTIME_DIAGNOSTICS_KEY]: {
|
|
992
|
+
schemaVersion: 2,
|
|
993
|
+
requireVerifiedDelivery: controller.state.requireVerifiedDelivery,
|
|
994
|
+
budget: { ...budget.stats },
|
|
995
|
+
phase: controller.state.phase,
|
|
996
|
+
candidate: controller.state.candidate,
|
|
997
|
+
revision: controller.state.revision,
|
|
998
|
+
checks: controller.state.receipts,
|
|
999
|
+
contextMeasurements: budget.contextMetrics,
|
|
1000
|
+
modelTimings: budget.modelTimings,
|
|
1001
|
+
omittedContextMeasurements: budget.omittedContextMeasurements
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
return target.save(...args);
|
|
1006
|
+
};
|
|
1007
|
+
const value = Reflect.get(target, key, target);
|
|
1008
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
var tokenUsageCheckpointStore = (store, runId, observed) => new Proxy(store, {
|
|
1012
|
+
get(target, key) {
|
|
1013
|
+
if (key === "save")
|
|
1014
|
+
return async (...args) => {
|
|
1015
|
+
const [state] = args;
|
|
1016
|
+
if (state.runId === runId && state.status === "failed") {
|
|
1017
|
+
const usage = observed();
|
|
1018
|
+
state.usage = {
|
|
1019
|
+
...state.usage,
|
|
1020
|
+
inputTokens: Math.max(state.usage?.inputTokens ?? 0, usage.inputTokens ?? 0),
|
|
1021
|
+
outputTokens: Math.max(state.usage?.outputTokens ?? 0, usage.outputTokens ?? 0),
|
|
1022
|
+
totalTokens: Math.max(state.usage?.totalTokens ?? 0, usage.totalTokens ?? 0)
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
return target.save(...args);
|
|
1026
|
+
};
|
|
1027
|
+
const value = Reflect.get(target, key, target);
|
|
1028
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
// src/persistence/operations.ts
|
|
1033
|
+
import { chmod, lstat, mkdir, open } from "node:fs/promises";
|
|
1034
|
+
import path2 from "node:path";
|
|
1035
|
+
import {
|
|
1036
|
+
cancelAgentRun,
|
|
1037
|
+
cancelAgentRunTree,
|
|
1038
|
+
createRedactionPolicy as createRedactionPolicy2,
|
|
1039
|
+
getAgentBudgetStatus
|
|
1040
|
+
} from "@zhivex-ai/agents";
|
|
1041
|
+
import {
|
|
1042
|
+
createAgentTraceArtifact,
|
|
1043
|
+
createHierarchicalAgentTrace,
|
|
1044
|
+
createFileAgentMemoryStore,
|
|
1045
|
+
createFileAgentRunStore,
|
|
1046
|
+
createProductionTraceOptions,
|
|
1047
|
+
createSqliteAgentMemoryStore,
|
|
1048
|
+
createSqliteAgentRunStore
|
|
1049
|
+
} from "@zhivex-ai/agents/ops";
|
|
1050
|
+
import { createAgentRunLedger } from "@zhivex-ai/agents/control-plane";
|
|
1051
|
+
var budgetScopeFor = (config) => ({
|
|
1052
|
+
tenantId: config.scope.tenantId,
|
|
1053
|
+
...config.scope.userId ? { userId: config.scope.userId } : {},
|
|
1054
|
+
namespace: "__zhivex_budget__"
|
|
1055
|
+
});
|
|
1056
|
+
var withBudgetLedgerStore = (primary, budget, config) => {
|
|
1057
|
+
const scope = budgetScopeFor(config);
|
|
1058
|
+
const budgetScope = (value) => value?.namespace === scope.namespace && value.tenantId === scope.tenantId && (value.userId ?? undefined) === (scope.userId ?? undefined);
|
|
1059
|
+
const assertId = (id) => {
|
|
1060
|
+
if (!/^budget_[a-f0-9]{64}$/.test(id))
|
|
1061
|
+
throw new HarnessStateConflictError("Invalid budget ledger identity.");
|
|
1062
|
+
};
|
|
1063
|
+
return {
|
|
1064
|
+
...primary,
|
|
1065
|
+
load: async (id, requestedScope) => {
|
|
1066
|
+
if (!budgetScope(requestedScope))
|
|
1067
|
+
return primary.load(id, requestedScope);
|
|
1068
|
+
assertId(id);
|
|
1069
|
+
return budget.load(id, requestedScope);
|
|
1070
|
+
},
|
|
1071
|
+
save: async (state, options) => {
|
|
1072
|
+
if (!budgetScope(state.scope))
|
|
1073
|
+
return primary.save(state, options);
|
|
1074
|
+
assertId(state.runId);
|
|
1075
|
+
if (state.provider !== "zhivex" || state.modelId !== "budget-coordinator" || state.metadata?.budgetCoordinator !== true) {
|
|
1076
|
+
throw new HarnessStateConflictError("Only SDK budget ledger states may use the reserved namespace.");
|
|
1077
|
+
}
|
|
1078
|
+
return budget.save(state, options);
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
};
|
|
1082
|
+
var HARNESS_SQLITE_FILE = "operations.sqlite";
|
|
1083
|
+
var HARNESS_OPERATIONS_SCHEMA_VERSION = 1;
|
|
1084
|
+
var TERMINAL_RUN_STATUSES = ["completed", "failed", "cancelled", "timed_out"];
|
|
1085
|
+
var runSummary = (state) => {
|
|
1086
|
+
const consumption = getAgentBudgetStatus(state, { includeChildRuns: false }).consumption;
|
|
1087
|
+
return {
|
|
1088
|
+
runId: state.runId,
|
|
1089
|
+
revision: state.revision ?? 0,
|
|
1090
|
+
status: state.status,
|
|
1091
|
+
provider: state.provider,
|
|
1092
|
+
model: state.modelId,
|
|
1093
|
+
agentId: state.agentId,
|
|
1094
|
+
parentRunId: state.parentRunId,
|
|
1095
|
+
idempotencyKey: state.idempotencyKey,
|
|
1096
|
+
scope: state.scope,
|
|
1097
|
+
steps: consumption.steps,
|
|
1098
|
+
toolCalls: consumption.toolCalls,
|
|
1099
|
+
toolErrors: consumption.toolErrors,
|
|
1100
|
+
pendingApprovals: state.pendingApprovals.length,
|
|
1101
|
+
compactions: state.compactions?.length ?? 0,
|
|
1102
|
+
childRuns: state.childRuns?.length ?? 0,
|
|
1103
|
+
usage: state.usage,
|
|
1104
|
+
startedAt: state.startedAt,
|
|
1105
|
+
updatedAt: state.updatedAt,
|
|
1106
|
+
cancelledAt: state.cancelledAt,
|
|
1107
|
+
cancellationReason: state.cancellationReason
|
|
1108
|
+
};
|
|
1109
|
+
};
|
|
1110
|
+
var listHarnessRuns = async (store, config, query = {}) => {
|
|
1111
|
+
if (!store.list) {
|
|
1112
|
+
throw new HarnessStateConflictError(`The ${config.storeBackend} run store does not support listing.`);
|
|
1113
|
+
}
|
|
1114
|
+
const page = await store.list(query, config.scope);
|
|
1115
|
+
return {
|
|
1116
|
+
schemaVersion: HARNESS_OPERATIONS_SCHEMA_VERSION,
|
|
1117
|
+
kind: "run-list",
|
|
1118
|
+
scope: config.scope,
|
|
1119
|
+
backend: config.storeBackend,
|
|
1120
|
+
runs: page.items.map(runSummary),
|
|
1121
|
+
...page.nextCursor ? { nextCursor: page.nextCursor } : {}
|
|
1122
|
+
};
|
|
1123
|
+
};
|
|
1124
|
+
var inspectHarnessRun = async (store, config, runId) => {
|
|
1125
|
+
const state = await store.load(runId, config.scope);
|
|
1126
|
+
if (!state) {
|
|
1127
|
+
throw new HarnessStateConflictError(`Run ${runId} was not found in ${config.stateDirectory}.`);
|
|
1128
|
+
}
|
|
1129
|
+
const redaction = createRedactionPolicy2({ includeEmails: true });
|
|
1130
|
+
const traceOptions = createProductionTraceOptions({
|
|
1131
|
+
includeMessages: false,
|
|
1132
|
+
includeToolInputs: false,
|
|
1133
|
+
includeToolOutputs: false,
|
|
1134
|
+
includeApprovalArguments: false,
|
|
1135
|
+
includeOutputText: false,
|
|
1136
|
+
outputPreviewLength: 240,
|
|
1137
|
+
redaction
|
|
1138
|
+
});
|
|
1139
|
+
const rawLedger = createAgentRunLedger(state, {
|
|
1140
|
+
includeTimeline: true,
|
|
1141
|
+
includeInput: false,
|
|
1142
|
+
includeOutput: false,
|
|
1143
|
+
includeMetadata: false,
|
|
1144
|
+
outputPreviewLength: 240,
|
|
1145
|
+
redaction,
|
|
1146
|
+
...config.costBudget ? {
|
|
1147
|
+
pricing: {
|
|
1148
|
+
inputCostPer1kTokens: config.costBudget.inputCostPer1kTokens,
|
|
1149
|
+
outputCostPer1kTokens: config.costBudget.outputCostPer1kTokens,
|
|
1150
|
+
currency: "USD"
|
|
1151
|
+
}
|
|
1152
|
+
} : {},
|
|
1153
|
+
trace: traceOptions
|
|
1154
|
+
});
|
|
1155
|
+
const { outputText: _ledgerOutput, ...ledgerSnapshot } = rawLedger.snapshot;
|
|
1156
|
+
const ledger = { ...rawLedger, snapshot: ledgerSnapshot };
|
|
1157
|
+
const snapshot = ledgerSnapshot;
|
|
1158
|
+
const journal = await store.listToolCalls?.(runId, config.scope) ?? [];
|
|
1159
|
+
const hierarchyStore = {
|
|
1160
|
+
load: (candidateRunId) => store.load(candidateRunId, config.scope),
|
|
1161
|
+
save: (candidateState, options) => store.save(candidateState, options),
|
|
1162
|
+
findByParentRunId: (parentRunId) => store.findByParentRunId?.(parentRunId, config.scope) ?? []
|
|
1163
|
+
};
|
|
1164
|
+
const hierarchy = await createHierarchicalAgentTrace(hierarchyStore, runId, traceOptions);
|
|
1165
|
+
return {
|
|
1166
|
+
schemaVersion: HARNESS_OPERATIONS_SCHEMA_VERSION,
|
|
1167
|
+
kind: "run-inspection",
|
|
1168
|
+
run: runSummary(state),
|
|
1169
|
+
budget: getAgentBudgetStatus(state, effectiveRuntimeBudget(config.budget)),
|
|
1170
|
+
snapshot,
|
|
1171
|
+
trace: createAgentTraceArtifact(state, traceOptions),
|
|
1172
|
+
hierarchy,
|
|
1173
|
+
ledger,
|
|
1174
|
+
runtimeDiagnostics: inspectRuntimeDiagnostics(state.metadata?.[RUNTIME_DIAGNOSTICS_KEY]),
|
|
1175
|
+
usageLedger: inspectUsageLedger(state.metadata?.[USAGE_LEDGER_KEY]),
|
|
1176
|
+
effectiveRuntime: inspectRuntimeManifest(state.metadata?.effectiveRuntime),
|
|
1177
|
+
toolJournal: journal.map((entry) => ({
|
|
1178
|
+
toolCallId: entry.toolCallId,
|
|
1179
|
+
toolName: entry.toolName,
|
|
1180
|
+
status: entry.status,
|
|
1181
|
+
revision: entry.revision,
|
|
1182
|
+
startedAt: entry.startedAt,
|
|
1183
|
+
completedAt: entry.completedAt,
|
|
1184
|
+
updatedAt: entry.updatedAt,
|
|
1185
|
+
error: entry.error ? { message: redaction.redactText(entry.error.message) } : undefined
|
|
1186
|
+
}))
|
|
1187
|
+
};
|
|
1188
|
+
};
|
|
1189
|
+
var cancelHarnessRun = async (store, config, runId, options = {}) => {
|
|
1190
|
+
const cancellationOptions = {
|
|
1191
|
+
scope: config.scope,
|
|
1192
|
+
mode: options.final ? "final" : "request",
|
|
1193
|
+
...options.reason ? { reason: options.reason } : {}
|
|
1194
|
+
};
|
|
1195
|
+
const result = options.cascade ? await cancelAgentRunTree(store, runId, cancellationOptions) : await cancelAgentRun(store, runId, cancellationOptions);
|
|
1196
|
+
if (!result || "parent" in result && !result.parent) {
|
|
1197
|
+
throw new HarnessStateConflictError(`Run ${runId} was not found in ${config.stateDirectory}.`);
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
schemaVersion: HARNESS_OPERATIONS_SCHEMA_VERSION,
|
|
1201
|
+
kind: "run-cancellation",
|
|
1202
|
+
cascade: options.cascade ?? false,
|
|
1203
|
+
...options.cascade ? {
|
|
1204
|
+
parent: runSummary(result.parent),
|
|
1205
|
+
children: result.children.map(runSummary)
|
|
1206
|
+
} : { run: runSummary(result) }
|
|
1207
|
+
};
|
|
1208
|
+
};
|
|
1209
|
+
var cleanupHarnessRuns = async (store, config, options) => {
|
|
1210
|
+
if (!store.deleteExpired) {
|
|
1211
|
+
throw new HarnessStateConflictError(`The ${config.storeBackend} run store does not support retention cleanup.`);
|
|
1212
|
+
}
|
|
1213
|
+
const statuses = options.statuses?.length ? options.statuses : [...TERMINAL_RUN_STATUSES];
|
|
1214
|
+
const deleted = await store.deleteExpired({
|
|
1215
|
+
before: options.before,
|
|
1216
|
+
statuses,
|
|
1217
|
+
limit: options.limit ?? 1000
|
|
1218
|
+
}, config.scope);
|
|
1219
|
+
return {
|
|
1220
|
+
schemaVersion: HARNESS_OPERATIONS_SCHEMA_VERSION,
|
|
1221
|
+
kind: "run-cleanup",
|
|
1222
|
+
before: options.before,
|
|
1223
|
+
statuses,
|
|
1224
|
+
deleted
|
|
1225
|
+
};
|
|
1226
|
+
};
|
|
1227
|
+
var emptyMigration = () => ({
|
|
1228
|
+
scannedRuns: 0,
|
|
1229
|
+
migratedRuns: 0,
|
|
1230
|
+
migratedToolCalls: 0
|
|
1231
|
+
});
|
|
1232
|
+
var shouldMigrateLegacyRuns = (config, requested) => requested ?? (config.scope.tenantId === "local" && config.scope.userId === undefined && config.scope.namespace === defaultHarnessNamespace(config.workspace));
|
|
1233
|
+
var sqliteAdapter = (database) => ({
|
|
1234
|
+
exec(sql) {
|
|
1235
|
+
return database.exec(sql);
|
|
1236
|
+
},
|
|
1237
|
+
prepare(sql) {
|
|
1238
|
+
const statement = database.query(sql);
|
|
1239
|
+
const bindings = (params) => params === undefined ? [] : Array.isArray(params) ? params : [params];
|
|
1240
|
+
return {
|
|
1241
|
+
run(params) {
|
|
1242
|
+
return statement.run(...bindings(params));
|
|
1243
|
+
},
|
|
1244
|
+
get(params) {
|
|
1245
|
+
return statement.get(...bindings(params));
|
|
1246
|
+
},
|
|
1247
|
+
all(params) {
|
|
1248
|
+
return statement.all(...bindings(params));
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
var ensurePrivateStateDirectory = async (directory) => {
|
|
1254
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
1255
|
+
const entry = await lstat(directory);
|
|
1256
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
1257
|
+
throw new HarnessWorkspaceError(`The state directory must be a real directory: ${directory}.`);
|
|
1258
|
+
}
|
|
1259
|
+
await chmod(directory, 448);
|
|
1260
|
+
};
|
|
1261
|
+
var migrateJournalEntry = (entry, state, config) => ({
|
|
1262
|
+
...entry,
|
|
1263
|
+
runId: state.runId,
|
|
1264
|
+
scope: config.scope
|
|
1265
|
+
});
|
|
1266
|
+
var migrateLegacyFileRuns = async (directory, target, config) => {
|
|
1267
|
+
const source = createFileAgentRunStore({ directory });
|
|
1268
|
+
const result = emptyMigration();
|
|
1269
|
+
let cursor;
|
|
1270
|
+
do {
|
|
1271
|
+
const page = await source.list?.({ limit: 100, ...cursor ? { cursor } : {} });
|
|
1272
|
+
if (!page) {
|
|
1273
|
+
break;
|
|
1274
|
+
}
|
|
1275
|
+
for (const legacyState of page.items) {
|
|
1276
|
+
result.scannedRuns += 1;
|
|
1277
|
+
if (legacyState.budgetCoordinatorId || legacyState.metadata?.budgetCoordinator === true) {
|
|
1278
|
+
throw new HarnessStateConflictError("Legacy file migration cannot rebind a shared budget identity; retain the original scoped store.");
|
|
1279
|
+
}
|
|
1280
|
+
const existing = await target.load(legacyState.runId, config.scope);
|
|
1281
|
+
if (existing) {
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
const migratedState = {
|
|
1285
|
+
...legacyState,
|
|
1286
|
+
scope: config.scope,
|
|
1287
|
+
metadata: {
|
|
1288
|
+
...legacyState.metadata ?? {},
|
|
1289
|
+
migratedFrom: "0.3-file-store"
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
await target.save(migratedState);
|
|
1293
|
+
result.migratedRuns += 1;
|
|
1294
|
+
const journal = await source.listToolCalls?.(legacyState.runId, legacyState.scope) ?? [];
|
|
1295
|
+
for (const entry of journal) {
|
|
1296
|
+
await target.saveToolCall?.(migrateJournalEntry(entry, migratedState, config));
|
|
1297
|
+
result.migratedToolCalls += 1;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
cursor = page.nextCursor;
|
|
1301
|
+
} while (cursor);
|
|
1302
|
+
return result;
|
|
1303
|
+
};
|
|
1304
|
+
var openHarnessPersistence = async (config, options = {}) => {
|
|
1305
|
+
if (config.scope.namespace === "__zhivex_budget__")
|
|
1306
|
+
throw new HarnessStateConflictError("The budget ledger namespace is reserved.");
|
|
1307
|
+
await validateStateDirectory(config.workspace, config.stateDirectory);
|
|
1308
|
+
await ensurePrivateStateDirectory(config.stateDirectory);
|
|
1309
|
+
await protectStateFromGit(config.workspace, config.stateDirectory);
|
|
1310
|
+
if (config.storeBackend === "file") {
|
|
1311
|
+
const store = withBudgetLedgerStore(createFileAgentRunStore({ directory: config.stateDirectory, scope: config.scope }), createFileAgentRunStore({ directory: config.stateDirectory, scope: budgetScopeFor(config) }), config);
|
|
1312
|
+
const migration = shouldMigrateLegacyRuns(config, options.migrateLegacyFileStore) ? await migrateLegacyFileRuns(config.stateDirectory, store, config) : emptyMigration();
|
|
1313
|
+
return {
|
|
1314
|
+
backend: "file",
|
|
1315
|
+
store,
|
|
1316
|
+
memory: createFileAgentMemoryStore({
|
|
1317
|
+
directory: path2.join(config.stateDirectory, "memory"),
|
|
1318
|
+
scope: config.scope
|
|
1319
|
+
}),
|
|
1320
|
+
migration,
|
|
1321
|
+
close() {}
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
const databasePath = path2.join(config.stateDirectory, HARNESS_SQLITE_FILE);
|
|
1325
|
+
let databaseEntry;
|
|
1326
|
+
try {
|
|
1327
|
+
const handle = await open(databasePath, "wx", 384);
|
|
1328
|
+
await handle.close();
|
|
1329
|
+
databaseEntry = await lstat(databasePath);
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
if (error.code !== "EEXIST")
|
|
1332
|
+
throw error;
|
|
1333
|
+
databaseEntry = await lstat(databasePath);
|
|
1334
|
+
}
|
|
1335
|
+
if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
|
|
1336
|
+
throw new HarnessWorkspaceError(`The SQLite state path must be a real file: ${databasePath}.`);
|
|
1337
|
+
}
|
|
1338
|
+
const database = new SqliteDatabase(databasePath, { create: false, strict: true });
|
|
1339
|
+
let closed = false;
|
|
1340
|
+
const closeDatabase = () => {
|
|
1341
|
+
if (!closed) {
|
|
1342
|
+
closed = true;
|
|
1343
|
+
database.close(false);
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
try {
|
|
1347
|
+
const openedEntry = await lstat(databasePath);
|
|
1348
|
+
if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
|
|
1349
|
+
throw new HarnessWorkspaceError(`The SQLite state path changed while it was being opened: ${databasePath}.`);
|
|
1350
|
+
}
|
|
1351
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
1352
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
1353
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
1354
|
+
await chmod(databasePath, 384);
|
|
1355
|
+
const databaseLike = sqliteAdapter(database);
|
|
1356
|
+
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
1357
|
+
const memory = createSqliteAgentMemoryStore({ db: databaseLike, scope: config.scope });
|
|
1358
|
+
const migration = shouldMigrateLegacyRuns(config, options.migrateLegacyFileStore) ? await migrateLegacyFileRuns(config.stateDirectory, store, config) : emptyMigration();
|
|
1359
|
+
return {
|
|
1360
|
+
backend: "sqlite",
|
|
1361
|
+
store,
|
|
1362
|
+
memory,
|
|
1363
|
+
databasePath,
|
|
1364
|
+
migration,
|
|
1365
|
+
close: closeDatabase
|
|
1366
|
+
};
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
closeDatabase();
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1373
|
+
export { workspaceCheckpointSchema, workspaceRestoreOperationSchema, openWorkspaceCheckpointStore, withRuntimeInstruction, withFreshSystemInstructions, effectiveRuntimeBudget, createRuntimeBudget, runtimeManifest, childRuntimeSafety, createCheckpointTokenCap, createRepairProgress, createRepairController, RUNTIME_DIAGNOSTICS_KEY, runtimeCheckpointStore, tokenUsageCheckpointStore, HARNESS_SQLITE_FILE, HARNESS_OPERATIONS_SCHEMA_VERSION, TERMINAL_RUN_STATUSES, listHarnessRuns, inspectHarnessRun, cancelHarnessRun, cleanupHarnessRuns, sqliteAdapter, migrateLegacyFileRuns, openHarnessPersistence };
|