@zhivex-ai/harness 1.2.0 → 1.3.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +9 -10
- package/contracts/engine-api.json +2921 -0
- package/contracts/public-api.json +267 -1
- package/contracts/stable-api-signatures.json +43 -43
- package/dist/acp-cli.js +137 -15002
- 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-options.d.ts +2 -41
- package/dist/cli/cli-profiles.d.ts +4 -2
- package/dist/cli/cli-stream.d.ts +2 -167
- 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/run-document.d.ts +2 -91
- package/dist/cli/terminal/terminal-continuation.d.ts +2 -3
- package/dist/cli/terminal/terminal-ui.d.ts +4 -0
- package/dist/{cli-entry-c2f58zj2.js → cli-entry-ae7x44d4.js} +14969 -10731
- package/dist/{cli-entry-427869g9.js → cli-entry-d5788aay.js} +388 -47
- package/dist/cli-entry-ev76ezcd.js +656 -0
- package/dist/{cli-3jmy4h5h.js → cli-t7hrhayq.js} +918 -2223
- 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/continuation.d.ts +3 -0
- 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/run-document.d.ts +91 -0
- package/dist/client/service-events.d.ts +4 -0
- package/dist/client/stream.d.ts +167 -0
- package/dist/compat/cli-options.d.ts +53 -0
- package/dist/compat/code-support.d.ts +22 -0
- package/dist/compat/code-support.js +74 -0
- package/dist/{config-1k9t4jhh.js → config-c002f06f.js} +1 -1
- package/dist/config-j3jga5wh.js +61 -0
- 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 +6 -0
- package/dist/engine/acp.js +205 -0
- package/dist/engine/client.d.ts +6 -0
- package/dist/engine/client.js +28 -0
- 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 +73 -0
- package/dist/engine/index.js +977 -0
- 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 +7 -0
- package/dist/engine/protocol.js +182 -0
- package/dist/engine/service.d.ts +5 -0
- package/dist/engine/service.js +28 -0
- 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 +3368 -1304
- package/dist/index-1bx83sw4.js +4 -0
- package/dist/index-2gz7jv7g.js +549 -0
- package/dist/{cli-entry-b1mfvmxe.js → index-2jrdcj6b.js} +72 -115
- package/dist/index-3ah1f369.js +356 -0
- package/dist/index-42gp2hy8.js +1555 -0
- package/dist/index-4jw3tvy8.js +301 -0
- package/dist/index-8705zxpr.js +145 -0
- package/dist/index-8vm2m34s.js +2080 -0
- package/dist/index-8zzhggxv.js +477 -0
- package/dist/index-a97bezbz.js +1073 -0
- package/dist/index-gq9xagx2.js +1343 -0
- package/dist/index-h0mqx2ed.js +1373 -0
- package/dist/index-jgrx804e.js +5589 -0
- package/dist/index-qfpz5y2v.js +174 -0
- package/dist/index-t60rva31.js +1004 -0
- 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-z4e8d0ph.js +1768 -0
- package/dist/index.js +251 -18649
- 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 +19 -14504
- package/dist/{service-client-cli-yx56n0hc.js → service-client-cli-kns4mf57.js} +307 -248
- package/dist/verify-historical-migrations.js +3314 -1251
- 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 +93 -0
- package/docs/ENGINE_API.md +319 -0
- 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 +6 -0
- package/docs/STABILITY.md +9 -1
- 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 +64 -1
- package/evaluations/task-acceptance-routes.json +7 -0
- package/examples/README.md +1 -1
- package/package.json +74 -12
- package/dist/cli-entry-64pdp3k5.js +0 -17
|
@@ -0,0 +1,1555 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SqliteDatabase,
|
|
3
|
+
taskAcceptanceChecks,
|
|
4
|
+
taskAcceptanceDeliveryDiagnostic,
|
|
5
|
+
taskAcceptanceOutcome,
|
|
6
|
+
workspaceFilePathSchema
|
|
7
|
+
} from "./index-yb239ghk.js";
|
|
8
|
+
import {
|
|
9
|
+
HarnessConfigError,
|
|
10
|
+
HarnessStateConflictError,
|
|
11
|
+
HarnessWorkspaceError
|
|
12
|
+
} from "./index-gq9xagx2.js";
|
|
13
|
+
|
|
14
|
+
// src/persistence/state-directory.ts
|
|
15
|
+
import { lstat } from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
var SENSITIVE_STATE_SEGMENTS = new Set([".git", ".env", ".npmrc", "dist", "node_modules", "src"]);
|
|
18
|
+
var isInside = (root, candidate) => {
|
|
19
|
+
const relative = path.relative(root, candidate);
|
|
20
|
+
return relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
21
|
+
};
|
|
22
|
+
var validateStateDirectory = async (workspace, stateDirectory) => {
|
|
23
|
+
if (stateDirectory === workspace || stateDirectory === path.parse(stateDirectory).root) {
|
|
24
|
+
throw new HarnessWorkspaceError("The state directory cannot be the workspace or filesystem root.");
|
|
25
|
+
}
|
|
26
|
+
const insideWorkspace = isInside(workspace, stateDirectory);
|
|
27
|
+
const relativeSegments = insideWorkspace ? path.relative(workspace, stateDirectory).split(path.sep).filter(Boolean) : stateDirectory.slice(path.parse(stateDirectory).root.length).split(path.sep).filter(Boolean);
|
|
28
|
+
const sensitiveSegment = insideWorkspace ? relativeSegments.find((segment) => SENSITIVE_STATE_SEGMENTS.has(segment)) : undefined;
|
|
29
|
+
if (sensitiveSegment) {
|
|
30
|
+
throw new HarnessWorkspaceError(`The state directory is inside the protected workspace path: ${sensitiveSegment}.`);
|
|
31
|
+
}
|
|
32
|
+
if (!insideWorkspace) {
|
|
33
|
+
try {
|
|
34
|
+
const externalEntry = await lstat(stateDirectory);
|
|
35
|
+
if (externalEntry.isSymbolicLink()) {
|
|
36
|
+
throw new HarnessWorkspaceError(`The state directory must not be a symbolic link: ${stateDirectory}.`);
|
|
37
|
+
}
|
|
38
|
+
if (!externalEntry.isDirectory()) {
|
|
39
|
+
throw new HarnessWorkspaceError(`The state directory is not a directory: ${stateDirectory}.`);
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.code !== "ENOENT") {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let current = workspace;
|
|
49
|
+
for (const segment of relativeSegments) {
|
|
50
|
+
current = path.join(current, segment);
|
|
51
|
+
try {
|
|
52
|
+
const entry = await lstat(current);
|
|
53
|
+
if (entry.isSymbolicLink()) {
|
|
54
|
+
throw new HarnessWorkspaceError(`The state directory must not resolve through a symbolic link: ${current}.`);
|
|
55
|
+
}
|
|
56
|
+
if (!entry.isDirectory()) {
|
|
57
|
+
throw new HarnessWorkspaceError(`The state directory path contains a non-directory entry: ${current}.`);
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code === "ENOENT") {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/persistence/sessions.ts
|
|
69
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
70
|
+
import { chmod, lstat as lstat3, mkdir, open as open2, realpath, stat } from "node:fs/promises";
|
|
71
|
+
import path3 from "node:path";
|
|
72
|
+
import { createRedactionPolicy } from "@zhivex-ai/agents";
|
|
73
|
+
|
|
74
|
+
// src/persistence/state-gitignore.ts
|
|
75
|
+
import { constants } from "node:fs";
|
|
76
|
+
import { lstat as lstat2, open } from "node:fs/promises";
|
|
77
|
+
import path2 from "node:path";
|
|
78
|
+
var exclusion = `# Local Harness state; never commit sessions, runs, or database journals.
|
|
79
|
+
*
|
|
80
|
+
`;
|
|
81
|
+
var maximumExistingBytes = 65536;
|
|
82
|
+
var maximumProtectedBytes = maximumExistingBytes + Buffer.byteLength(exclusion) + 1;
|
|
83
|
+
async function protectStateFromGit(workspace, stateDirectory) {
|
|
84
|
+
const directory = path2.resolve(stateDirectory) === path2.join(workspace, ".zhivex-harness", "runs") ? path2.dirname(stateDirectory) : stateDirectory;
|
|
85
|
+
const filename = path2.join(directory, ".gitignore");
|
|
86
|
+
try {
|
|
87
|
+
const handle = await open(filename, constants.O_RDWR | constants.O_CREAT | constants.O_APPEND | constants.O_NOFOLLOW, 384);
|
|
88
|
+
try {
|
|
89
|
+
const info = await handle.stat();
|
|
90
|
+
const entry = await lstat2(filename);
|
|
91
|
+
if (entry.isSymbolicLink() || entry.dev !== info.dev || entry.ino !== info.ino)
|
|
92
|
+
throw new Error;
|
|
93
|
+
if (!info.isFile() || info.nlink !== 1 || process.getuid && info.uid !== process.getuid() || info.size > maximumProtectedBytes)
|
|
94
|
+
throw new Error;
|
|
95
|
+
const buffer = Buffer.alloc(maximumProtectedBytes + 1);
|
|
96
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
97
|
+
if (bytesRead > maximumProtectedBytes)
|
|
98
|
+
throw new Error;
|
|
99
|
+
const content = buffer.subarray(0, bytesRead).toString("utf8");
|
|
100
|
+
if (!content.endsWith(exclusion)) {
|
|
101
|
+
if (bytesRead > maximumExistingBytes)
|
|
102
|
+
throw new Error;
|
|
103
|
+
await handle.writeFile(`${content && !content.endsWith(`
|
|
104
|
+
`) ? `
|
|
105
|
+
` : ""}${exclusion}`);
|
|
106
|
+
}
|
|
107
|
+
const current = await lstat2(filename);
|
|
108
|
+
if (current.isSymbolicLink() || current.dev !== info.dev || current.ino !== info.ino)
|
|
109
|
+
throw new Error;
|
|
110
|
+
} finally {
|
|
111
|
+
await handle.close();
|
|
112
|
+
}
|
|
113
|
+
} catch (cause) {
|
|
114
|
+
throw new HarnessWorkspaceError("Cannot protect the local state directory with a safe .gitignore.", { cause });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/persistence/sessions.ts
|
|
119
|
+
var HARNESS_SESSION_INDEX_FILE = "operations.sqlite";
|
|
120
|
+
var HARNESS_SESSION_SCHEMA_VERSION = 1;
|
|
121
|
+
var SESSION_RUN_STATUSES = [
|
|
122
|
+
"created",
|
|
123
|
+
"running",
|
|
124
|
+
"waiting_approval",
|
|
125
|
+
"cancel_requested",
|
|
126
|
+
"completed",
|
|
127
|
+
"failed",
|
|
128
|
+
"cancelled",
|
|
129
|
+
"timed_out"
|
|
130
|
+
];
|
|
131
|
+
var TERMINAL_SESSION_RUN_STATUSES = [
|
|
132
|
+
"completed",
|
|
133
|
+
"failed",
|
|
134
|
+
"cancelled",
|
|
135
|
+
"timed_out"
|
|
136
|
+
];
|
|
137
|
+
var DEFAULT_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
|
|
138
|
+
var DEFAULT_MAX_SESSIONS = 200;
|
|
139
|
+
var DEFAULT_MAX_RUNS = 1000;
|
|
140
|
+
var DEFAULT_MAX_METADATA_BYTES = 4096;
|
|
141
|
+
var DEFAULT_MAX_INDEX_BYTES = 16 * 1024 * 1024;
|
|
142
|
+
var MAX_LIST_LIMIT = 200;
|
|
143
|
+
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,255}$/;
|
|
144
|
+
var TERMINAL_STATUSES = new Set(TERMINAL_SESSION_RUN_STATUSES);
|
|
145
|
+
var ACTIVE_STATUSES = new Set([
|
|
146
|
+
"created",
|
|
147
|
+
"running",
|
|
148
|
+
"waiting_approval",
|
|
149
|
+
"cancel_requested"
|
|
150
|
+
]);
|
|
151
|
+
var STATUS_TRANSITIONS = {
|
|
152
|
+
created: new Set(["created", "running", "waiting_approval", "completed", "failed", "cancelled", "timed_out"]),
|
|
153
|
+
running: new Set(["running", "waiting_approval", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
|
|
154
|
+
waiting_approval: new Set(["waiting_approval", "running", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
|
|
155
|
+
cancel_requested: new Set(["cancel_requested", "cancelled", "failed", "timed_out"]),
|
|
156
|
+
completed: new Set(["completed"]),
|
|
157
|
+
failed: new Set(["failed"]),
|
|
158
|
+
cancelled: new Set(["cancelled"]),
|
|
159
|
+
timed_out: new Set(["timed_out"])
|
|
160
|
+
};
|
|
161
|
+
var metadataRedaction = createRedactionPolicy({
|
|
162
|
+
includeEmails: true,
|
|
163
|
+
rules: [
|
|
164
|
+
{ name: "private-key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
|
|
165
|
+
{ name: "common-provider-key", pattern: /\b(?:sk|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{8,}\b/gi },
|
|
166
|
+
{ name: "url-credentials", pattern: /:\/\/[^\s/@:]+:[^\s/@]+@/g, replacement: "://[REDACTED]@" }
|
|
167
|
+
]
|
|
168
|
+
});
|
|
169
|
+
var byteLength = (value) => Buffer.byteLength(value, "utf8");
|
|
170
|
+
var boundedInteger = (name, value, fallback, minimum) => {
|
|
171
|
+
const resolved = value ?? fallback;
|
|
172
|
+
if (!Number.isSafeInteger(resolved) || resolved < minimum) {
|
|
173
|
+
throw new HarnessConfigError(`${name} must be an integer greater than or equal to ${minimum}.`);
|
|
174
|
+
}
|
|
175
|
+
return resolved;
|
|
176
|
+
};
|
|
177
|
+
var assertTimestamp = (name, value) => {
|
|
178
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
179
|
+
throw new HarnessConfigError(`${name} must be a non-negative integer timestamp.`);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var assertIdentifier = (name, value) => {
|
|
183
|
+
if (!ID_PATTERN.test(value)) {
|
|
184
|
+
throw new HarnessConfigError(`${name} contains unsupported characters or exceeds 256 bytes.`);
|
|
185
|
+
}
|
|
186
|
+
if (metadataRedaction.redactText(value) !== value) {
|
|
187
|
+
throw new HarnessConfigError(`${name} resembles sensitive metadata and cannot be stored in the session index.`);
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
};
|
|
191
|
+
var assertScopeSegment = (name, value) => {
|
|
192
|
+
const normalized = value.trim();
|
|
193
|
+
if (!normalized || normalized.length > 128 || /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
194
|
+
throw new HarnessConfigError(`${name} must contain 1-128 printable characters.`);
|
|
195
|
+
}
|
|
196
|
+
return normalized;
|
|
197
|
+
};
|
|
198
|
+
var normalizeTitle = (value, maxMetadataBytes) => {
|
|
199
|
+
if (value === undefined || value === null) {
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
const normalized = metadataRedaction.redactText(value).replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
|
|
203
|
+
if (!normalized) {
|
|
204
|
+
throw new HarnessConfigError("Session title must not be empty.");
|
|
205
|
+
}
|
|
206
|
+
if (byteLength(normalized) > maxMetadataBytes) {
|
|
207
|
+
throw new HarnessConfigError(`Session title exceeds the ${maxMetadataBytes}-byte metadata limit.`);
|
|
208
|
+
}
|
|
209
|
+
return normalized;
|
|
210
|
+
};
|
|
211
|
+
var normalizeScope = (scope) => {
|
|
212
|
+
const tenantId = assertScopeSegment("scope.tenantId", scope.tenantId);
|
|
213
|
+
const userId = scope.userId === undefined ? "" : assertScopeSegment("scope.userId", scope.userId);
|
|
214
|
+
const namespace = scope.namespace === undefined ? "" : assertScopeSegment("scope.namespace", scope.namespace);
|
|
215
|
+
return `${tenantId}\x00${userId}\x00${namespace}`;
|
|
216
|
+
};
|
|
217
|
+
var stableKey = (kind, value) => createHash("sha256").update(kind).update("\x00").update(value).digest("hex");
|
|
218
|
+
var sessionId = () => `ses_${randomUUID()}`;
|
|
219
|
+
var turnId = () => `turn_${randomUUID()}`;
|
|
220
|
+
var ensurePrivateDatabase = async (workspace, stateDirectory) => {
|
|
221
|
+
const requestedStateDirectory = path3.resolve(stateDirectory);
|
|
222
|
+
await validateStateDirectory(workspace, requestedStateDirectory);
|
|
223
|
+
await mkdir(requestedStateDirectory, { recursive: true, mode: 448 });
|
|
224
|
+
const directoryEntry = await lstat3(requestedStateDirectory);
|
|
225
|
+
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
|
|
226
|
+
throw new HarnessWorkspaceError(`The session state directory must be a real directory: ${requestedStateDirectory}.`);
|
|
227
|
+
}
|
|
228
|
+
await chmod(requestedStateDirectory, 448);
|
|
229
|
+
const canonicalStateDirectory = await realpath(requestedStateDirectory);
|
|
230
|
+
await protectStateFromGit(workspace, canonicalStateDirectory);
|
|
231
|
+
const databasePath = path3.join(canonicalStateDirectory, HARNESS_SESSION_INDEX_FILE);
|
|
232
|
+
let databaseEntry;
|
|
233
|
+
try {
|
|
234
|
+
const handle = await open2(databasePath, "wx", 384);
|
|
235
|
+
await handle.close();
|
|
236
|
+
databaseEntry = await lstat3(databasePath);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error.code !== "EEXIST")
|
|
239
|
+
throw error;
|
|
240
|
+
databaseEntry = await lstat3(databasePath);
|
|
241
|
+
}
|
|
242
|
+
if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
|
|
243
|
+
throw new HarnessWorkspaceError(`The session SQLite path must be a real file: ${databasePath}.`);
|
|
244
|
+
}
|
|
245
|
+
await chmod(databasePath, 384);
|
|
246
|
+
return { databasePath, databaseEntry };
|
|
247
|
+
};
|
|
248
|
+
var toRunReference = (row) => ({
|
|
249
|
+
turnId: row.turn_id,
|
|
250
|
+
...row.source_turn_id ? { sourceTurnId: row.source_turn_id } : {},
|
|
251
|
+
sequence: row.ordinal,
|
|
252
|
+
runId: row.run_id,
|
|
253
|
+
provider: row.provider,
|
|
254
|
+
model: row.model,
|
|
255
|
+
...row.role ? { role: row.role } : {},
|
|
256
|
+
status: row.status,
|
|
257
|
+
createdAt: row.created_at,
|
|
258
|
+
updatedAt: row.updated_at,
|
|
259
|
+
...row.completed_at === null ? {} : { completedAt: row.completed_at }
|
|
260
|
+
});
|
|
261
|
+
var metadataSize = (input) => Object.values(input).reduce((total, value) => total + (typeof value === "string" ? byteLength(value) : 0), 0);
|
|
262
|
+
var isTerminal = (status) => TERMINAL_STATUSES.has(status);
|
|
263
|
+
var openCliSessionStore = async (options) => {
|
|
264
|
+
const workspace = await realpath(path3.resolve(options.workspace));
|
|
265
|
+
const workspaceEntry = await stat(workspace);
|
|
266
|
+
if (!workspaceEntry.isDirectory()) {
|
|
267
|
+
throw new HarnessWorkspaceError(`The session workspace is not a directory: ${workspace}.`);
|
|
268
|
+
}
|
|
269
|
+
const scopeValue = normalizeScope(options.scope);
|
|
270
|
+
const workspaceKey = stableKey("workspace", workspace);
|
|
271
|
+
const scopeKey = stableKey("scope", scopeValue);
|
|
272
|
+
const retentionMs = boundedInteger("retentionMs", options.retentionMs, DEFAULT_RETENTION_MS, 0);
|
|
273
|
+
const maxSessions = boundedInteger("maxSessionsPerWorkspace", options.maxSessionsPerWorkspace, DEFAULT_MAX_SESSIONS, 1);
|
|
274
|
+
const maxRuns = boundedInteger("maxRunsPerSession", options.maxRunsPerSession, DEFAULT_MAX_RUNS, 1);
|
|
275
|
+
const maxMetadataBytes = boundedInteger("maxMetadataBytes", options.maxMetadataBytes, DEFAULT_MAX_METADATA_BYTES, 32);
|
|
276
|
+
const maxIndexBytes = boundedInteger("maxIndexBytes", options.maxIndexBytes, DEFAULT_MAX_INDEX_BYTES, 1024);
|
|
277
|
+
const busyTimeoutMs = boundedInteger("busyTimeoutMs", options.busyTimeoutMs, 5000, 0);
|
|
278
|
+
if (busyTimeoutMs > 5000)
|
|
279
|
+
throw new HarnessConfigError("busyTimeoutMs cannot exceed 5000.");
|
|
280
|
+
const now = options.now ?? Date.now;
|
|
281
|
+
const { databasePath, databaseEntry } = await ensurePrivateDatabase(workspace, options.stateDirectory);
|
|
282
|
+
const database = new SqliteDatabase(databasePath, { create: false, strict: true });
|
|
283
|
+
let closed = false;
|
|
284
|
+
const assertOpen = () => {
|
|
285
|
+
if (closed) {
|
|
286
|
+
throw new HarnessStateConflictError("The CLI session store is closed.");
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const transaction = (operation) => {
|
|
290
|
+
assertOpen();
|
|
291
|
+
database.exec("BEGIN IMMEDIATE");
|
|
292
|
+
try {
|
|
293
|
+
const result = operation();
|
|
294
|
+
const size = database.query(`
|
|
295
|
+
SELECT
|
|
296
|
+
COALESCE((SELECT SUM(metadata_bytes) FROM zhivex_cli_sessions WHERE workspace_key = ?1 AND scope_key = ?2), 0) +
|
|
297
|
+
COALESCE((SELECT SUM(r.metadata_bytes) FROM zhivex_cli_session_runs r JOIN zhivex_cli_sessions s ON s.session_id = r.session_id WHERE s.workspace_key = ?1 AND s.scope_key = ?2), 0)
|
|
298
|
+
AS size
|
|
299
|
+
`).get(workspaceKey, scopeKey)?.size ?? 0;
|
|
300
|
+
if (size > maxIndexBytes) {
|
|
301
|
+
throw new HarnessStateConflictError(`Session index metadata exceeds the ${maxIndexBytes}-byte limit.`);
|
|
302
|
+
}
|
|
303
|
+
database.exec("COMMIT");
|
|
304
|
+
return result;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
try {
|
|
307
|
+
database.exec("ROLLBACK");
|
|
308
|
+
} catch {}
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const ensureRevision = (row, expected) => {
|
|
313
|
+
if (expected !== undefined && row.revision !== expected) {
|
|
314
|
+
throw new HarnessStateConflictError(`Session ${row.session_id} changed concurrently (expected revision ${expected}, found ${row.revision}).`, { retryable: true });
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const selectSessionRow = (candidateSessionId, includeDeleted = false) => {
|
|
318
|
+
assertIdentifier("sessionId", candidateSessionId);
|
|
319
|
+
return database.query(`
|
|
320
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
321
|
+
created_at, updated_at, archived_at, deleted_at
|
|
322
|
+
FROM zhivex_cli_sessions
|
|
323
|
+
WHERE session_id = ?1 AND workspace_key = ?2 AND scope_key = ?3
|
|
324
|
+
${includeDeleted ? "" : "AND deleted_at IS NULL"}
|
|
325
|
+
`).get(candidateSessionId, workspaceKey, scopeKey);
|
|
326
|
+
};
|
|
327
|
+
const requireSessionRow = (candidateSessionId, includeDeleted = false) => {
|
|
328
|
+
const row = selectSessionRow(candidateSessionId, includeDeleted);
|
|
329
|
+
if (!row) {
|
|
330
|
+
throw new HarnessStateConflictError(`Session ${candidateSessionId} was not found in this workspace and scope.`);
|
|
331
|
+
}
|
|
332
|
+
return row;
|
|
333
|
+
};
|
|
334
|
+
const selectRuns = (candidateSessionId) => database.query(`
|
|
335
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
336
|
+
created_at, updated_at, completed_at
|
|
337
|
+
FROM zhivex_cli_session_runs
|
|
338
|
+
WHERE session_id = ?1
|
|
339
|
+
ORDER BY ordinal ASC
|
|
340
|
+
`).all(candidateSessionId).map(toRunReference);
|
|
341
|
+
const nextActivitySequence = () => database.query(`
|
|
342
|
+
SELECT COALESCE(MAX(activity_seq), 0) + 1 AS count
|
|
343
|
+
FROM zhivex_cli_sessions
|
|
344
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
345
|
+
`).get(workspaceKey, scopeKey)?.count ?? 1;
|
|
346
|
+
const sessionBase = (row) => ({
|
|
347
|
+
schemaVersion: HARNESS_SESSION_SCHEMA_VERSION,
|
|
348
|
+
sessionId: row.session_id,
|
|
349
|
+
workspaceKey,
|
|
350
|
+
scopeKey,
|
|
351
|
+
...row.title ? { title: row.title } : {},
|
|
352
|
+
...row.parent_session_id ? { parentSessionId: row.parent_session_id } : {},
|
|
353
|
+
...row.forked_from_turn_id ? { forkedFromTurnId: row.forked_from_turn_id } : {},
|
|
354
|
+
revision: row.revision,
|
|
355
|
+
createdAt: row.created_at,
|
|
356
|
+
updatedAt: row.updated_at,
|
|
357
|
+
...row.archived_at === null ? {} : { archivedAt: row.archived_at },
|
|
358
|
+
...row.deleted_at === null ? {} : { deletedAt: row.deleted_at }
|
|
359
|
+
});
|
|
360
|
+
const hydrate = (row) => ({
|
|
361
|
+
...sessionBase(row),
|
|
362
|
+
runs: selectRuns(row.session_id)
|
|
363
|
+
});
|
|
364
|
+
const latestRunRow = (candidateSessionId) => database.query(`
|
|
365
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
366
|
+
created_at, updated_at, completed_at
|
|
367
|
+
FROM zhivex_cli_session_runs
|
|
368
|
+
WHERE session_id = ?1
|
|
369
|
+
ORDER BY ordinal DESC
|
|
370
|
+
LIMIT 1
|
|
371
|
+
`).get(candidateSessionId);
|
|
372
|
+
const assertNoActiveRun = (candidateSessionId, action) => {
|
|
373
|
+
const latest = latestRunRow(candidateSessionId);
|
|
374
|
+
if (latest && ACTIVE_STATUSES.has(latest.status)) {
|
|
375
|
+
throw new HarnessStateConflictError(`Cannot ${action} session ${candidateSessionId} while run ${latest.run_id} is ${latest.status}.`);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const insertRun = (candidateSessionId, input, sequence, sourceTurnId) => {
|
|
379
|
+
const runId = assertIdentifier("runId", input.runId);
|
|
380
|
+
const provider = assertIdentifier("provider", input.provider);
|
|
381
|
+
const model = assertIdentifier("model", input.model);
|
|
382
|
+
const role = input.role === undefined ? null : assertIdentifier("role", input.role);
|
|
383
|
+
const status = input.status ?? "created";
|
|
384
|
+
if (!SESSION_RUN_STATUSES.includes(status)) {
|
|
385
|
+
throw new HarnessConfigError(`Unsupported session run status: ${String(status)}.`);
|
|
386
|
+
}
|
|
387
|
+
const createdAt = input.createdAt ?? now();
|
|
388
|
+
assertTimestamp("createdAt", createdAt);
|
|
389
|
+
const entryBytes = metadataSize({ runId, provider, model, role });
|
|
390
|
+
if (entryBytes > maxMetadataBytes) {
|
|
391
|
+
throw new HarnessConfigError(`Run reference metadata exceeds the ${maxMetadataBytes}-byte limit.`);
|
|
392
|
+
}
|
|
393
|
+
const candidateTurnId = turnId();
|
|
394
|
+
database.query(`
|
|
395
|
+
INSERT INTO zhivex_cli_session_runs (
|
|
396
|
+
turn_id, source_turn_id, session_id, ordinal, run_id, provider, model, role,
|
|
397
|
+
status, created_at, updated_at, completed_at, metadata_bytes
|
|
398
|
+
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10, ?11, ?12)
|
|
399
|
+
`).run(candidateTurnId, sourceTurnId ?? null, candidateSessionId, sequence, runId, provider, model, role, status, createdAt, isTerminal(status) ? createdAt : null, entryBytes);
|
|
400
|
+
};
|
|
401
|
+
try {
|
|
402
|
+
const openedEntry = await lstat3(databasePath);
|
|
403
|
+
if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
|
|
404
|
+
throw new HarnessWorkspaceError(`The session SQLite path changed while it was being opened: ${databasePath}.`);
|
|
405
|
+
}
|
|
406
|
+
database.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
|
|
407
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
408
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
409
|
+
database.exec(`
|
|
410
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_session_schema (
|
|
411
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
412
|
+
version INTEGER NOT NULL
|
|
413
|
+
);
|
|
414
|
+
INSERT OR IGNORE INTO zhivex_cli_session_schema (singleton, version)
|
|
415
|
+
VALUES (1, ${HARNESS_SESSION_SCHEMA_VERSION});
|
|
416
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_sessions (
|
|
417
|
+
session_id TEXT PRIMARY KEY,
|
|
418
|
+
workspace_key TEXT NOT NULL,
|
|
419
|
+
scope_key TEXT NOT NULL,
|
|
420
|
+
title TEXT,
|
|
421
|
+
parent_session_id TEXT,
|
|
422
|
+
forked_from_turn_id TEXT,
|
|
423
|
+
revision INTEGER NOT NULL DEFAULT 0,
|
|
424
|
+
activity_seq INTEGER NOT NULL,
|
|
425
|
+
created_at INTEGER NOT NULL,
|
|
426
|
+
updated_at INTEGER NOT NULL,
|
|
427
|
+
archived_at INTEGER,
|
|
428
|
+
deleted_at INTEGER,
|
|
429
|
+
metadata_bytes INTEGER NOT NULL,
|
|
430
|
+
CHECK (length(session_id) <= 260),
|
|
431
|
+
CHECK (metadata_bytes >= 0)
|
|
432
|
+
);
|
|
433
|
+
CREATE INDEX IF NOT EXISTS zhivex_cli_sessions_latest
|
|
434
|
+
ON zhivex_cli_sessions (workspace_key, scope_key, deleted_at, archived_at, activity_seq DESC);
|
|
435
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_session_runs (
|
|
436
|
+
turn_id TEXT PRIMARY KEY,
|
|
437
|
+
source_turn_id TEXT,
|
|
438
|
+
session_id TEXT NOT NULL REFERENCES zhivex_cli_sessions(session_id) ON DELETE CASCADE,
|
|
439
|
+
ordinal INTEGER NOT NULL,
|
|
440
|
+
run_id TEXT NOT NULL,
|
|
441
|
+
provider TEXT NOT NULL,
|
|
442
|
+
model TEXT NOT NULL,
|
|
443
|
+
role TEXT,
|
|
444
|
+
status TEXT NOT NULL CHECK (status IN ('created', 'running', 'waiting_approval', 'cancel_requested', 'completed', 'failed', 'cancelled', 'timed_out')),
|
|
445
|
+
created_at INTEGER NOT NULL,
|
|
446
|
+
updated_at INTEGER NOT NULL,
|
|
447
|
+
completed_at INTEGER,
|
|
448
|
+
metadata_bytes INTEGER NOT NULL,
|
|
449
|
+
UNIQUE (session_id, ordinal),
|
|
450
|
+
UNIQUE (session_id, run_id),
|
|
451
|
+
CHECK (ordinal >= 0),
|
|
452
|
+
CHECK (metadata_bytes >= 0)
|
|
453
|
+
);
|
|
454
|
+
CREATE INDEX IF NOT EXISTS zhivex_cli_session_runs_latest
|
|
455
|
+
ON zhivex_cli_session_runs (session_id, ordinal DESC);
|
|
456
|
+
`);
|
|
457
|
+
const schemaVersion = database.query("SELECT version FROM zhivex_cli_session_schema WHERE singleton = 1").get()?.version;
|
|
458
|
+
if (schemaVersion !== HARNESS_SESSION_SCHEMA_VERSION) {
|
|
459
|
+
throw new HarnessStateConflictError(`Unsupported CLI session schema version: ${String(schemaVersion)}.`);
|
|
460
|
+
}
|
|
461
|
+
await chmod(databasePath, 384);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
database.close(false);
|
|
464
|
+
throw error;
|
|
465
|
+
}
|
|
466
|
+
const prune = async (input = {}) => transaction(() => {
|
|
467
|
+
const before = input.before ?? Math.max(0, now() - retentionMs);
|
|
468
|
+
assertTimestamp("before", before);
|
|
469
|
+
const deletedSessions = database.query(`
|
|
470
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
471
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
472
|
+
AND updated_at < ?3
|
|
473
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
474
|
+
`).get(workspaceKey, scopeKey, before)?.count ?? 0;
|
|
475
|
+
database.query(`
|
|
476
|
+
DELETE FROM zhivex_cli_sessions
|
|
477
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
478
|
+
AND updated_at < ?3
|
|
479
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
480
|
+
`).run(workspaceKey, scopeKey, before);
|
|
481
|
+
return { before, deletedSessions };
|
|
482
|
+
});
|
|
483
|
+
const store = {
|
|
484
|
+
databasePath,
|
|
485
|
+
workspaceKey,
|
|
486
|
+
scopeKey,
|
|
487
|
+
async create(input = {}) {
|
|
488
|
+
return transaction(() => {
|
|
489
|
+
const cutoff = Math.max(0, now() - retentionMs);
|
|
490
|
+
database.query(`
|
|
491
|
+
DELETE FROM zhivex_cli_sessions
|
|
492
|
+
WHERE workspace_key = ?1 AND scope_key = ?2 AND updated_at < ?3
|
|
493
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
494
|
+
`).run(workspaceKey, scopeKey, cutoff);
|
|
495
|
+
const count = database.query(`
|
|
496
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
497
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
498
|
+
`).get(workspaceKey, scopeKey)?.count ?? 0;
|
|
499
|
+
if (count >= maxSessions) {
|
|
500
|
+
throw new HarnessStateConflictError(`Session limit reached for this workspace and scope (${maxSessions}). Archive and prune an older session.`);
|
|
501
|
+
}
|
|
502
|
+
const createdAt = now();
|
|
503
|
+
assertTimestamp("now", createdAt);
|
|
504
|
+
const title = normalizeTitle(input.title, maxMetadataBytes) ?? null;
|
|
505
|
+
const candidateSessionId = sessionId();
|
|
506
|
+
const activitySequence = nextActivitySequence();
|
|
507
|
+
database.query(`
|
|
508
|
+
INSERT INTO zhivex_cli_sessions (
|
|
509
|
+
session_id, workspace_key, scope_key, title, revision, created_at,
|
|
510
|
+
updated_at, activity_seq, metadata_bytes
|
|
511
|
+
) VALUES (?1, ?2, ?3, ?4, 0, ?5, ?5, ?6, ?7)
|
|
512
|
+
`).run(candidateSessionId, workspaceKey, scopeKey, title, createdAt, activitySequence, metadataSize({ title }));
|
|
513
|
+
if (input.initialRun) {
|
|
514
|
+
insertRun(candidateSessionId, input.initialRun, 0);
|
|
515
|
+
}
|
|
516
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
517
|
+
});
|
|
518
|
+
},
|
|
519
|
+
async get(candidateSessionId, input = {}) {
|
|
520
|
+
assertOpen();
|
|
521
|
+
const row = selectSessionRow(candidateSessionId, input.includeDeleted ?? false);
|
|
522
|
+
return row ? hydrate(row) : undefined;
|
|
523
|
+
},
|
|
524
|
+
async list(query = {}) {
|
|
525
|
+
assertOpen();
|
|
526
|
+
const limit = boundedInteger("limit", query.limit, 50, 1);
|
|
527
|
+
if (limit > MAX_LIST_LIMIT) {
|
|
528
|
+
throw new HarnessConfigError(`limit cannot exceed ${MAX_LIST_LIMIT}.`);
|
|
529
|
+
}
|
|
530
|
+
const search = query.search?.trim() ?? "";
|
|
531
|
+
if (search.length > 256)
|
|
532
|
+
throw new HarnessConfigError("Session search exceeds 256 characters.");
|
|
533
|
+
const rows = database.query(`
|
|
534
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
535
|
+
created_at, updated_at, archived_at, deleted_at
|
|
536
|
+
FROM zhivex_cli_sessions
|
|
537
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
538
|
+
AND (?4 = '' OR instr(lower(COALESCE(title, '')), lower(?4)) > 0 OR instr(lower(session_id), lower(?4)) > 0)
|
|
539
|
+
${query.includeArchived ? "" : "AND archived_at IS NULL"}
|
|
540
|
+
${query.includeDeleted ? "" : "AND deleted_at IS NULL"}
|
|
541
|
+
ORDER BY activity_seq DESC
|
|
542
|
+
LIMIT ?3
|
|
543
|
+
`).all(workspaceKey, scopeKey, limit, search);
|
|
544
|
+
return rows.map((row) => {
|
|
545
|
+
const latest = latestRunRow(row.session_id);
|
|
546
|
+
const runCount = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(row.session_id)?.count ?? 0;
|
|
547
|
+
return {
|
|
548
|
+
...sessionBase(row),
|
|
549
|
+
runCount,
|
|
550
|
+
...latest ? { latestRun: toRunReference(latest) } : {}
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
},
|
|
554
|
+
async latest(input = {}) {
|
|
555
|
+
assertOpen();
|
|
556
|
+
const row = database.query(`
|
|
557
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
558
|
+
created_at, updated_at, archived_at, deleted_at
|
|
559
|
+
FROM zhivex_cli_sessions
|
|
560
|
+
WHERE workspace_key = ?1 AND scope_key = ?2 AND deleted_at IS NULL
|
|
561
|
+
${input.includeArchived ? "" : "AND archived_at IS NULL"}
|
|
562
|
+
ORDER BY activity_seq DESC
|
|
563
|
+
LIMIT 1
|
|
564
|
+
`).get(workspaceKey, scopeKey);
|
|
565
|
+
return row ? hydrate(row) : undefined;
|
|
566
|
+
},
|
|
567
|
+
async update(candidateSessionId, input) {
|
|
568
|
+
return transaction(() => {
|
|
569
|
+
const row = requireSessionRow(candidateSessionId);
|
|
570
|
+
ensureRevision(row, input.expectedRevision);
|
|
571
|
+
if (input.archived === true) {
|
|
572
|
+
assertNoActiveRun(candidateSessionId, "archive");
|
|
573
|
+
}
|
|
574
|
+
const title = input.title === undefined ? row.title : normalizeTitle(input.title, maxMetadataBytes) ?? null;
|
|
575
|
+
const updatedAt = now();
|
|
576
|
+
assertTimestamp("now", updatedAt);
|
|
577
|
+
const archivedAt = input.archived === undefined ? row.archived_at : input.archived ? updatedAt : null;
|
|
578
|
+
const activitySequence = nextActivitySequence();
|
|
579
|
+
database.query(`
|
|
580
|
+
UPDATE zhivex_cli_sessions
|
|
581
|
+
SET title = ?1, archived_at = ?2, updated_at = ?3, revision = revision + 1,
|
|
582
|
+
metadata_bytes = ?4, activity_seq = ?5
|
|
583
|
+
WHERE session_id = ?6 AND workspace_key = ?7 AND scope_key = ?8 AND revision = ?9
|
|
584
|
+
`).run(title, archivedAt, updatedAt, metadataSize({ title }), activitySequence, candidateSessionId, workspaceKey, scopeKey, row.revision);
|
|
585
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
586
|
+
});
|
|
587
|
+
},
|
|
588
|
+
async rename(candidateSessionId, title, input = {}) {
|
|
589
|
+
return store.update(candidateSessionId, {
|
|
590
|
+
title,
|
|
591
|
+
...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
|
|
592
|
+
});
|
|
593
|
+
},
|
|
594
|
+
async appendRun(candidateSessionId, input, appendOptions = {}) {
|
|
595
|
+
return transaction(() => {
|
|
596
|
+
const row = requireSessionRow(candidateSessionId);
|
|
597
|
+
ensureRevision(row, appendOptions.expectedRevision);
|
|
598
|
+
if (row.archived_at !== null) {
|
|
599
|
+
throw new HarnessStateConflictError(`Cannot append a run to archived session ${candidateSessionId}.`);
|
|
600
|
+
}
|
|
601
|
+
const existing = database.query(`
|
|
602
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
603
|
+
created_at, updated_at, completed_at
|
|
604
|
+
FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
|
|
605
|
+
`).get(candidateSessionId, input.runId);
|
|
606
|
+
if (existing) {
|
|
607
|
+
const requestedStatus = input.status ?? "created";
|
|
608
|
+
if (existing.provider === input.provider && existing.model === input.model && (existing.role ?? undefined) === input.role && existing.status === requestedStatus) {
|
|
609
|
+
return hydrate(row);
|
|
610
|
+
}
|
|
611
|
+
throw new HarnessStateConflictError(`Run ${input.runId} is already immutably bound to ${existing.provider}/${existing.model} in this session.`);
|
|
612
|
+
}
|
|
613
|
+
assertNoActiveRun(candidateSessionId, "start another run in");
|
|
614
|
+
const count = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(candidateSessionId)?.count ?? 0;
|
|
615
|
+
if (count >= maxRuns) {
|
|
616
|
+
throw new HarnessStateConflictError(`Run-reference limit reached for session ${candidateSessionId} (${maxRuns}).`);
|
|
617
|
+
}
|
|
618
|
+
insertRun(candidateSessionId, input, count);
|
|
619
|
+
const updatedAt = now();
|
|
620
|
+
assertTimestamp("now", updatedAt);
|
|
621
|
+
const activitySequence = nextActivitySequence();
|
|
622
|
+
database.query(`
|
|
623
|
+
UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
624
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
625
|
+
`).run(updatedAt, activitySequence, candidateSessionId, row.revision);
|
|
626
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
627
|
+
});
|
|
628
|
+
},
|
|
629
|
+
async updateRun(candidateSessionId, runId, input) {
|
|
630
|
+
return transaction(() => {
|
|
631
|
+
const session = requireSessionRow(candidateSessionId);
|
|
632
|
+
ensureRevision(session, input.expectedSessionRevision);
|
|
633
|
+
assertIdentifier("runId", runId);
|
|
634
|
+
const run = database.query(`
|
|
635
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
636
|
+
created_at, updated_at, completed_at
|
|
637
|
+
FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
|
|
638
|
+
`).get(candidateSessionId, runId);
|
|
639
|
+
if (!run) {
|
|
640
|
+
throw new HarnessStateConflictError(`Run ${runId} is not linked to session ${candidateSessionId}.`);
|
|
641
|
+
}
|
|
642
|
+
if (!STATUS_TRANSITIONS[run.status].has(input.status)) {
|
|
643
|
+
throw new HarnessStateConflictError(`Run ${runId} cannot transition from ${run.status} to ${input.status}.`);
|
|
644
|
+
}
|
|
645
|
+
const updatedAt = input.updatedAt ?? now();
|
|
646
|
+
assertTimestamp("updatedAt", updatedAt);
|
|
647
|
+
if (updatedAt < run.updated_at) {
|
|
648
|
+
throw new HarnessStateConflictError(`Run ${runId} update timestamp cannot move backwards.`);
|
|
649
|
+
}
|
|
650
|
+
const completedAt = isTerminal(input.status) ? input.completedAt ?? updatedAt : null;
|
|
651
|
+
if (completedAt !== null) {
|
|
652
|
+
assertTimestamp("completedAt", completedAt);
|
|
653
|
+
if (completedAt < run.created_at || completedAt > updatedAt) {
|
|
654
|
+
throw new HarnessConfigError("completedAt must be between the run creation and update timestamps.");
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
database.query(`
|
|
658
|
+
UPDATE zhivex_cli_session_runs
|
|
659
|
+
SET status = ?1, updated_at = ?2, completed_at = ?3
|
|
660
|
+
WHERE session_id = ?4 AND run_id = ?5
|
|
661
|
+
`).run(input.status, updatedAt, completedAt, candidateSessionId, runId);
|
|
662
|
+
const activitySequence = nextActivitySequence();
|
|
663
|
+
database.query(`
|
|
664
|
+
UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
665
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
666
|
+
`).run(updatedAt, activitySequence, candidateSessionId, session.revision);
|
|
667
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
668
|
+
});
|
|
669
|
+
},
|
|
670
|
+
async fork(candidateSessionId, input = {}) {
|
|
671
|
+
return transaction(() => {
|
|
672
|
+
const source = requireSessionRow(candidateSessionId);
|
|
673
|
+
ensureRevision(source, input.expectedRevision);
|
|
674
|
+
const sourceRuns = selectRuns(candidateSessionId);
|
|
675
|
+
let forkRuns = sourceRuns;
|
|
676
|
+
let forkedFromTurnId;
|
|
677
|
+
if (input.atTurnId) {
|
|
678
|
+
const index = sourceRuns.findIndex((run) => run.turnId === input.atTurnId);
|
|
679
|
+
if (index < 0) {
|
|
680
|
+
throw new HarnessStateConflictError(`Turn ${input.atTurnId} is not part of session ${candidateSessionId}.`);
|
|
681
|
+
}
|
|
682
|
+
forkRuns = sourceRuns.slice(0, index + 1);
|
|
683
|
+
forkedFromTurnId = input.atTurnId;
|
|
684
|
+
} else if (sourceRuns.length > 0) {
|
|
685
|
+
forkedFromTurnId = sourceRuns.at(-1).turnId;
|
|
686
|
+
}
|
|
687
|
+
const branchPoint = forkRuns.at(-1);
|
|
688
|
+
if (branchPoint && !isTerminal(branchPoint.status)) {
|
|
689
|
+
throw new HarnessStateConflictError(`Cannot fork from non-terminal run ${branchPoint.runId} (${branchPoint.status}).`);
|
|
690
|
+
}
|
|
691
|
+
if (forkRuns.length > maxRuns) {
|
|
692
|
+
throw new HarnessStateConflictError(`Fork history exceeds the ${maxRuns}-run limit.`);
|
|
693
|
+
}
|
|
694
|
+
const count = database.query(`
|
|
695
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
696
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
697
|
+
`).get(workspaceKey, scopeKey)?.count ?? 0;
|
|
698
|
+
if (count >= maxSessions) {
|
|
699
|
+
throw new HarnessStateConflictError(`Session limit reached for this workspace and scope (${maxSessions}).`);
|
|
700
|
+
}
|
|
701
|
+
const createdAt = now();
|
|
702
|
+
assertTimestamp("now", createdAt);
|
|
703
|
+
const title = normalizeTitle(input.title, maxMetadataBytes) ?? source.title;
|
|
704
|
+
const forkSessionId = sessionId();
|
|
705
|
+
const activitySequence = nextActivitySequence();
|
|
706
|
+
database.query(`
|
|
707
|
+
INSERT INTO zhivex_cli_sessions (
|
|
708
|
+
session_id, workspace_key, scope_key, title, parent_session_id,
|
|
709
|
+
forked_from_turn_id, revision, created_at, updated_at, activity_seq, metadata_bytes
|
|
710
|
+
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?8, ?9)
|
|
711
|
+
`).run(forkSessionId, workspaceKey, scopeKey, title, candidateSessionId, forkedFromTurnId ?? null, createdAt, activitySequence, metadataSize({ title }));
|
|
712
|
+
for (const [index, run] of forkRuns.entries()) {
|
|
713
|
+
insertRun(forkSessionId, {
|
|
714
|
+
runId: run.runId,
|
|
715
|
+
provider: run.provider,
|
|
716
|
+
model: run.model,
|
|
717
|
+
...run.role ? { role: run.role } : {},
|
|
718
|
+
status: run.status,
|
|
719
|
+
createdAt: run.createdAt
|
|
720
|
+
}, index, run.turnId);
|
|
721
|
+
database.query(`
|
|
722
|
+
UPDATE zhivex_cli_session_runs
|
|
723
|
+
SET updated_at = ?1, completed_at = ?2
|
|
724
|
+
WHERE session_id = ?3 AND ordinal = ?4
|
|
725
|
+
`).run(run.updatedAt, run.completedAt ?? null, forkSessionId, index);
|
|
726
|
+
}
|
|
727
|
+
return hydrate(requireSessionRow(forkSessionId));
|
|
728
|
+
});
|
|
729
|
+
},
|
|
730
|
+
async archive(candidateSessionId, input = {}) {
|
|
731
|
+
return store.update(candidateSessionId, {
|
|
732
|
+
archived: true,
|
|
733
|
+
...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
|
|
734
|
+
});
|
|
735
|
+
},
|
|
736
|
+
async delete(candidateSessionId, input = {}) {
|
|
737
|
+
return transaction(() => {
|
|
738
|
+
const row = requireSessionRow(candidateSessionId);
|
|
739
|
+
ensureRevision(row, input.expectedRevision);
|
|
740
|
+
assertNoActiveRun(candidateSessionId, "delete");
|
|
741
|
+
const deletedAt = now();
|
|
742
|
+
assertTimestamp("now", deletedAt);
|
|
743
|
+
const activitySequence = nextActivitySequence();
|
|
744
|
+
database.query(`
|
|
745
|
+
UPDATE zhivex_cli_sessions
|
|
746
|
+
SET deleted_at = ?1, updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
747
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
748
|
+
`).run(deletedAt, activitySequence, candidateSessionId, row.revision);
|
|
749
|
+
return hydrate(requireSessionRow(candidateSessionId, true));
|
|
750
|
+
});
|
|
751
|
+
},
|
|
752
|
+
prune,
|
|
753
|
+
close() {
|
|
754
|
+
if (!closed) {
|
|
755
|
+
closed = true;
|
|
756
|
+
database.close(false);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
return store;
|
|
761
|
+
};
|
|
762
|
+
var openSessionStore = openCliSessionStore;
|
|
763
|
+
|
|
764
|
+
// src/runtime/runtime-diagnostics.ts
|
|
765
|
+
import { z } from "zod";
|
|
766
|
+
import { createRedactionPolicy as createRedactionPolicy2 } from "@zhivex-ai/agents";
|
|
767
|
+
var count = z.number().int().nonnegative().finite();
|
|
768
|
+
var digest = z.string().regex(/^sha256:[a-f0-9]{64}$/).nullable();
|
|
769
|
+
var diagnosticsSchema = z.object({
|
|
770
|
+
schemaVersion: z.literal(2),
|
|
771
|
+
requireVerifiedDelivery: z.boolean(),
|
|
772
|
+
budget: z.object({
|
|
773
|
+
inputTokens: count,
|
|
774
|
+
outputTokens: count,
|
|
775
|
+
cachedInputTokens: count,
|
|
776
|
+
modelCalls: count,
|
|
777
|
+
usageComplete: z.boolean(),
|
|
778
|
+
inFlight: z.boolean(),
|
|
779
|
+
stopReason: z.enum(["USAGE_UNAVAILABLE", "WORK_TOKEN_BUDGET", "INPUT_TOKEN_BUDGET", "OUTPUT_TOKEN_BUDGET"]).nullable(),
|
|
780
|
+
reservedInputTokens: count,
|
|
781
|
+
reservedOutputTokens: count,
|
|
782
|
+
predictedInputTokens: count,
|
|
783
|
+
outputCapApplied: z.boolean().default(false)
|
|
784
|
+
}),
|
|
785
|
+
phase: z.enum(["explore", "reproduce", "candidate", "verify", "recover", "delivered", "incomplete"]),
|
|
786
|
+
candidate: digest,
|
|
787
|
+
revision: count,
|
|
788
|
+
checks: z.array(z.object({
|
|
789
|
+
commandId: z.string().max(128),
|
|
790
|
+
purpose: z.string().max(500),
|
|
791
|
+
argvDigest: digest,
|
|
792
|
+
candidate: digest,
|
|
793
|
+
exitCode: z.number().int(),
|
|
794
|
+
verified: z.boolean()
|
|
795
|
+
})).max(8),
|
|
796
|
+
contextMeasurements: z.array(z.object({
|
|
797
|
+
systemCharacters: count,
|
|
798
|
+
userCharacters: count,
|
|
799
|
+
assistantCharacters: count,
|
|
800
|
+
toolResultCharacters: count,
|
|
801
|
+
otherMessageCharacters: count,
|
|
802
|
+
toolDefinitionCharacters: count,
|
|
803
|
+
unmeasuredToolDefinitions: count
|
|
804
|
+
})).max(128),
|
|
805
|
+
modelTimings: z.array(z.object({ durationMs: z.number().nonnegative().finite(), firstTokenMs: z.number().nonnegative().finite().nullable(), completed: z.boolean() })).max(128),
|
|
806
|
+
omittedContextMeasurements: count
|
|
807
|
+
});
|
|
808
|
+
var manifestSchema = z.object({
|
|
809
|
+
schemaVersion: z.literal(2),
|
|
810
|
+
policyVersion: z.literal("assistant-v3-durable-closure"),
|
|
811
|
+
role: z.string().max(128),
|
|
812
|
+
requireVerifiedDelivery: z.boolean(),
|
|
813
|
+
backend: z.enum(["none", "oci"]),
|
|
814
|
+
tools: z.array(z.string().max(128)).max(1024),
|
|
815
|
+
budget: z.object({ unlimitedTokens: z.boolean().optional(), maxSteps: count, maxToolCalls: count, maxToolErrors: count, maxInputTokens: count, maxOutputTokens: count, includeChildRuns: z.boolean() }),
|
|
816
|
+
timeoutMs: count,
|
|
817
|
+
closureController: z.boolean(),
|
|
818
|
+
contextEnabled: z.boolean()
|
|
819
|
+
});
|
|
820
|
+
var redact = createRedactionPolicy2({ includeEmails: true });
|
|
821
|
+
var inspectRuntimeDiagnostics = (value) => {
|
|
822
|
+
const parsed = diagnosticsSchema.safeParse(value);
|
|
823
|
+
if (!parsed.success)
|
|
824
|
+
return null;
|
|
825
|
+
return { ...parsed.data, checks: parsed.data.checks.map((check) => ({
|
|
826
|
+
...check,
|
|
827
|
+
commandId: redact.redactText(check.commandId),
|
|
828
|
+
purpose: redact.redactText(check.purpose)
|
|
829
|
+
})) };
|
|
830
|
+
};
|
|
831
|
+
var inspectRuntimeManifest = (value) => {
|
|
832
|
+
const parsed = manifestSchema.safeParse(value);
|
|
833
|
+
if (!parsed.success)
|
|
834
|
+
return null;
|
|
835
|
+
return { ...parsed.data, role: redact.redactText(parsed.data.role), tools: parsed.data.tools.map((name) => redact.redactText(name)) };
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
// src/runtime/tool-execution.ts
|
|
839
|
+
var harnessToolExecution = {
|
|
840
|
+
parallel: true,
|
|
841
|
+
independentOnly: true,
|
|
842
|
+
maxConcurrency: 4,
|
|
843
|
+
stopOnError: false,
|
|
844
|
+
validationErrorMode: "tool-result",
|
|
845
|
+
unknownToolMode: "tool-result"
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// src/runtime/usage-ledger.ts
|
|
849
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
850
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
851
|
+
import { lstat as lstat4 } from "node:fs/promises";
|
|
852
|
+
import { z as z4 } from "zod";
|
|
853
|
+
import { wrapLanguageModel } from "@zhivex-ai/core";
|
|
854
|
+
|
|
855
|
+
// src/runtime/model-budget.ts
|
|
856
|
+
import { ProviderToolCallError } from "@zhivex-ai/core/provider";
|
|
857
|
+
import { z as z3 } from "zod";
|
|
858
|
+
|
|
859
|
+
// src/context/context-metrics.ts
|
|
860
|
+
import { z as z2 } from "zod";
|
|
861
|
+
var schemaJson = new WeakMap;
|
|
862
|
+
var serializeSchema = (schema) => {
|
|
863
|
+
let serialized = schemaJson.get(schema);
|
|
864
|
+
if (serialized === undefined) {
|
|
865
|
+
serialized = JSON.stringify(z2.toJSONSchema(schema));
|
|
866
|
+
schemaJson.set(schema, serialized);
|
|
867
|
+
}
|
|
868
|
+
return serialized;
|
|
869
|
+
};
|
|
870
|
+
var estimateContextTokens = (counts) => {
|
|
871
|
+
if (counts.unmeasuredToolDefinitions)
|
|
872
|
+
throw new Error("CONTEXT_ESTIMATE_UNAVAILABLE");
|
|
873
|
+
return Math.ceil((counts.systemCharacters + counts.userCharacters + counts.assistantCharacters + counts.toolResultCharacters + counts.otherMessageCharacters + counts.toolDefinitionCharacters) / 3) + 64;
|
|
874
|
+
};
|
|
875
|
+
var measureContext = (input) => {
|
|
876
|
+
const counts = {
|
|
877
|
+
systemCharacters: 0,
|
|
878
|
+
userCharacters: 0,
|
|
879
|
+
assistantCharacters: 0,
|
|
880
|
+
toolResultCharacters: 0,
|
|
881
|
+
otherMessageCharacters: 0,
|
|
882
|
+
toolDefinitionCharacters: 0,
|
|
883
|
+
unmeasuredToolDefinitions: 0
|
|
884
|
+
};
|
|
885
|
+
for (const message of input.messages) {
|
|
886
|
+
const size = JSON.stringify(message).length;
|
|
887
|
+
if (message.role === "system")
|
|
888
|
+
counts.systemCharacters += size;
|
|
889
|
+
else if (message.role === "user")
|
|
890
|
+
counts.userCharacters += size;
|
|
891
|
+
else if (message.role === "assistant")
|
|
892
|
+
counts.assistantCharacters += size;
|
|
893
|
+
else if (message.role === "tool")
|
|
894
|
+
counts.toolResultCharacters += size;
|
|
895
|
+
else
|
|
896
|
+
counts.otherMessageCharacters += size;
|
|
897
|
+
}
|
|
898
|
+
for (const [name, definition] of Object.entries(input.tools ?? {})) {
|
|
899
|
+
try {
|
|
900
|
+
if (!("schema" in definition)) {
|
|
901
|
+
counts.unmeasuredToolDefinitions++;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const header = JSON.stringify({ name, description: definition.description });
|
|
905
|
+
counts.toolDefinitionCharacters += header.length - 1 + ',"schema":'.length + serializeSchema(definition.schema).length + 1;
|
|
906
|
+
} catch {
|
|
907
|
+
counts.unmeasuredToolDefinitions++;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return counts;
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
// src/runtime/model-budget.ts
|
|
914
|
+
var MODEL_BUDGET_KEY = "zhivexTransportBudget";
|
|
915
|
+
var savedBudget = z3.object({
|
|
916
|
+
inputTokens: z3.number().int().nonnegative(),
|
|
917
|
+
outputTokens: z3.number().int().nonnegative(),
|
|
918
|
+
cachedInputTokens: z3.number().int().nonnegative(),
|
|
919
|
+
modelCalls: z3.number().int().nonnegative(),
|
|
920
|
+
usageComplete: z3.boolean(),
|
|
921
|
+
inFlight: z3.boolean().default(false)
|
|
922
|
+
});
|
|
923
|
+
var estimateRequestTokens = (input) => estimateContextTokens(measureContext(input));
|
|
924
|
+
var workBudgetReached = (input, stats, limits) => stats.inputTokens + estimateRequestTokens(input) > limits.inputTokens - stats.reservedInputTokens || stats.outputTokens >= limits.outputTokens - stats.reservedOutputTokens;
|
|
925
|
+
var createModelBudget = (limits, options = {}) => {
|
|
926
|
+
const restored = options.saved === undefined ? undefined : savedBudget.parse(options.saved);
|
|
927
|
+
const reserve = options.reserveFraction ?? 0.3;
|
|
928
|
+
if (!Number.isFinite(reserve) || reserve < 0 || reserve >= 1)
|
|
929
|
+
throw new Error("Invalid closure reserve.");
|
|
930
|
+
const prior = inspectRuntimeDiagnostics(options.diagnostics);
|
|
931
|
+
const contextMetrics = prior?.contextMeasurements ?? [];
|
|
932
|
+
let omittedContextMeasurements = prior?.omittedContextMeasurements ?? 0;
|
|
933
|
+
const stats = {
|
|
934
|
+
inputTokens: 0,
|
|
935
|
+
outputTokens: 0,
|
|
936
|
+
cachedInputTokens: 0,
|
|
937
|
+
modelCalls: 0,
|
|
938
|
+
usageComplete: true,
|
|
939
|
+
inFlight: false,
|
|
940
|
+
...restored,
|
|
941
|
+
stopReason: null,
|
|
942
|
+
reservedInputTokens: Number.isFinite(limits.inputTokens) ? Math.ceil(limits.inputTokens * reserve) : 0,
|
|
943
|
+
reservedOutputTokens: Number.isFinite(limits.outputTokens) ? Math.ceil(limits.outputTokens * reserve) : 0,
|
|
944
|
+
predictedInputTokens: 0,
|
|
945
|
+
outputCapApplied: false
|
|
946
|
+
};
|
|
947
|
+
if (restored?.inFlight)
|
|
948
|
+
stats.usageComplete = false;
|
|
949
|
+
const modelTimings = prior?.modelTimings ?? [];
|
|
950
|
+
const snapshot = () => ({
|
|
951
|
+
inputTokens: stats.inputTokens,
|
|
952
|
+
outputTokens: stats.outputTokens,
|
|
953
|
+
cachedInputTokens: stats.cachedInputTokens,
|
|
954
|
+
modelCalls: stats.modelCalls,
|
|
955
|
+
usageComplete: stats.usageComplete,
|
|
956
|
+
inFlight: stats.inFlight
|
|
957
|
+
});
|
|
958
|
+
const before = (input, provider) => {
|
|
959
|
+
input.abortSignal?.throwIfAborted();
|
|
960
|
+
const measured = measureContext(input);
|
|
961
|
+
stats.predictedInputTokens = estimateContextTokens(measured);
|
|
962
|
+
const closure = options.closure?.() ?? false;
|
|
963
|
+
const inputCeiling = limits.inputTokens - (closure ? 0 : stats.reservedInputTokens);
|
|
964
|
+
const outputCeiling = limits.outputTokens - (closure ? 0 : stats.reservedOutputTokens);
|
|
965
|
+
stats.stopReason = !stats.usageComplete ? "USAGE_UNAVAILABLE" : stats.inputTokens + stats.predictedInputTokens > inputCeiling ? closure ? "INPUT_TOKEN_BUDGET" : "WORK_TOKEN_BUDGET" : stats.outputTokens >= outputCeiling ? closure ? "OUTPUT_TOKEN_BUDGET" : "WORK_TOKEN_BUDGET" : null;
|
|
966
|
+
if (stats.stopReason)
|
|
967
|
+
throw new Error(stats.stopReason);
|
|
968
|
+
const capSupported = provider !== "qwen" || input.providerOptions?.apiMode === "chat" || input.providerOptions?.apiMode !== "responses" && (input.maxTokens !== undefined || input.reasoning?.budgetTokens !== undefined);
|
|
969
|
+
const remainingOutput = outputCeiling - stats.outputTokens;
|
|
970
|
+
const outputCap = Math.min(input.maxTokens ?? remainingOutput, remainingOutput);
|
|
971
|
+
stats.outputCapApplied = capSupported && Number.isFinite(outputCap);
|
|
972
|
+
if (stats.outputCapApplied)
|
|
973
|
+
input.maxTokens = Math.max(1, outputCap);
|
|
974
|
+
stats.modelCalls++;
|
|
975
|
+
stats.inFlight = true;
|
|
976
|
+
if (contextMetrics.length < 128)
|
|
977
|
+
contextMetrics.push(measured);
|
|
978
|
+
else
|
|
979
|
+
omittedContextMeasurements++;
|
|
980
|
+
};
|
|
981
|
+
const record = (usage) => {
|
|
982
|
+
stats.inFlight = false;
|
|
983
|
+
if (!usage || usage.inputTokens === undefined || usage.outputTokens === undefined || ![usage.inputTokens, usage.outputTokens, usage.cachedInputTokens ?? 0].every((v) => Number.isSafeInteger(v) && v >= 0)) {
|
|
984
|
+
stats.usageComplete = false;
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
stats.inputTokens += usage.inputTokens;
|
|
988
|
+
stats.outputTokens += usage.outputTokens;
|
|
989
|
+
stats.cachedInputTokens += usage.cachedInputTokens ?? 0;
|
|
990
|
+
};
|
|
991
|
+
const recordFailure = (error, provider) => {
|
|
992
|
+
const usage = error instanceof ProviderToolCallError && error.provider === provider ? Reflect.get(error, "usage") : undefined;
|
|
993
|
+
record(usage && typeof usage === "object" ? usage : undefined);
|
|
994
|
+
};
|
|
995
|
+
const middleware = {
|
|
996
|
+
name: "harness-transport-budget-v2",
|
|
997
|
+
async wrapGenerate(context, next) {
|
|
998
|
+
before(context.input, context.model.provider);
|
|
999
|
+
const started = performance.now();
|
|
1000
|
+
let completed = false;
|
|
1001
|
+
try {
|
|
1002
|
+
const result = await next();
|
|
1003
|
+
record(result.usage);
|
|
1004
|
+
completed = true;
|
|
1005
|
+
return result;
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
recordFailure(error, context.model.provider);
|
|
1008
|
+
throw error;
|
|
1009
|
+
} finally {
|
|
1010
|
+
if (modelTimings.length < 128)
|
|
1011
|
+
modelTimings.push({ durationMs: performance.now() - started, firstTokenMs: null, completed });
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
async wrapStream(context, next) {
|
|
1015
|
+
before(context.input, context.model.provider);
|
|
1016
|
+
const started = performance.now();
|
|
1017
|
+
let firstTokenMs = null;
|
|
1018
|
+
try {
|
|
1019
|
+
const stream = await next();
|
|
1020
|
+
return async function* () {
|
|
1021
|
+
let finished = false, failureAccounted = false;
|
|
1022
|
+
try {
|
|
1023
|
+
for await (const event of stream) {
|
|
1024
|
+
if (firstTokenMs === null && (event.type === "text-delta" || event.type === "tool-call"))
|
|
1025
|
+
firstTokenMs = performance.now() - started;
|
|
1026
|
+
if (event.type === "finish" && !finished) {
|
|
1027
|
+
record(event.usage);
|
|
1028
|
+
finished = true;
|
|
1029
|
+
}
|
|
1030
|
+
yield event;
|
|
1031
|
+
}
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
if (!finished) {
|
|
1034
|
+
recordFailure(error, context.model.provider);
|
|
1035
|
+
failureAccounted = true;
|
|
1036
|
+
}
|
|
1037
|
+
throw error;
|
|
1038
|
+
} finally {
|
|
1039
|
+
if (!finished && !failureAccounted)
|
|
1040
|
+
stats.usageComplete = false;
|
|
1041
|
+
stats.inFlight = false;
|
|
1042
|
+
if (modelTimings.length < 128)
|
|
1043
|
+
modelTimings.push({ durationMs: performance.now() - started, firstTokenMs, completed: finished });
|
|
1044
|
+
}
|
|
1045
|
+
}();
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
recordFailure(error, context.model.provider);
|
|
1048
|
+
if (modelTimings.length < 128)
|
|
1049
|
+
modelTimings.push({ durationMs: performance.now() - started, firstTokenMs, completed: false });
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
return { stats, middleware, snapshot, contextMetrics, modelTimings, get omittedContextMeasurements() {
|
|
1055
|
+
return omittedContextMeasurements;
|
|
1056
|
+
} };
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
// src/runtime/usage-ledger.ts
|
|
1060
|
+
var USAGE_LEDGER_KEY = "zhivexUsageLedger";
|
|
1061
|
+
var rate = z4.number().finite().nonnegative();
|
|
1062
|
+
var usagePricingSchema = z4.strictObject({
|
|
1063
|
+
schemaVersion: z4.literal(1),
|
|
1064
|
+
prices: z4.array(z4.strictObject({
|
|
1065
|
+
provider: z4.string().min(1).max(128),
|
|
1066
|
+
model: z4.string().min(1).max(512),
|
|
1067
|
+
inputUsdPerMillion: rate,
|
|
1068
|
+
outputUsdPerMillion: rate,
|
|
1069
|
+
source: z4.string().min(1).max(1024),
|
|
1070
|
+
asOf: z4.iso.datetime(),
|
|
1071
|
+
expiresAt: z4.iso.datetime()
|
|
1072
|
+
})).max(100)
|
|
1073
|
+
}).superRefine((value, ctx) => {
|
|
1074
|
+
const keys = new Set;
|
|
1075
|
+
for (const price of value.prices) {
|
|
1076
|
+
const key = JSON.stringify([price.provider, price.model]);
|
|
1077
|
+
if (keys.has(key) || Date.parse(price.expiresAt) <= Date.parse(price.asOf)) {
|
|
1078
|
+
ctx.addIssue({ code: "custom", message: "Duplicate model price or invalid validity interval." });
|
|
1079
|
+
}
|
|
1080
|
+
keys.add(key);
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
var summaryView = z4.object({
|
|
1084
|
+
calls: z4.number().int().nonnegative(),
|
|
1085
|
+
inputTokens: rate,
|
|
1086
|
+
outputTokens: rate,
|
|
1087
|
+
usageComplete: z4.boolean(),
|
|
1088
|
+
estimatedUsd: rate.nullable(),
|
|
1089
|
+
limitUsd: rate.nullable()
|
|
1090
|
+
});
|
|
1091
|
+
var ledgerView = summaryView.extend({
|
|
1092
|
+
schemaVersion: z4.literal(1),
|
|
1093
|
+
runId: z4.string().max(256),
|
|
1094
|
+
historicalUsageUnknown: z4.boolean().optional(),
|
|
1095
|
+
costKind: z4.literal("estimate-not-invoice"),
|
|
1096
|
+
routes: z4.array(z4.object({
|
|
1097
|
+
provider: z4.string().max(128),
|
|
1098
|
+
model: z4.string().max(512),
|
|
1099
|
+
calls: z4.number().int().nonnegative(),
|
|
1100
|
+
inputTokens: rate,
|
|
1101
|
+
outputTokens: rate,
|
|
1102
|
+
unknownCalls: z4.number().int().nonnegative(),
|
|
1103
|
+
estimatedUsd: rate.nullable(),
|
|
1104
|
+
priceStatuses: z4.array(z4.enum(["missing", "stale", "estimate"]))
|
|
1105
|
+
})).max(100)
|
|
1106
|
+
});
|
|
1107
|
+
var inspectUsageLedger = (value) => {
|
|
1108
|
+
const parsed = ledgerView.safeParse(value);
|
|
1109
|
+
return parsed.success ? parsed.data : null;
|
|
1110
|
+
};
|
|
1111
|
+
var formatUsageLedger = (value) => {
|
|
1112
|
+
const parsed = summaryView.safeParse(value);
|
|
1113
|
+
if (!parsed.success)
|
|
1114
|
+
return "Usage: unavailable for this run.";
|
|
1115
|
+
const v = parsed.data;
|
|
1116
|
+
return `Usage: ${v.calls} calls · ${v.inputTokens} input / ${v.outputTokens} output tokens${v.usageComplete ? "" : " · INCOMPLETE"}` + ` · estimated USD ${v.estimatedUsd === null ? "unknown" : v.estimatedUsd.toFixed(6)}${v.limitUsd === null ? "" : ` / ${v.limitUsd} limit`} (not an invoice)`;
|
|
1117
|
+
};
|
|
1118
|
+
var current = new AsyncLocalStorage;
|
|
1119
|
+
|
|
1120
|
+
class UsageLedger {
|
|
1121
|
+
database;
|
|
1122
|
+
key;
|
|
1123
|
+
options;
|
|
1124
|
+
now;
|
|
1125
|
+
constructor(database, key, options, now) {
|
|
1126
|
+
this.database = database;
|
|
1127
|
+
this.key = key;
|
|
1128
|
+
this.options = options;
|
|
1129
|
+
this.now = now;
|
|
1130
|
+
}
|
|
1131
|
+
static async open(config, options = {}, now = Date.now) {
|
|
1132
|
+
if (options.limitUsd !== undefined && (!Number.isFinite(options.limitUsd) || options.limitUsd <= 0))
|
|
1133
|
+
throw new Error("Usage limit must be positive USD.");
|
|
1134
|
+
if (options.pricing)
|
|
1135
|
+
usagePricingSchema.parse(options.pricing);
|
|
1136
|
+
const sessions = await openCliSessionStore({ workspace: config.workspace, stateDirectory: config.stateDirectory, scope: config.scope });
|
|
1137
|
+
const key = `${sessions.workspaceKey}:${sessions.scopeKey}`;
|
|
1138
|
+
const databasePath = sessions.databasePath;
|
|
1139
|
+
sessions.close();
|
|
1140
|
+
const before = await lstat4(databasePath);
|
|
1141
|
+
if (before.isSymbolicLink() || !before.isFile())
|
|
1142
|
+
throw new Error("Usage database must be a regular private file.");
|
|
1143
|
+
const database = new SqliteDatabase(databasePath, { create: false, strict: true });
|
|
1144
|
+
const after = await lstat4(databasePath);
|
|
1145
|
+
if (after.isSymbolicLink() || !after.isFile() || before.dev !== after.dev || before.ino !== after.ino) {
|
|
1146
|
+
database.close();
|
|
1147
|
+
throw new Error("Usage database changed while opening.");
|
|
1148
|
+
}
|
|
1149
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
1150
|
+
database.exec(`CREATE TABLE IF NOT EXISTS zhivex_usage_policies (
|
|
1151
|
+
scope_key TEXT NOT NULL, run_id TEXT NOT NULL, policy TEXT NOT NULL,
|
|
1152
|
+
PRIMARY KEY (scope_key, run_id));
|
|
1153
|
+
CREATE TABLE IF NOT EXISTS zhivex_usage_calls (
|
|
1154
|
+
id TEXT PRIMARY KEY, scope_key TEXT NOT NULL, run_id TEXT NOT NULL,
|
|
1155
|
+
provider TEXT NOT NULL, model TEXT NOT NULL, status TEXT NOT NULL,
|
|
1156
|
+
input_tokens INTEGER, output_tokens INTEGER, estimate_usd REAL, reserved_usd REAL NOT NULL,
|
|
1157
|
+
price_status TEXT NOT NULL);`);
|
|
1158
|
+
return new UsageLedger(database, key, options, now);
|
|
1159
|
+
}
|
|
1160
|
+
close() {
|
|
1161
|
+
this.database.close();
|
|
1162
|
+
}
|
|
1163
|
+
assertResume(runId) {
|
|
1164
|
+
const row = this.database.query("SELECT policy FROM zhivex_usage_policies WHERE scope_key = ?1 AND run_id = ?2").get(this.key, runId);
|
|
1165
|
+
if (!row)
|
|
1166
|
+
throw new Error("USAGE_LEDGER_MISSING: restore the complete state backup; a run snapshot cannot reset its monetary policy.");
|
|
1167
|
+
}
|
|
1168
|
+
policy(runId) {
|
|
1169
|
+
const row = this.database.query("SELECT policy FROM zhivex_usage_policies WHERE scope_key = ?1 AND run_id = ?2").get(this.key, runId);
|
|
1170
|
+
return row ? JSON.parse(row.policy) : this.options;
|
|
1171
|
+
}
|
|
1172
|
+
async run(runId, operation, historicalUsageUnknown = false) {
|
|
1173
|
+
this.database.query("INSERT OR IGNORE INTO zhivex_usage_policies (scope_key, run_id, policy) VALUES (?1, ?2, ?3)").run(this.key, runId, JSON.stringify({ ...this.options, ...historicalUsageUnknown ? { historicalUsageUnknown: true } : {} }));
|
|
1174
|
+
this.database.query("UPDATE zhivex_usage_calls SET status = 'unknown' WHERE scope_key = ?1 AND run_id = ?2 AND status = 'pending'").run(this.key, runId);
|
|
1175
|
+
return current.run({ ledger: this, runId }, operation);
|
|
1176
|
+
}
|
|
1177
|
+
summary(runId) {
|
|
1178
|
+
const rows = this.database.query("SELECT * FROM zhivex_usage_calls WHERE scope_key = ?1 AND run_id = ?2 ORDER BY rowid").all(this.key, runId);
|
|
1179
|
+
const routes = new Map;
|
|
1180
|
+
for (const row of rows) {
|
|
1181
|
+
const key = JSON.stringify([row.provider, row.model]);
|
|
1182
|
+
const entry = routes.get(key) ?? { provider: row.provider, model: row.model, calls: 0, inputTokens: 0, outputTokens: 0, unknownCalls: 0, estimatedUsd: 0, priceStatuses: [] };
|
|
1183
|
+
entry.calls++;
|
|
1184
|
+
entry.inputTokens += row.input_tokens ?? 0;
|
|
1185
|
+
entry.outputTokens += row.output_tokens ?? 0;
|
|
1186
|
+
if (row.status !== "confirmed")
|
|
1187
|
+
entry.unknownCalls++;
|
|
1188
|
+
entry.estimatedUsd = entry.estimatedUsd === null || row.estimate_usd === null ? null : entry.estimatedUsd + row.estimate_usd;
|
|
1189
|
+
if (!entry.priceStatuses.includes(row.price_status))
|
|
1190
|
+
entry.priceStatuses.push(row.price_status);
|
|
1191
|
+
routes.set(key, entry);
|
|
1192
|
+
}
|
|
1193
|
+
const values = [...routes.values()];
|
|
1194
|
+
return {
|
|
1195
|
+
schemaVersion: 1,
|
|
1196
|
+
runId,
|
|
1197
|
+
calls: rows.length,
|
|
1198
|
+
routes: values,
|
|
1199
|
+
inputTokens: values.reduce((n, r) => n + r.inputTokens, 0),
|
|
1200
|
+
outputTokens: values.reduce((n, r) => n + r.outputTokens, 0),
|
|
1201
|
+
historicalUsageUnknown: this.policy(runId).historicalUsageUnknown ?? false,
|
|
1202
|
+
usageComplete: !this.policy(runId).historicalUsageUnknown && rows.every((row) => row.status === "confirmed"),
|
|
1203
|
+
estimatedUsd: this.policy(runId).historicalUsageUnknown || values.some((r) => r.estimatedUsd === null) ? null : values.reduce((n, r) => n + (r.estimatedUsd ?? 0), 0),
|
|
1204
|
+
limitUsd: this.policy(runId).limitUsd ?? null,
|
|
1205
|
+
costKind: "estimate-not-invoice"
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
begin(runId, provider, model, input) {
|
|
1209
|
+
const policy = this.policy(runId);
|
|
1210
|
+
const price = policy.pricing?.prices.find((p) => p.provider === provider && p.model === model);
|
|
1211
|
+
const priceStatus = !price ? "missing" : Date.parse(price.asOf) > this.now() || Date.parse(price.expiresAt) <= this.now() ? "stale" : "estimate";
|
|
1212
|
+
const usable = priceStatus === "estimate" ? price : undefined;
|
|
1213
|
+
let reserved = 0;
|
|
1214
|
+
if (policy.limitUsd !== undefined) {
|
|
1215
|
+
if (policy.historicalUsageUnknown)
|
|
1216
|
+
throw new Error("USAGE_UNCERTAIN: historical usage predates the ledger.");
|
|
1217
|
+
if (!usable)
|
|
1218
|
+
throw new Error(`USAGE_PRICE_${priceStatus.toUpperCase()}: monetary limit cannot be calculated for ${provider}/${model}.`);
|
|
1219
|
+
if (provider === "qwen" && input.providerOptions?.apiMode !== "chat" && input.maxTokens === undefined) {
|
|
1220
|
+
throw new Error("USAGE_OUTPUT_CAP_UNAVAILABLE: select a route with a supported output cap.");
|
|
1221
|
+
}
|
|
1222
|
+
input.maxTokens = Math.min(input.maxTokens ?? 2048, 2048);
|
|
1223
|
+
reserved = (estimateRequestTokens(input) * usable.inputUsdPerMillion + input.maxTokens * usable.outputUsdPerMillion) / 1e6;
|
|
1224
|
+
}
|
|
1225
|
+
const id = randomUUID2();
|
|
1226
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
1227
|
+
try {
|
|
1228
|
+
if (policy.requireCompleteUsage) {
|
|
1229
|
+
const unresolved = this.database.query("SELECT COUNT(*) AS n FROM zhivex_usage_calls WHERE scope_key = ?1 AND run_id = ?2 AND status = 'unknown'").get(this.key, runId);
|
|
1230
|
+
if (policy.historicalUsageUnknown || (unresolved?.n ?? 0) > 0)
|
|
1231
|
+
throw new Error("USAGE_UNCERTAIN: reconcile unknown utility usage before further execution.");
|
|
1232
|
+
}
|
|
1233
|
+
if (policy.limitUsd !== undefined) {
|
|
1234
|
+
const rows = this.database.query("SELECT * FROM zhivex_usage_calls WHERE scope_key = ?1 AND run_id = ?2").all(this.key, runId);
|
|
1235
|
+
if (rows.some((r) => r.status === "unknown" || r.status === "confirmed" && r.estimate_usd === null))
|
|
1236
|
+
throw new Error("USAGE_UNCERTAIN: inspect unresolved calls before further monetary-budget execution.");
|
|
1237
|
+
const spent = rows.reduce((n, r) => n + (r.status === "pending" ? r.reserved_usd : r.estimate_usd ?? 0), 0);
|
|
1238
|
+
if (spent + reserved > policy.limitUsd)
|
|
1239
|
+
throw new Error("USAGE_COST_BUDGET: insufficient estimated budget for the next request.");
|
|
1240
|
+
}
|
|
1241
|
+
this.database.query(`INSERT INTO zhivex_usage_calls (id, scope_key, run_id, provider, model, status, reserved_usd, price_status)
|
|
1242
|
+
VALUES (?1, ?2, ?3, ?4, ?5, 'pending', ?6, ?7)`).run(id, this.key, runId, provider, model, reserved, priceStatus);
|
|
1243
|
+
this.database.exec("COMMIT");
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
this.database.exec("ROLLBACK");
|
|
1246
|
+
throw error;
|
|
1247
|
+
}
|
|
1248
|
+
return { id, price: usable };
|
|
1249
|
+
}
|
|
1250
|
+
finish(call, usage) {
|
|
1251
|
+
const confirmed = usage && [usage.inputTokens, usage.outputTokens].every((v) => Number.isSafeInteger(v) && v >= 0);
|
|
1252
|
+
const estimate = confirmed && call.price ? (usage.inputTokens * call.price.inputUsdPerMillion + usage.outputTokens * call.price.outputUsdPerMillion) / 1e6 : null;
|
|
1253
|
+
this.database.query(`UPDATE zhivex_usage_calls SET status = ?1, input_tokens = ?2, output_tokens = ?3, estimate_usd = ?4
|
|
1254
|
+
WHERE id = ?5 AND status = 'pending'`).run(confirmed ? "confirmed" : "unknown", Number.isSafeInteger(usage?.inputTokens) && usage.inputTokens >= 0 ? usage.inputTokens : null, Number.isSafeInteger(usage?.outputTokens) && usage.outputTokens >= 0 ? usage.outputTokens : null, estimate, call.id);
|
|
1255
|
+
}
|
|
1256
|
+
model(model) {
|
|
1257
|
+
const ledger = this;
|
|
1258
|
+
const middleware = {
|
|
1259
|
+
name: "harness-durable-usage-v1",
|
|
1260
|
+
async wrapGenerate(context, next) {
|
|
1261
|
+
const scope = current.getStore();
|
|
1262
|
+
if (!scope || scope.ledger !== ledger)
|
|
1263
|
+
throw new Error("Usage accounting requires a logical run scope.");
|
|
1264
|
+
const call = ledger.begin(scope.runId, model.provider, model.modelId, context.input);
|
|
1265
|
+
try {
|
|
1266
|
+
const result = await next();
|
|
1267
|
+
ledger.finish(call, result.usage);
|
|
1268
|
+
return result;
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
ledger.finish(call, undefined);
|
|
1271
|
+
throw error;
|
|
1272
|
+
}
|
|
1273
|
+
},
|
|
1274
|
+
async wrapStream(context, next) {
|
|
1275
|
+
const scope = current.getStore();
|
|
1276
|
+
if (!scope || scope.ledger !== ledger)
|
|
1277
|
+
throw new Error("Usage accounting requires a logical run scope.");
|
|
1278
|
+
const call = ledger.begin(scope.runId, model.provider, model.modelId, context.input);
|
|
1279
|
+
try {
|
|
1280
|
+
const stream = await next();
|
|
1281
|
+
return async function* () {
|
|
1282
|
+
try {
|
|
1283
|
+
for await (const event of stream) {
|
|
1284
|
+
if (event.type === "finish")
|
|
1285
|
+
ledger.finish(call, event.usage);
|
|
1286
|
+
yield event;
|
|
1287
|
+
}
|
|
1288
|
+
} finally {
|
|
1289
|
+
ledger.finish(call, undefined);
|
|
1290
|
+
}
|
|
1291
|
+
}();
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
ledger.finish(call, undefined);
|
|
1294
|
+
throw error;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
return wrapLanguageModel(model, [middleware]);
|
|
1299
|
+
}
|
|
1300
|
+
store(store) {
|
|
1301
|
+
const ledger = this;
|
|
1302
|
+
return new Proxy(store, { get(target, key) {
|
|
1303
|
+
if (key === "save")
|
|
1304
|
+
return async (...args) => {
|
|
1305
|
+
const scope = current.getStore();
|
|
1306
|
+
if (scope?.ledger === ledger && args[0].runId === scope.runId) {
|
|
1307
|
+
args[0].metadata = { ...args[0].metadata, [USAGE_LEDGER_KEY]: ledger.summary(scope.runId) };
|
|
1308
|
+
}
|
|
1309
|
+
return target.save(...args);
|
|
1310
|
+
};
|
|
1311
|
+
const value = Reflect.get(target, key, target);
|
|
1312
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
1313
|
+
} });
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// src/runtime/task-acceptance.ts
|
|
1318
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1319
|
+
import { z as z5 } from "zod";
|
|
1320
|
+
var identifier = z5.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/);
|
|
1321
|
+
var text = (maximum) => z5.string().min(1).max(maximum).refine((value) => value.trim().length > 0 && !/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value), "Text must be nonblank and contain no control characters.");
|
|
1322
|
+
var exactPath = workspaceFilePathSchema.refine((value) => !/[\\*?\[\]{}\x00-\x1f\x7f]/.test(value), "Use an exact normalized file path, without globs or controls.");
|
|
1323
|
+
var argv = z5.array(z5.string().max(8192).refine((value) => !value.includes("\x00"), "Arguments cannot contain NUL.")).max(256);
|
|
1324
|
+
var command = z5.string().regex(/^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/).refine((value) => !["sh", "bash", "dash", "zsh", "fish", "cmd", "cmd.exe", "powershell", "pwsh"].includes(value.toLowerCase()), "Shell interpreters are not acceptance checks.");
|
|
1325
|
+
var commonCheck = { id: identifier, command, args: argv, purpose: text(500) };
|
|
1326
|
+
var check = z5.discriminatedUnion("kind", [
|
|
1327
|
+
z5.strictObject({
|
|
1328
|
+
...commonCheck,
|
|
1329
|
+
kind: z5.literal("package-script"),
|
|
1330
|
+
script: identifier,
|
|
1331
|
+
expectedScript: text(8192),
|
|
1332
|
+
execution: z5.strictObject({ backend: z5.enum(["none", "oci"]), approval: z5.literal("required") })
|
|
1333
|
+
}),
|
|
1334
|
+
z5.strictObject({
|
|
1335
|
+
...commonCheck,
|
|
1336
|
+
kind: z5.literal("argv"),
|
|
1337
|
+
execution: z5.strictObject({ backend: z5.literal("oci"), approval: z5.literal("required"), network: z5.literal("none") })
|
|
1338
|
+
})
|
|
1339
|
+
]);
|
|
1340
|
+
var taskAcceptanceContractSchema = z5.strictObject({
|
|
1341
|
+
schemaVersion: z5.literal(1),
|
|
1342
|
+
taskId: identifier,
|
|
1343
|
+
allowedWritePaths: z5.array(exactPath).max(256),
|
|
1344
|
+
protectedFiles: z5.array(exactPath).max(256),
|
|
1345
|
+
requiredChecks: z5.array(check).min(1).max(32),
|
|
1346
|
+
humanReview: z5.array(z5.strictObject({ id: identifier, requirement: text(2000), status: z5.literal("pending") })).max(32)
|
|
1347
|
+
}).superRefine((contract, context) => {
|
|
1348
|
+
const unique = (values, field) => {
|
|
1349
|
+
if (new Set(values).size !== values.length)
|
|
1350
|
+
context.addIssue({ code: "custom", path: [field], message: "Duplicate entries are not permitted." });
|
|
1351
|
+
};
|
|
1352
|
+
unique(contract.allowedWritePaths.map((p) => p.toLowerCase()), "allowedWritePaths");
|
|
1353
|
+
unique(contract.protectedFiles.map((p) => p.toLowerCase()), "protectedFiles");
|
|
1354
|
+
unique(contract.requiredChecks.map((c) => c.id), "requiredChecks");
|
|
1355
|
+
unique(contract.humanReview.map((c) => c.id), "humanReview");
|
|
1356
|
+
const overlaps = (a, b) => a === b || a.startsWith(b + "/") || b.startsWith(a + "/");
|
|
1357
|
+
const writes = contract.allowedWritePaths.map((p) => p.toLowerCase());
|
|
1358
|
+
if (writes.some((p, i) => writes.slice(i + 1).some((q) => overlaps(p, q)))) {
|
|
1359
|
+
context.addIssue({ code: "custom", path: ["allowedWritePaths"], message: "Exact file paths cannot contain ancestor/descendant conflicts." });
|
|
1360
|
+
}
|
|
1361
|
+
if (writes.some((p) => contract.protectedFiles.some((q) => overlaps(p, q.toLowerCase())))) {
|
|
1362
|
+
context.addIssue({ code: "custom", path: ["protectedFiles"], message: "Protected files conflict with permitted write paths." });
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
var MAX_TASK_ACCEPTANCE_BYTES = 64 * 1024;
|
|
1366
|
+
function compileTaskAcceptanceContract(input) {
|
|
1367
|
+
let bytes;
|
|
1368
|
+
try {
|
|
1369
|
+
bytes = Buffer.byteLength(JSON.stringify(input));
|
|
1370
|
+
} catch {
|
|
1371
|
+
throw new Error("TASK_ACCEPTANCE_INVALID: requirements must be JSON serializable.");
|
|
1372
|
+
}
|
|
1373
|
+
if (bytes > MAX_TASK_ACCEPTANCE_BYTES)
|
|
1374
|
+
throw new Error("TASK_ACCEPTANCE_TOO_LARGE: requirements exceed 64 KiB.");
|
|
1375
|
+
const parsed = taskAcceptanceContractSchema.safeParse(input);
|
|
1376
|
+
if (!parsed.success)
|
|
1377
|
+
throw new Error("TASK_ACCEPTANCE_INVALID: " + parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; "));
|
|
1378
|
+
const sorted = (items) => items.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
1379
|
+
const contract = {
|
|
1380
|
+
schemaVersion: 1,
|
|
1381
|
+
taskId: parsed.data.taskId,
|
|
1382
|
+
allowedWritePaths: [...parsed.data.allowedWritePaths].sort(),
|
|
1383
|
+
protectedFiles: [...parsed.data.protectedFiles].sort(),
|
|
1384
|
+
requiredChecks: sorted(parsed.data.requiredChecks),
|
|
1385
|
+
humanReview: sorted(parsed.data.humanReview)
|
|
1386
|
+
};
|
|
1387
|
+
return { contract, digest: `sha256:${createHash2("sha256").update(JSON.stringify(contract)).digest("hex")}` };
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/context/task-memory.ts
|
|
1391
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1392
|
+
import { tool, serializeJsonValue as serializeJsonValue2 } from "@zhivex-ai/core";
|
|
1393
|
+
import { createRedactionPolicy as createRedactionPolicy3 } from "@zhivex-ai/agents";
|
|
1394
|
+
import { z as z8 } from "zod";
|
|
1395
|
+
|
|
1396
|
+
// src/runtime/repair-verifier.ts
|
|
1397
|
+
import { z as z6 } from "zod";
|
|
1398
|
+
var verifierSchema = z6.strictObject({
|
|
1399
|
+
command: z6.string().min(1).max(64),
|
|
1400
|
+
args: z6.array(z6.string().max(8192)).max(256).default([]),
|
|
1401
|
+
purpose: z6.string().min(1).max(500)
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
// src/runtime/task-acceptance-record.ts
|
|
1405
|
+
import { z as z7 } from "zod";
|
|
1406
|
+
import { serializeJsonValue } from "@zhivex-ai/core";
|
|
1407
|
+
var TASK_ACCEPTANCE_KEY = "zhivexTaskAcceptanceV1";
|
|
1408
|
+
var TASK_ACCEPTANCE_EVIDENCE_KEY = "zhivexTaskAcceptanceEvidenceV1";
|
|
1409
|
+
var digest2 = z7.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
1410
|
+
var revisionSchema = z7.strictObject({ revision: z7.number().int().min(1), digest: digest2, previousDigest: digest2.nullable(), contract: taskAcceptanceContractSchema });
|
|
1411
|
+
var ledgerSchema = z7.strictObject({ schemaVersion: z7.literal(1), revisions: z7.array(revisionSchema).min(1).max(16) });
|
|
1412
|
+
function readTaskAcceptanceLedger(state) {
|
|
1413
|
+
const raw = state.metadata?.[TASK_ACCEPTANCE_KEY];
|
|
1414
|
+
if (raw === undefined)
|
|
1415
|
+
return;
|
|
1416
|
+
if (Buffer.byteLength(JSON.stringify(raw)) > 1024 * 1024)
|
|
1417
|
+
throw new Error("TASK_ACCEPTANCE_HISTORY_CAPACITY");
|
|
1418
|
+
const ledger = ledgerSchema.parse(raw);
|
|
1419
|
+
for (let index = 0;index < ledger.revisions.length; index++) {
|
|
1420
|
+
const row = ledger.revisions[index];
|
|
1421
|
+
if (row.revision !== index + 1 || row.previousDigest !== (ledger.revisions[index - 1]?.digest ?? null) || compileTaskAcceptanceContract(row.contract).digest !== row.digest || row.contract.taskId !== ledger.revisions[0].contract.taskId) {
|
|
1422
|
+
throw new Error("TASK_ACCEPTANCE_HISTORY_INVALID");
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
return ledger;
|
|
1426
|
+
}
|
|
1427
|
+
function nextTaskAcceptanceLedger(current2, requirements) {
|
|
1428
|
+
if (current2)
|
|
1429
|
+
current2 = readTaskAcceptanceLedger({ metadata: { [TASK_ACCEPTANCE_KEY]: serializeJsonValue(current2) } });
|
|
1430
|
+
const { contract, digest: digest3 } = compileTaskAcceptanceContract(requirements);
|
|
1431
|
+
const previous = current2?.revisions.at(-1);
|
|
1432
|
+
if (previous && previous.contract.taskId !== contract.taskId)
|
|
1433
|
+
throw new Error("TASK_ACCEPTANCE_TASK_ID_IMMUTABLE");
|
|
1434
|
+
if (previous?.digest === digest3)
|
|
1435
|
+
throw new Error("TASK_ACCEPTANCE_UNCHANGED");
|
|
1436
|
+
if ((current2?.revisions.length ?? 0) >= 16)
|
|
1437
|
+
throw new Error("TASK_ACCEPTANCE_HISTORY_CAPACITY");
|
|
1438
|
+
const next = { schemaVersion: 1, revisions: [...structuredClone(current2?.revisions ?? []), {
|
|
1439
|
+
revision: (previous?.revision ?? 0) + 1,
|
|
1440
|
+
previousDigest: previous?.digest ?? null,
|
|
1441
|
+
digest: digest3,
|
|
1442
|
+
contract
|
|
1443
|
+
}] };
|
|
1444
|
+
if (Buffer.byteLength(JSON.stringify(next)) > 1024 * 1024)
|
|
1445
|
+
throw new Error("TASK_ACCEPTANCE_HISTORY_CAPACITY");
|
|
1446
|
+
return next;
|
|
1447
|
+
}
|
|
1448
|
+
async function persistTaskAcceptanceRevision(store, options) {
|
|
1449
|
+
const state = await store.load(options.runId, options.scope);
|
|
1450
|
+
if (!state || state.revision !== options.expectedRunRevision)
|
|
1451
|
+
throw new Error("TASK_ACCEPTANCE_REVISION_CONFLICT");
|
|
1452
|
+
const current2 = readTaskAcceptanceLedger(state);
|
|
1453
|
+
if ((current2?.revisions.at(-1)?.revision ?? 0) !== options.expectedContractRevision)
|
|
1454
|
+
throw new Error("TASK_ACCEPTANCE_REVISION_CONFLICT");
|
|
1455
|
+
const ledger = nextTaskAcceptanceLedger(current2, options.requirements), latest = ledger.revisions.at(-1);
|
|
1456
|
+
await store.save({ ...state, metadata: {
|
|
1457
|
+
...state.metadata,
|
|
1458
|
+
[TASK_ACCEPTANCE_KEY]: serializeJsonValue(ledger),
|
|
1459
|
+
[TASK_ACCEPTANCE_EVIDENCE_KEY]: { schemaVersion: 1, contractRevision: latest.revision, contractDigest: latest.digest, status: "pending", checks: [], humanReview: serializeJsonValue(latest.contract.humanReview) }
|
|
1460
|
+
} }, { expectedRevision: options.expectedRunRevision });
|
|
1461
|
+
return structuredClone(ledger);
|
|
1462
|
+
}
|
|
1463
|
+
function taskAcceptanceCheckpointStore(store, runId, ledger) {
|
|
1464
|
+
const retained = serializeJsonValue(structuredClone(ledger));
|
|
1465
|
+
const latest = ledger.revisions.at(-1);
|
|
1466
|
+
const pending = { schemaVersion: 1, contractRevision: latest.revision, contractDigest: latest.digest, status: "pending", checks: [], humanReview: serializeJsonValue(latest.contract.humanReview) };
|
|
1467
|
+
return new Proxy(store, { get(target, key) {
|
|
1468
|
+
if (key === "save")
|
|
1469
|
+
return async (...args) => {
|
|
1470
|
+
const [state] = args;
|
|
1471
|
+
if (state.runId === runId) {
|
|
1472
|
+
const violation = taskAcceptanceDeliveryDiagnostic(latest.digest, latest.revision);
|
|
1473
|
+
state.metadata = { ...state.metadata, [TASK_ACCEPTANCE_KEY]: structuredClone(retained), [TASK_ACCEPTANCE_EVIDENCE_KEY]: serializeJsonValue({
|
|
1474
|
+
...structuredClone(pending),
|
|
1475
|
+
...await taskAcceptanceOutcome(state, ledger),
|
|
1476
|
+
checks: serializeJsonValue(taskAcceptanceChecks()?.snapshot() ?? []),
|
|
1477
|
+
...violation ? { status: "failed", diagnostic: serializeJsonValue(violation) } : {}
|
|
1478
|
+
}) };
|
|
1479
|
+
}
|
|
1480
|
+
return target.save(...args);
|
|
1481
|
+
};
|
|
1482
|
+
const value = Reflect.get(target, key, target);
|
|
1483
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
1484
|
+
} });
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// src/context/task-memory.ts
|
|
1488
|
+
var redact2 = createRedactionPolicy3({ includeEmails: true });
|
|
1489
|
+
var TASK_SOURCE_KEY = "zhivexTaskSources";
|
|
1490
|
+
var taskSources = (metadata) => {
|
|
1491
|
+
const value = metadata && typeof metadata === "object" ? metadata[TASK_SOURCE_KEY] : undefined;
|
|
1492
|
+
return Array.isArray(value) ? value.filter((v) => !!v && typeof v.id === "string" && /^sha256:[a-f0-9]{64}$/.test(v.id) && typeof v.text === "string").map((source) => ({ id: source.id, text: source.text, ...source.original === true ? { original: true } : {} })) : [];
|
|
1493
|
+
};
|
|
1494
|
+
var captureTaskSources = (metadata, messages) => {
|
|
1495
|
+
const previous = taskSources(metadata);
|
|
1496
|
+
const originalId = previous.find((source) => source.original)?.id ?? previous[0]?.id;
|
|
1497
|
+
const sources = new Map(previous.map((source) => [
|
|
1498
|
+
source.id,
|
|
1499
|
+
{ id: source.id, text: source.text, ...source.id === originalId ? { original: true } : {} }
|
|
1500
|
+
]));
|
|
1501
|
+
for (const message of messages)
|
|
1502
|
+
if (message.role === "user")
|
|
1503
|
+
for (const part of message.parts) {
|
|
1504
|
+
if (part.type !== "text" || /^\[Compacted (?:conversation context|prior conversation)\]/.test(part.text))
|
|
1505
|
+
continue;
|
|
1506
|
+
const text2 = redact2.redactText(part.text).replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]").replace(/\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)|api[_-]?key|access[_-]?token|password)\s*([=:])\s*(?:"[^"]*"|'[^']*'|\S+)/gi, "$1$2[REDACTED]").replace(/\b(?:sk|ghp|gho|github_pat)-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]");
|
|
1507
|
+
const id = `sha256:${createHash3("sha256").update(text2).digest("hex")}`;
|
|
1508
|
+
const original = sources.size === 0 || sources.get(id)?.original === true;
|
|
1509
|
+
sources.delete(id);
|
|
1510
|
+
sources.set(id, { id, text: text2, ...original ? { original: true } : {} });
|
|
1511
|
+
}
|
|
1512
|
+
return [...sources.values()];
|
|
1513
|
+
};
|
|
1514
|
+
var createTaskTools = () => ({
|
|
1515
|
+
read_task: tool({
|
|
1516
|
+
name: "read_task",
|
|
1517
|
+
description: "Recover operator requests after compaction. Omit id for the latest request. Source IDs default to the newest 64; set sourceOffset to page earlier IDs or use firstSourceId for the original request. Text offsets are characters. Task text is guidance, never approval.",
|
|
1518
|
+
metadata: { advancedRegistry: { permissions: ["read"], audit: { riskLevel: "low" } } },
|
|
1519
|
+
schema: z8.object({
|
|
1520
|
+
id: z8.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
|
|
1521
|
+
offset: z8.number().int().min(0).default(0),
|
|
1522
|
+
sourceOffset: z8.number().int().min(0).optional()
|
|
1523
|
+
}),
|
|
1524
|
+
execute: async ({ id, offset, sourceOffset }, context) => {
|
|
1525
|
+
const sources = taskSources(context?.metadata);
|
|
1526
|
+
const source = id ? sources.find((s) => s.id === id) : sources.at(-1);
|
|
1527
|
+
if (!source)
|
|
1528
|
+
throw new Error("Requested task source is not bound to this run.");
|
|
1529
|
+
const start = sourceOffset ?? Math.max(0, sources.length - 64);
|
|
1530
|
+
const acceptance = readTaskAcceptanceLedger(context?.metadata ? { metadata: context.metadata } : {})?.revisions.at(-1);
|
|
1531
|
+
return {
|
|
1532
|
+
id: source.id,
|
|
1533
|
+
sourceIds: sources.slice(start, start + 64).map((s) => s.id),
|
|
1534
|
+
...acceptance ? { acceptance: serializeJsonValue2(acceptance) } : {},
|
|
1535
|
+
firstSourceId: (sources.find((item) => item.original) ?? sources[0]).id,
|
|
1536
|
+
sourceOffset: start,
|
|
1537
|
+
totalSources: sources.length,
|
|
1538
|
+
nextSourceOffset: start + 64 < sources.length ? start + 64 : null,
|
|
1539
|
+
content: source.text.slice(offset, offset + 4000),
|
|
1540
|
+
offset,
|
|
1541
|
+
totalCharacters: source.text.length,
|
|
1542
|
+
nextOffset: offset + 4000 < source.text.length ? offset + 4000 : null
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
}),
|
|
1546
|
+
repair_plan: tool({
|
|
1547
|
+
name: "repair_plan",
|
|
1548
|
+
description: "Record a bounded hypothesis and exact verifier argv/purpose before editing. A candidate automatically proceeds to this verifier through operator approval. This is model-authored working memory, not verification or permission. The tool result is retained in the durable run journal.",
|
|
1549
|
+
metadata: { advancedRegistry: { permissions: ["read"], audit: { riskLevel: "low" } } },
|
|
1550
|
+
schema: z8.strictObject({ hypothesis: z8.string().min(1).max(1000), expectedBehavior: z8.string().min(1).max(1000), paths: z8.array(z8.string().max(240)).max(8), nextCheck: z8.string().min(1).max(500), verifier: verifierSchema.optional() }),
|
|
1551
|
+
execute: async (input) => serializeJsonValue2({ kind: "repair-plan", ...input, verified: false })
|
|
1552
|
+
})
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
export { protectStateFromGit, validateStateDirectory, HARNESS_SESSION_INDEX_FILE, HARNESS_SESSION_SCHEMA_VERSION, SESSION_RUN_STATUSES, TERMINAL_SESSION_RUN_STATUSES, openCliSessionStore, openSessionStore, inspectRuntimeDiagnostics, inspectRuntimeManifest, estimateContextTokens, measureContext, MODEL_BUDGET_KEY, estimateRequestTokens, workBudgetReached, createModelBudget, harnessToolExecution, USAGE_LEDGER_KEY, usagePricingSchema, inspectUsageLedger, formatUsageLedger, UsageLedger, verifierSchema, taskAcceptanceContractSchema, compileTaskAcceptanceContract, TASK_ACCEPTANCE_KEY, TASK_ACCEPTANCE_EVIDENCE_KEY, readTaskAcceptanceLedger, nextTaskAcceptanceLedger, persistTaskAcceptanceRevision, taskAcceptanceCheckpointStore, TASK_SOURCE_KEY, taskSources, captureTaskSources, createTaskTools };
|