@zhivex-ai/harness 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +43 -15
- package/ROADMAP.md +49 -19
- package/SUPPORT.md +2 -2
- package/dist/cli-stream.d.ts +167 -0
- package/dist/cli.d.ts +18 -27
- package/dist/cli.js +1865 -286
- package/dist/config.d.ts +8 -38
- package/dist/harness.d.ts +4 -1
- package/dist/hostile-repository-demo.js +396 -146
- package/dist/index.d.ts +9 -3
- package/dist/index.js +1422 -368
- package/dist/providers.d.ts +77 -0
- package/dist/routing.d.ts +16 -0
- package/dist/sessions.d.ts +122 -0
- package/dist/zhx.js +6910 -0
- package/docs/CLI.md +37 -2
- package/docs/DURABLE_OPERATIONS.md +31 -0
- package/docs/EXTENSIBILITY.md +33 -1
- package/docs/LIVE_CERTIFICATION.md +34 -135
- package/docs/RELEASE.md +10 -25
- package/package.json +6 -3
package/dist/cli.js
CHANGED
|
@@ -3,21 +3,369 @@
|
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { createInterface } from "readline/promises";
|
|
6
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
6
7
|
import { constants as fsConstants } from "fs";
|
|
7
|
-
import { access, lstat as
|
|
8
|
-
import
|
|
8
|
+
import { access, lstat as lstat7, readFile as readFile4, stat as stat4 } from "fs/promises";
|
|
9
|
+
import path9 from "path";
|
|
9
10
|
import {
|
|
10
11
|
getAgentBudgetStatus as getAgentBudgetStatus3
|
|
11
12
|
} from "@zhivex-ai/agents";
|
|
12
13
|
|
|
13
14
|
// src/config.ts
|
|
14
|
-
import { createHash } from "crypto";
|
|
15
|
+
import { createHash as createHash2 } from "crypto";
|
|
15
16
|
import { realpathSync } from "fs";
|
|
16
17
|
import path from "path";
|
|
18
|
+
|
|
19
|
+
// src/providers.ts
|
|
20
|
+
import { createHash } from "crypto";
|
|
21
|
+
import { createGemini } from "@zhivex-ai/gemini";
|
|
17
22
|
import { createMeta } from "@zhivex-ai/meta";
|
|
18
23
|
import { createOpenAI } from "@zhivex-ai/openai";
|
|
19
24
|
import { createQwen } from "@zhivex-ai/qwen";
|
|
20
|
-
var PROVIDERS = ["meta", "qwen", "openai"];
|
|
25
|
+
var PROVIDERS = ["meta", "qwen", "openai", "gemini"];
|
|
26
|
+
var HARNESS_PROVIDER_CAPABILITIES = [
|
|
27
|
+
"streaming",
|
|
28
|
+
"tool-calling",
|
|
29
|
+
"approval-resume"
|
|
30
|
+
];
|
|
31
|
+
var QWEN_REGIONS = [
|
|
32
|
+
"singapore",
|
|
33
|
+
"beijing",
|
|
34
|
+
"hong-kong",
|
|
35
|
+
"tokyo",
|
|
36
|
+
"frankfurt",
|
|
37
|
+
"virginia"
|
|
38
|
+
];
|
|
39
|
+
var PROVIDER_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
40
|
+
var ENVIRONMENT_VARIABLE = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
41
|
+
var DIAGNOSTIC_KEY = /^[a-z][A-Za-z0-9]{0,63}$/;
|
|
42
|
+
var requiredText = (name, value) => {
|
|
43
|
+
const normalized = value.trim();
|
|
44
|
+
if (!normalized || /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
45
|
+
throw new Error(`${name} must be a non-empty printable string.`);
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
};
|
|
49
|
+
var environmentVariable = (name, value) => {
|
|
50
|
+
if (!ENVIRONMENT_VARIABLE.test(value)) {
|
|
51
|
+
throw new Error(`${name} must be a valid environment variable name.`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
};
|
|
55
|
+
var diagnosticKey = (value) => {
|
|
56
|
+
if (!DIAGNOSTIC_KEY.test(value)) {
|
|
57
|
+
throw new Error(`Invalid provider diagnostic key: ${value}.`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
};
|
|
61
|
+
var normalizedDescriptor = (descriptor) => {
|
|
62
|
+
const id = descriptor.id.trim().toLowerCase();
|
|
63
|
+
if (descriptor.id !== id || !PROVIDER_ID.test(id)) {
|
|
64
|
+
throw new Error(`Invalid provider id: ${descriptor.id}. Use 1-64 lowercase letters, digits, dot, underscore, or hyphen.`);
|
|
65
|
+
}
|
|
66
|
+
if (!(descriptor.support === "certified" || descriptor.support === "provisional")) {
|
|
67
|
+
throw new Error(`Invalid support level for provider ${id}.`);
|
|
68
|
+
}
|
|
69
|
+
const credentialNames = [...new Set(descriptor.credentialNames.map((name) => environmentVariable(`Credential for provider ${id}`, name)))];
|
|
70
|
+
const capabilities = [...new Set(descriptor.capabilities)];
|
|
71
|
+
const invalidCapability = capabilities.find((capability) => !HARNESS_PROVIDER_CAPABILITIES.includes(capability));
|
|
72
|
+
if (invalidCapability) {
|
|
73
|
+
throw new Error(`Invalid capability for provider ${id}: ${invalidCapability}.`);
|
|
74
|
+
}
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
id,
|
|
77
|
+
name: requiredText(`Name for provider ${id}`, descriptor.name),
|
|
78
|
+
defaultModel: requiredText(`Default model for provider ${id}`, descriptor.defaultModel),
|
|
79
|
+
credentialNames: Object.freeze(credentialNames),
|
|
80
|
+
capabilities: Object.freeze(capabilities),
|
|
81
|
+
support: descriptor.support
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
var normalizedDiagnostics = (provider, diagnostics) => {
|
|
85
|
+
const endpointEnvironmentVariable = diagnostics?.endpointEnvironmentVariable === undefined ? undefined : environmentVariable(`Endpoint diagnostic for provider ${provider}`, diagnostics.endpointEnvironmentVariable);
|
|
86
|
+
const seenKeys = new Set;
|
|
87
|
+
const presence = (diagnostics?.presence ?? []).map((entry) => {
|
|
88
|
+
const key = diagnosticKey(entry.key);
|
|
89
|
+
if (seenKeys.has(key)) {
|
|
90
|
+
throw new Error(`Duplicate diagnostic key for provider ${provider}: ${key}.`);
|
|
91
|
+
}
|
|
92
|
+
seenKeys.add(key);
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
key,
|
|
95
|
+
environmentVariable: environmentVariable(`Presence diagnostic for provider ${provider}`, entry.environmentVariable)
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
const enums = (diagnostics?.enums ?? []).map((entry) => {
|
|
99
|
+
const key = diagnosticKey(entry.key);
|
|
100
|
+
if (seenKeys.has(key)) {
|
|
101
|
+
throw new Error(`Duplicate diagnostic key for provider ${provider}: ${key}.`);
|
|
102
|
+
}
|
|
103
|
+
seenKeys.add(key);
|
|
104
|
+
const allowedValues = [...new Set(entry.allowedValues.map((value) => requiredText(`Allowed diagnostic value for provider ${provider}`, value)))];
|
|
105
|
+
if (allowedValues.length === 0) {
|
|
106
|
+
throw new Error(`Enum diagnostic ${key} for provider ${provider} needs allowed values.`);
|
|
107
|
+
}
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
key,
|
|
110
|
+
environmentVariable: environmentVariable(`Enum diagnostic for provider ${provider}`, entry.environmentVariable),
|
|
111
|
+
allowedValues: Object.freeze(allowedValues)
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
return Object.freeze({
|
|
115
|
+
...endpointEnvironmentVariable ? { endpointEnvironmentVariable } : {},
|
|
116
|
+
...presence.length > 0 ? { presence: Object.freeze(presence) } : {},
|
|
117
|
+
...enums.length > 0 ? { enums: Object.freeze(enums) } : {}
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
var endpointConfiguration = (value) => {
|
|
121
|
+
const configured = Boolean(value?.trim());
|
|
122
|
+
if (!configured) {
|
|
123
|
+
return {
|
|
124
|
+
customEndpoint: false,
|
|
125
|
+
endpointValid: true,
|
|
126
|
+
endpointSecure: true
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const endpoint = new URL(value.trim());
|
|
131
|
+
const supportedProtocol = endpoint.protocol === "https:" || endpoint.protocol === "http:";
|
|
132
|
+
return {
|
|
133
|
+
customEndpoint: true,
|
|
134
|
+
endpointValid: supportedProtocol && !endpoint.username && !endpoint.password && !endpoint.hash && !endpoint.search,
|
|
135
|
+
endpointSecure: endpoint.protocol === "https:"
|
|
136
|
+
};
|
|
137
|
+
} catch {
|
|
138
|
+
return {
|
|
139
|
+
customEndpoint: true,
|
|
140
|
+
endpointValid: false,
|
|
141
|
+
endpointSecure: false
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var createCredentials = (env, provider, names) => Object.freeze({
|
|
146
|
+
names,
|
|
147
|
+
get: (name) => {
|
|
148
|
+
if (!names.includes(name)) {
|
|
149
|
+
throw new Error(`Credential ${name} is not declared for provider ${provider}.`);
|
|
150
|
+
}
|
|
151
|
+
return env[name]?.trim() || undefined;
|
|
152
|
+
},
|
|
153
|
+
first: () => {
|
|
154
|
+
for (const name of names) {
|
|
155
|
+
const value = env[name]?.trim();
|
|
156
|
+
if (value) {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
},
|
|
162
|
+
require: () => {
|
|
163
|
+
for (const name of names) {
|
|
164
|
+
const value = env[name]?.trim();
|
|
165
|
+
if (value) {
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw new Error(names.length === 0 ? `Provider ${provider} does not declare a credential.` : `Missing credentials for ${provider}. Set ${names.join(" or ")} in the environment.`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
var normalizeRegistration = (registration) => {
|
|
173
|
+
if (typeof registration.factory !== "function") {
|
|
174
|
+
throw new Error("Provider registration factory must be a function.");
|
|
175
|
+
}
|
|
176
|
+
const descriptor = normalizedDescriptor(registration.descriptor);
|
|
177
|
+
return Object.freeze({
|
|
178
|
+
descriptor,
|
|
179
|
+
factory: registration.factory,
|
|
180
|
+
diagnostics: normalizedDiagnostics(descriptor.id, registration.diagnostics)
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
var providerConfiguration = (diagnostics, env) => {
|
|
184
|
+
const configuration = endpointConfiguration(diagnostics.endpointEnvironmentVariable ? env[diagnostics.endpointEnvironmentVariable] : undefined);
|
|
185
|
+
for (const entry of diagnostics.presence ?? []) {
|
|
186
|
+
configuration[`${entry.key}Configured`] = Boolean(env[entry.environmentVariable]?.trim());
|
|
187
|
+
}
|
|
188
|
+
for (const entry of diagnostics.enums ?? []) {
|
|
189
|
+
const value = env[entry.environmentVariable]?.trim();
|
|
190
|
+
configuration[`${entry.key}Configured`] = Boolean(value);
|
|
191
|
+
configuration[`${entry.key}Valid`] = !value || entry.allowedValues.includes(value);
|
|
192
|
+
}
|
|
193
|
+
return Object.freeze(configuration);
|
|
194
|
+
};
|
|
195
|
+
var parseWithRegistrations = (value, registrations) => {
|
|
196
|
+
const defaultProvider = registrations.has("openai") ? "openai" : registrations.keys().next().value;
|
|
197
|
+
const normalized = (value ?? defaultProvider ?? "").trim().toLowerCase();
|
|
198
|
+
if (registrations.has(normalized)) {
|
|
199
|
+
return normalized;
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`Unknown provider: ${value}. Use ${[...registrations.keys()].join(", ")}.`);
|
|
202
|
+
};
|
|
203
|
+
var createProviderRegistry = (registrations) => {
|
|
204
|
+
if (registrations.length === 0) {
|
|
205
|
+
throw new Error("Provider registry must contain at least one provider.");
|
|
206
|
+
}
|
|
207
|
+
const entries = new Map;
|
|
208
|
+
for (const registration of registrations) {
|
|
209
|
+
const normalized = normalizeRegistration(registration);
|
|
210
|
+
if (entries.has(normalized.descriptor.id)) {
|
|
211
|
+
throw new Error(`Duplicate provider registration: ${normalized.descriptor.id}.`);
|
|
212
|
+
}
|
|
213
|
+
entries.set(normalized.descriptor.id, normalized);
|
|
214
|
+
}
|
|
215
|
+
const frozenRegistrations = Object.freeze([...entries.values()]);
|
|
216
|
+
const descriptors = Object.freeze(frozenRegistrations.map((entry) => entry.descriptor));
|
|
217
|
+
const registry = Object.freeze({
|
|
218
|
+
descriptors,
|
|
219
|
+
has: (provider) => entries.has(provider.trim().toLowerCase()),
|
|
220
|
+
parse: (value) => parseWithRegistrations(value, entries),
|
|
221
|
+
descriptor: (provider) => {
|
|
222
|
+
const registration = entries.get(provider);
|
|
223
|
+
if (!registration) {
|
|
224
|
+
throw new Error(`No descriptor is available for provider ${provider}.`);
|
|
225
|
+
}
|
|
226
|
+
return registration.descriptor;
|
|
227
|
+
},
|
|
228
|
+
createModel: (config, env = process.env) => {
|
|
229
|
+
const registration = entries.get(config.provider);
|
|
230
|
+
if (!registration) {
|
|
231
|
+
throw new Error(`No factory is available for provider ${config.provider}.`);
|
|
232
|
+
}
|
|
233
|
+
return registration.factory(Object.freeze({
|
|
234
|
+
provider: config.provider,
|
|
235
|
+
model: config.model,
|
|
236
|
+
env,
|
|
237
|
+
credentials: createCredentials(env, config.provider, registration.descriptor.credentialNames)
|
|
238
|
+
}));
|
|
239
|
+
},
|
|
240
|
+
availability: (env = process.env) => Object.freeze(frozenRegistrations.map((registration) => {
|
|
241
|
+
const credentials = Object.freeze(registration.descriptor.credentialNames.map((name) => Object.freeze({ name, present: Boolean(env[name]?.trim()) })));
|
|
242
|
+
return Object.freeze({
|
|
243
|
+
...registration.descriptor,
|
|
244
|
+
configured: credentials.length === 0 || credentials.some((credential) => credential.present),
|
|
245
|
+
credentials,
|
|
246
|
+
configuration: providerConfiguration(registration.diagnostics, env)
|
|
247
|
+
});
|
|
248
|
+
})),
|
|
249
|
+
transportFingerprint: (env = process.env) => {
|
|
250
|
+
const transport = frozenRegistrations.map((registration) => {
|
|
251
|
+
const names = [
|
|
252
|
+
registration.diagnostics.endpointEnvironmentVariable,
|
|
253
|
+
...(registration.diagnostics.presence ?? []).map((entry) => entry.environmentVariable),
|
|
254
|
+
...(registration.diagnostics.enums ?? []).map((entry) => entry.environmentVariable)
|
|
255
|
+
].filter((name) => Boolean(name));
|
|
256
|
+
return {
|
|
257
|
+
provider: registration.descriptor.id,
|
|
258
|
+
values: names.map((name) => [name, env[name]?.trim() ?? ""])
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(transport)).digest("hex")}`;
|
|
262
|
+
},
|
|
263
|
+
extend: (additional) => createProviderRegistry([
|
|
264
|
+
...frozenRegistrations,
|
|
265
|
+
...additional
|
|
266
|
+
])
|
|
267
|
+
});
|
|
268
|
+
return registry;
|
|
269
|
+
};
|
|
270
|
+
var optionalQwenRegion = (value) => {
|
|
271
|
+
if (!value) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (QWEN_REGIONS.includes(value)) {
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
throw new Error(`Invalid QWEN_REGION: ${value}.`);
|
|
278
|
+
};
|
|
279
|
+
var BUILTIN_PROVIDER_REGISTRATIONS = Object.freeze([
|
|
280
|
+
{
|
|
281
|
+
descriptor: {
|
|
282
|
+
id: "meta",
|
|
283
|
+
name: "Meta Model API",
|
|
284
|
+
defaultModel: "muse-spark-1.2",
|
|
285
|
+
credentialNames: ["MODEL_API_KEY"],
|
|
286
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
287
|
+
support: "certified"
|
|
288
|
+
},
|
|
289
|
+
diagnostics: { endpointEnvironmentVariable: "META_BASE_URL" },
|
|
290
|
+
factory: ({ model, env, credentials }) => {
|
|
291
|
+
const baseURL = env.META_BASE_URL?.trim();
|
|
292
|
+
return createMeta({
|
|
293
|
+
apiKey: credentials.require(),
|
|
294
|
+
...baseURL ? { baseURL } : {}
|
|
295
|
+
})(model);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
descriptor: {
|
|
300
|
+
id: "qwen",
|
|
301
|
+
name: "Qwen / Alibaba Cloud Model Studio",
|
|
302
|
+
defaultModel: "qwen3.8-max",
|
|
303
|
+
credentialNames: ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
|
|
304
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
305
|
+
support: "certified"
|
|
306
|
+
},
|
|
307
|
+
diagnostics: {
|
|
308
|
+
endpointEnvironmentVariable: "QWEN_BASE_URL",
|
|
309
|
+
presence: [{ key: "workspaceId", environmentVariable: "QWEN_WORKSPACE_ID" }],
|
|
310
|
+
enums: [{
|
|
311
|
+
key: "region",
|
|
312
|
+
environmentVariable: "QWEN_REGION",
|
|
313
|
+
allowedValues: QWEN_REGIONS
|
|
314
|
+
}]
|
|
315
|
+
},
|
|
316
|
+
factory: ({ model, env, credentials }) => {
|
|
317
|
+
const baseURL = env.QWEN_BASE_URL?.trim();
|
|
318
|
+
const workspaceId = env.QWEN_WORKSPACE_ID?.trim();
|
|
319
|
+
const region = optionalQwenRegion(env.QWEN_REGION?.trim());
|
|
320
|
+
return createQwen({
|
|
321
|
+
apiKey: credentials.require(),
|
|
322
|
+
...baseURL ? { baseURL } : {},
|
|
323
|
+
...workspaceId ? { workspaceId } : {},
|
|
324
|
+
...region ? { region } : {}
|
|
325
|
+
})(model);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
descriptor: {
|
|
330
|
+
id: "openai",
|
|
331
|
+
name: "OpenAI",
|
|
332
|
+
defaultModel: "gpt-5.4",
|
|
333
|
+
credentialNames: ["OPENAI_API_KEY"],
|
|
334
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
335
|
+
support: "certified"
|
|
336
|
+
},
|
|
337
|
+
diagnostics: { endpointEnvironmentVariable: "OPENAI_BASE_URL" },
|
|
338
|
+
factory: ({ model, env, credentials }) => {
|
|
339
|
+
const baseURL = env.OPENAI_BASE_URL?.trim();
|
|
340
|
+
return createOpenAI({
|
|
341
|
+
apiKey: credentials.require(),
|
|
342
|
+
...baseURL ? { baseURL } : {}
|
|
343
|
+
})(model);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
descriptor: {
|
|
348
|
+
id: "gemini",
|
|
349
|
+
name: "Google Gemini",
|
|
350
|
+
defaultModel: "gemini-3.6-flash",
|
|
351
|
+
credentialNames: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
|
|
352
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
353
|
+
support: "provisional"
|
|
354
|
+
},
|
|
355
|
+
diagnostics: { endpointEnvironmentVariable: "GEMINI_BASE_URL" },
|
|
356
|
+
factory: ({ model, env, credentials }) => {
|
|
357
|
+
const baseURL = env.GEMINI_BASE_URL?.trim();
|
|
358
|
+
return createGemini({
|
|
359
|
+
apiKey: credentials.require(),
|
|
360
|
+
...baseURL ? { baseURL } : {}
|
|
361
|
+
})(model);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
]);
|
|
365
|
+
var DEFAULT_PROVIDER_REGISTRY = createProviderRegistry(BUILTIN_PROVIDER_REGISTRATIONS);
|
|
366
|
+
var PROVIDER_DESCRIPTORS = DEFAULT_PROVIDER_REGISTRY.descriptors;
|
|
367
|
+
|
|
368
|
+
// src/config.ts
|
|
21
369
|
var DEFAULT_ALLOWED_CHECKS = ["test", "typecheck", "lint", "build"];
|
|
22
370
|
var HARNESS_CONFIG_SCHEMA_VERSION = 4;
|
|
23
371
|
var HARNESS_STORE_BACKENDS = ["sqlite", "file"];
|
|
@@ -73,45 +421,6 @@ var DEFAULT_SUBAGENT_BUDGET = {
|
|
|
73
421
|
maxTotalTokens: 36000,
|
|
74
422
|
includeChildRuns: false
|
|
75
423
|
};
|
|
76
|
-
var HARNESS_PROVIDER_CAPABILITIES = [
|
|
77
|
-
"streaming",
|
|
78
|
-
"tool-calling",
|
|
79
|
-
"approval-resume"
|
|
80
|
-
];
|
|
81
|
-
var PROVIDER_DESCRIPTORS = [
|
|
82
|
-
{
|
|
83
|
-
id: "meta",
|
|
84
|
-
name: "Meta Model API",
|
|
85
|
-
defaultModel: "muse-spark-1.2",
|
|
86
|
-
credentialNames: ["MODEL_API_KEY"],
|
|
87
|
-
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
88
|
-
support: "certified"
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
id: "qwen",
|
|
92
|
-
name: "Qwen / Alibaba Cloud Model Studio",
|
|
93
|
-
defaultModel: "qwen3.8-max",
|
|
94
|
-
credentialNames: ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
|
|
95
|
-
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
96
|
-
support: "certified"
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
id: "openai",
|
|
100
|
-
name: "OpenAI",
|
|
101
|
-
defaultModel: "gpt-5.4",
|
|
102
|
-
credentialNames: ["OPENAI_API_KEY"],
|
|
103
|
-
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
104
|
-
support: "certified"
|
|
105
|
-
}
|
|
106
|
-
];
|
|
107
|
-
var QWEN_REGIONS = [
|
|
108
|
-
"singapore",
|
|
109
|
-
"beijing",
|
|
110
|
-
"hong-kong",
|
|
111
|
-
"tokyo",
|
|
112
|
-
"frankfurt",
|
|
113
|
-
"virginia"
|
|
114
|
-
];
|
|
115
424
|
var integerOption = (name, input, envValue, fallback, minimum, maximum) => {
|
|
116
425
|
const value = input ?? (envValue === undefined ? fallback : Number(envValue));
|
|
117
426
|
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
@@ -140,7 +449,7 @@ var nonNegativeNumberOption = (name, input, envValue) => {
|
|
|
140
449
|
}
|
|
141
450
|
return value;
|
|
142
451
|
};
|
|
143
|
-
var defaultHarnessNamespace = (workspace) => `workspace-${
|
|
452
|
+
var defaultHarnessNamespace = (workspace) => `workspace-${createHash2("sha256").update(workspace).digest("hex").slice(0, 16)}`;
|
|
144
453
|
var canonicalStateDirectory = (requestedWorkspace, canonicalWorkspace, configured) => {
|
|
145
454
|
if (!configured) {
|
|
146
455
|
return path.join(canonicalWorkspace, ".zhivex-harness", "runs");
|
|
@@ -242,26 +551,14 @@ var canonicalWorkspaceFile = (requestedWorkspace, canonicalWorkspace, configured
|
|
|
242
551
|
const insideRequestedWorkspace = relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
243
552
|
return insideRequestedWorkspace ? path.resolve(canonicalWorkspace, relative) : requestedPath;
|
|
244
553
|
};
|
|
245
|
-
var parseProvider = (value) =>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return normalized;
|
|
249
|
-
}
|
|
250
|
-
throw new Error(`Unknown provider: ${value}. Use meta, qwen, or openai.`);
|
|
251
|
-
};
|
|
252
|
-
var providerDescriptor = (provider) => {
|
|
253
|
-
const descriptor = PROVIDER_DESCRIPTORS.find((entry) => entry.id === provider);
|
|
254
|
-
if (!descriptor) {
|
|
255
|
-
throw new Error(`No descriptor is available for provider ${provider}.`);
|
|
256
|
-
}
|
|
257
|
-
return descriptor;
|
|
258
|
-
};
|
|
259
|
-
var resolveHarnessConfig = (input = {}) => {
|
|
554
|
+
var parseProvider = (value, registry = DEFAULT_PROVIDER_REGISTRY) => registry.parse(value);
|
|
555
|
+
var providerDescriptor = (provider, registry = DEFAULT_PROVIDER_REGISTRY) => registry.descriptor(provider);
|
|
556
|
+
var resolveHarnessConfig = (input = {}, registry = DEFAULT_PROVIDER_REGISTRY) => {
|
|
260
557
|
if (input.schemaVersion !== undefined && input.schemaVersion !== HARNESS_CONFIG_SCHEMA_VERSION) {
|
|
261
558
|
throw new Error(`Unsupported config schema version: ${input.schemaVersion}. Expected ${HARNESS_CONFIG_SCHEMA_VERSION}.`);
|
|
262
559
|
}
|
|
263
|
-
const provider = parseProvider(input.provider ?? process.env.ZHIVEX_HARNESS_PROVIDER);
|
|
264
|
-
const descriptor = providerDescriptor(provider);
|
|
560
|
+
const provider = parseProvider(input.provider ?? process.env.ZHIVEX_HARNESS_PROVIDER, registry);
|
|
561
|
+
const descriptor = providerDescriptor(provider, registry);
|
|
265
562
|
const requestedWorkspace = path.resolve(input.workspace ?? process.cwd());
|
|
266
563
|
let workspace = requestedWorkspace;
|
|
267
564
|
try {
|
|
@@ -355,105 +652,11 @@ var resolveHarnessConfig = (input = {}) => {
|
|
|
355
652
|
...mcpConfigPath ? { mcpConfigPath } : {}
|
|
356
653
|
};
|
|
357
654
|
};
|
|
358
|
-
var
|
|
359
|
-
|
|
360
|
-
const value = env[name]?.trim();
|
|
361
|
-
if (value) {
|
|
362
|
-
return value;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
throw new Error(`Missing credentials for ${provider}. Set ${names.join(" or ")} in the environment.`);
|
|
366
|
-
};
|
|
367
|
-
var optionalQwenRegion = (value) => {
|
|
368
|
-
if (!value) {
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
if (QWEN_REGIONS.includes(value)) {
|
|
372
|
-
return value;
|
|
373
|
-
}
|
|
374
|
-
throw new Error(`Invalid QWEN_REGION: ${value}.`);
|
|
375
|
-
};
|
|
376
|
-
var createProviderModel = (config, env = process.env) => {
|
|
377
|
-
switch (config.provider) {
|
|
378
|
-
case "meta": {
|
|
379
|
-
const baseURL = env.META_BASE_URL?.trim();
|
|
380
|
-
const provider = createMeta({
|
|
381
|
-
apiKey: requiredCredential(env, "meta", ["MODEL_API_KEY"]),
|
|
382
|
-
...baseURL ? { baseURL } : {}
|
|
383
|
-
});
|
|
384
|
-
return provider(config.model);
|
|
385
|
-
}
|
|
386
|
-
case "qwen": {
|
|
387
|
-
const baseURL = env.QWEN_BASE_URL?.trim();
|
|
388
|
-
const workspaceId = env.QWEN_WORKSPACE_ID?.trim();
|
|
389
|
-
const region = optionalQwenRegion(env.QWEN_REGION?.trim());
|
|
390
|
-
const provider = createQwen({
|
|
391
|
-
apiKey: requiredCredential(env, "qwen", ["DASHSCOPE_API_KEY", "QWEN_API_KEY"]),
|
|
392
|
-
...baseURL ? { baseURL } : {},
|
|
393
|
-
...workspaceId ? { workspaceId } : {},
|
|
394
|
-
...region ? { region } : {}
|
|
395
|
-
});
|
|
396
|
-
return provider(config.model);
|
|
397
|
-
}
|
|
398
|
-
case "openai": {
|
|
399
|
-
const baseURL = env.OPENAI_BASE_URL?.trim();
|
|
400
|
-
const provider = createOpenAI({
|
|
401
|
-
apiKey: requiredCredential(env, "openai", ["OPENAI_API_KEY"]),
|
|
402
|
-
...baseURL ? { baseURL } : {}
|
|
403
|
-
});
|
|
404
|
-
return provider(config.model);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
};
|
|
408
|
-
var endpointConfiguration = (value) => {
|
|
409
|
-
const configured = Boolean(value?.trim());
|
|
410
|
-
if (!configured) {
|
|
411
|
-
return {
|
|
412
|
-
customEndpoint: false,
|
|
413
|
-
endpointValid: true,
|
|
414
|
-
endpointSecure: true
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
try {
|
|
418
|
-
const endpoint = new URL(value.trim());
|
|
419
|
-
const supportedProtocol = endpoint.protocol === "https:" || endpoint.protocol === "http:";
|
|
420
|
-
return {
|
|
421
|
-
customEndpoint: true,
|
|
422
|
-
endpointValid: supportedProtocol && !endpoint.username && !endpoint.password && !endpoint.hash,
|
|
423
|
-
endpointSecure: endpoint.protocol === "https:"
|
|
424
|
-
};
|
|
425
|
-
} catch {
|
|
426
|
-
return {
|
|
427
|
-
customEndpoint: true,
|
|
428
|
-
endpointValid: false,
|
|
429
|
-
endpointSecure: false
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
var providerAvailability = (env = process.env) => PROVIDER_DESCRIPTORS.map((descriptor) => {
|
|
434
|
-
const credentials = descriptor.credentialNames.map((name) => ({
|
|
435
|
-
name,
|
|
436
|
-
present: Boolean(env[name]?.trim())
|
|
437
|
-
}));
|
|
438
|
-
const endpointVariable = descriptor.id === "meta" ? "META_BASE_URL" : descriptor.id === "qwen" ? "QWEN_BASE_URL" : "OPENAI_BASE_URL";
|
|
439
|
-
const regionValue = descriptor.id === "qwen" ? env.QWEN_REGION?.trim() : undefined;
|
|
440
|
-
return {
|
|
441
|
-
...descriptor,
|
|
442
|
-
configured: credentials.some((credential) => credential.present),
|
|
443
|
-
credentials,
|
|
444
|
-
configuration: {
|
|
445
|
-
...endpointConfiguration(env[endpointVariable]),
|
|
446
|
-
...descriptor.id === "qwen" ? {
|
|
447
|
-
workspaceIdConfigured: Boolean(env.QWEN_WORKSPACE_ID?.trim()),
|
|
448
|
-
regionConfigured: Boolean(regionValue),
|
|
449
|
-
regionValid: !regionValue || QWEN_REGIONS.includes(regionValue)
|
|
450
|
-
} : {}
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
});
|
|
655
|
+
var createProviderModel = (config, env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => registry.createModel(config, env);
|
|
656
|
+
var providerAvailability = (env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => registry.availability(env);
|
|
454
657
|
|
|
455
658
|
// src/harness.ts
|
|
456
|
-
import { createHash as
|
|
659
|
+
import { createHash as createHash7 } from "crypto";
|
|
457
660
|
import {
|
|
458
661
|
Agent,
|
|
459
662
|
applySafetyPolicyToAgent as applySafetyPolicyToAgent2,
|
|
@@ -523,7 +726,7 @@ var selectHarnessModel = (candidates, required) => {
|
|
|
523
726
|
};
|
|
524
727
|
|
|
525
728
|
// src/edit-contracts.ts
|
|
526
|
-
import { createHash as
|
|
729
|
+
import { createHash as createHash3 } from "crypto";
|
|
527
730
|
import path2 from "path";
|
|
528
731
|
import { z } from "zod";
|
|
529
732
|
var EDIT_CONTRACT_SCHEMA_VERSION = 1;
|
|
@@ -623,7 +826,7 @@ var mutationAuditEntrySchema = z.strictObject({
|
|
|
623
826
|
timestamp: z.iso.datetime(),
|
|
624
827
|
quarantineId: z.string().min(1).max(200).optional()
|
|
625
828
|
});
|
|
626
|
-
var digest = (value) => `sha256:${
|
|
829
|
+
var digest = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
|
|
627
830
|
var canonicalProposalPayload = (changes) => ({
|
|
628
831
|
schemaVersion: EDIT_CONTRACT_SCHEMA_VERSION,
|
|
629
832
|
kind: "edit-proposal",
|
|
@@ -658,7 +861,7 @@ var editContractDocument = (kind, result) => ({
|
|
|
658
861
|
});
|
|
659
862
|
|
|
660
863
|
// src/workspace.ts
|
|
661
|
-
import { createHash as
|
|
864
|
+
import { createHash as createHash4, randomUUID } from "crypto";
|
|
662
865
|
import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "fs/promises";
|
|
663
866
|
import path3 from "path";
|
|
664
867
|
var HARD_IGNORES = new Set([".git", ".next", ".turbo", ".zhivex-harness", "coverage", "dist", "node_modules"]);
|
|
@@ -676,7 +879,7 @@ var isInside = (root, candidate) => {
|
|
|
676
879
|
return relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative);
|
|
677
880
|
};
|
|
678
881
|
var wirePath = (value) => value.split(path3.sep).join("/");
|
|
679
|
-
var digestBytes = (value) => `sha256:${
|
|
882
|
+
var digestBytes = (value) => `sha256:${createHash4("sha256").update(value).digest("hex")}`;
|
|
680
883
|
var truncate = (value, maxLength = MAX_TOOL_OUTPUT) => value.length <= maxLength ? value : `${value.slice(0, maxLength)}
|
|
681
884
|
\u2026 output truncated (${value.length - maxLength} characters omitted)`;
|
|
682
885
|
var cursorEncode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
@@ -2173,7 +2376,7 @@ var createHarnessMcpTools = async (configuration, options = {}) => {
|
|
|
2173
2376
|
var mcpConfigurationFingerprintInput = (configuration) => serializeJsonValue(configuration);
|
|
2174
2377
|
|
|
2175
2378
|
// src/orchestration.ts
|
|
2176
|
-
import { createHash as
|
|
2379
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
|
|
2177
2380
|
import {
|
|
2178
2381
|
applySafetyPolicyToAgent,
|
|
2179
2382
|
createBudgetGuard,
|
|
@@ -2220,7 +2423,7 @@ var childHarnessBinding = (parent, profile, model, toolNames, config) => ({
|
|
|
2220
2423
|
schemaVersion: 1,
|
|
2221
2424
|
id: `${parent.id}-${profile}`,
|
|
2222
2425
|
version: parent.version,
|
|
2223
|
-
fingerprint: `sha256:${
|
|
2426
|
+
fingerprint: `sha256:${createHash5("sha256").update(JSON.stringify({
|
|
2224
2427
|
parent: parent.fingerprint,
|
|
2225
2428
|
profile,
|
|
2226
2429
|
model: {
|
|
@@ -2344,7 +2547,7 @@ var runHarnessReviewGroup = async (runtime, input, profiles = ["explorer", "revi
|
|
|
2344
2547
|
};
|
|
2345
2548
|
|
|
2346
2549
|
// src/execution-environment.ts
|
|
2347
|
-
import { createHash as
|
|
2550
|
+
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
2348
2551
|
import {
|
|
2349
2552
|
chmod as chmod2,
|
|
2350
2553
|
lstat as lstat5,
|
|
@@ -2384,8 +2587,8 @@ var BUILT_IN_TOOL_NAMES = new Set([
|
|
|
2384
2587
|
"apply_environment_patch",
|
|
2385
2588
|
"environment_status"
|
|
2386
2589
|
]);
|
|
2387
|
-
var digest2 = (value) => `sha256:${
|
|
2388
|
-
var runHash = (runId) =>
|
|
2590
|
+
var digest2 = (value) => `sha256:${createHash6("sha256").update(value).digest("hex")}`;
|
|
2591
|
+
var runHash = (runId) => createHash6("sha256").update(runId).digest("hex").slice(0, 24);
|
|
2389
2592
|
var isInside3 = (root, candidate) => {
|
|
2390
2593
|
const relative = path7.relative(root, candidate);
|
|
2391
2594
|
return relative === "" || relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
|
|
@@ -3357,11 +3560,11 @@ var describeOciCommand = (command, args) => commandDisplay([command, ...args]);
|
|
|
3357
3560
|
// src/harness.ts
|
|
3358
3561
|
var APPROVAL_VERSION = "2026-08-17-v5";
|
|
3359
3562
|
var TOOL_CONTRACT_VERSION = "enforced-execution-v1";
|
|
3360
|
-
var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, executionEnvironment) => ({
|
|
3563
|
+
var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, providerTransportFingerprint, executionEnvironment) => ({
|
|
3361
3564
|
schemaVersion: 1,
|
|
3362
3565
|
id: "zhivex-harness",
|
|
3363
3566
|
version: HARNESS_VERSION,
|
|
3364
|
-
fingerprint: `sha256:${
|
|
3567
|
+
fingerprint: `sha256:${createHash7("sha256").update(JSON.stringify({
|
|
3365
3568
|
configSchemaVersion: HARNESS_CONFIG_SCHEMA_VERSION,
|
|
3366
3569
|
approvalVersion: APPROVAL_VERSION,
|
|
3367
3570
|
toolContractVersion: TOOL_CONTRACT_VERSION,
|
|
@@ -3369,6 +3572,7 @@ var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, exe
|
|
|
3369
3572
|
provider: config.provider,
|
|
3370
3573
|
model: config.model,
|
|
3371
3574
|
runtimeModel: inspectHarnessModelCapabilities(model),
|
|
3575
|
+
providerTransportFingerprint,
|
|
3372
3576
|
subagentModels: Object.fromEntries(config.orchestration.profiles.map((profile) => [
|
|
3373
3577
|
profile,
|
|
3374
3578
|
inspectHarnessModelCapabilities(subagentModels?.[profile] ?? model)
|
|
@@ -3591,32 +3795,49 @@ var createExecutionEnvironmentTools = (workspace) => ({
|
|
|
3591
3795
|
})
|
|
3592
3796
|
});
|
|
3593
3797
|
var estimateMessageTokens = (messages) => Math.max(1, Math.ceil(JSON.stringify(messages).length / 4));
|
|
3594
|
-
var
|
|
3798
|
+
var summarizeHarnessMessages = (messages) => {
|
|
3595
3799
|
const redaction = createRedactionPolicy2({ includeEmails: true });
|
|
3596
|
-
|
|
3597
|
-
const
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
return typeof part.type === "string" ? part.type : "part";
|
|
3613
|
-
});
|
|
3614
|
-
return `${index + 1}. ${record.role ?? "message"}: ${parts.join(" | ")}`;
|
|
3800
|
+
const summaries = messages.map((message, index) => {
|
|
3801
|
+
const record = message;
|
|
3802
|
+
const parts = (record.parts ?? []).map((part) => {
|
|
3803
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
3804
|
+
const text = redaction.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]").replace(/\s+/g, " ").trim();
|
|
3805
|
+
return text.length > 160 ? `${text.slice(0, 160)}\u2026` : text;
|
|
3806
|
+
}
|
|
3807
|
+
if (part.type === "tool-call") {
|
|
3808
|
+
const call = part.toolCall;
|
|
3809
|
+
return `tool-call:${typeof call?.name === "string" ? call.name : "unknown"}`;
|
|
3810
|
+
}
|
|
3811
|
+
if (part.type === "tool-result") {
|
|
3812
|
+
const result = part.toolResult;
|
|
3813
|
+
return `tool-result:${typeof result?.toolName === "string" ? result.toolName : "unknown"}`;
|
|
3814
|
+
}
|
|
3815
|
+
return typeof part.type === "string" ? part.type : "part";
|
|
3615
3816
|
});
|
|
3616
|
-
|
|
3817
|
+
return `${index + 1}. ${record.role ?? "message"}: ${parts.join(" | ")}`;
|
|
3818
|
+
});
|
|
3819
|
+
const summary = summaries.join(`
|
|
3617
3820
|
`);
|
|
3821
|
+
return summary.length > 4000 ? `${summary.slice(0, 4000)}\u2026` : summary;
|
|
3822
|
+
};
|
|
3823
|
+
var compactHarnessMessages = (messages) => {
|
|
3824
|
+
if (messages.length === 0)
|
|
3825
|
+
return [];
|
|
3826
|
+
return [
|
|
3827
|
+
{
|
|
3828
|
+
role: "user",
|
|
3829
|
+
parts: [{
|
|
3830
|
+
type: "text",
|
|
3831
|
+
text: `[Compacted conversation context]
|
|
3832
|
+
${summarizeHarnessMessages(messages)}`
|
|
3833
|
+
}]
|
|
3834
|
+
}
|
|
3835
|
+
];
|
|
3836
|
+
};
|
|
3837
|
+
var createHarnessCompactor = () => {
|
|
3838
|
+
return async ({ messages }) => {
|
|
3618
3839
|
return {
|
|
3619
|
-
summary:
|
|
3840
|
+
summary: summarizeHarnessMessages(messages),
|
|
3620
3841
|
metadata: {
|
|
3621
3842
|
strategy: "deterministic-redacted-transcript",
|
|
3622
3843
|
sourceMessages: messages.length
|
|
@@ -3679,7 +3900,7 @@ var createProviderCompatibleBudget = (config) => {
|
|
|
3679
3900
|
};
|
|
3680
3901
|
};
|
|
3681
3902
|
var createHarness = async (options = {}) => {
|
|
3682
|
-
const config = resolveHarnessConfig(options);
|
|
3903
|
+
const config = resolveHarnessConfig(options, options.providerRegistry);
|
|
3683
3904
|
const workspace = await Workspace.open(config.workspace);
|
|
3684
3905
|
await validateStateDirectory(config.workspace, config.stateDirectory);
|
|
3685
3906
|
const executionEnvironment = config.execution.backend === "oci" ? await createHarnessOciExecutionEnvironment({
|
|
@@ -3688,7 +3909,7 @@ var createHarness = async (options = {}) => {
|
|
|
3688
3909
|
stateDirectory: config.stateDirectory,
|
|
3689
3910
|
...options.ociRuntimeAdapter ? { runtime: options.ociRuntimeAdapter } : {}
|
|
3690
3911
|
}) : undefined;
|
|
3691
|
-
const model = options.modelInstance ?? createProviderModel(config, options.env ?? process.env);
|
|
3912
|
+
const model = options.modelInstance ?? createProviderModel(config, options.env ?? process.env, options.providerRegistry);
|
|
3692
3913
|
const capabilityRequirements = [...new Set([
|
|
3693
3914
|
...config.requiredCapabilities,
|
|
3694
3915
|
...config.orchestration.profiles.length > 0 || config.mcpConfigPath || options.mcpConfiguration ? ["tools", "streaming"] : []
|
|
@@ -3730,7 +3951,7 @@ var createHarness = async (options = {}) => {
|
|
|
3730
3951
|
await options.onTelemetryEvent?.(event);
|
|
3731
3952
|
};
|
|
3732
3953
|
const costGuardrails = createCostGuardrails(config);
|
|
3733
|
-
const binding = createHarnessBinding(config, mcpConfiguration, model, options.subagentModels, executionEnvironment);
|
|
3954
|
+
const binding = createHarnessBinding(config, mcpConfiguration, model, options.subagentModels, (options.providerRegistry ?? DEFAULT_PROVIDER_REGISTRY).transportFingerprint(options.env ?? process.env), executionEnvironment);
|
|
3734
3955
|
const subagentRuntime = createHarnessSubagents({
|
|
3735
3956
|
config,
|
|
3736
3957
|
parentBinding: binding,
|
|
@@ -3862,33 +4083,851 @@ var appendUserMessage = (messages, text) => [
|
|
|
3862
4083
|
import {
|
|
3863
4084
|
estimateAgentRunCost
|
|
3864
4085
|
} from "@zhivex-ai/agents/ops";
|
|
3865
|
-
var CLI_JSON_SCHEMA_VERSION = 1;
|
|
3866
|
-
var HARNESS_RESUME_METADATA_KEY = "zhivexHarnessResume";
|
|
3867
|
-
var HARNESS_RESUME_METADATA_SCHEMA_VERSION = 1;
|
|
3868
|
-
var CLI_EXIT_CODES = {
|
|
3869
|
-
success: 0,
|
|
3870
|
-
runtimeError: 1,
|
|
3871
|
-
usageError: 2,
|
|
3872
|
-
doctorFailed: 3
|
|
3873
|
-
};
|
|
3874
|
-
var COMMANDS = new Set(["run", "review", "chat", "providers", "doctor", "resume", "runs", "help", "version"]);
|
|
3875
|
-
var RUNS_COMMANDS = new Set(["list", "inspect", "cancel", "cleanup", "export"]);
|
|
3876
|
-
var RUN_STATUSES = new Set([
|
|
3877
|
-
"queued",
|
|
3878
|
-
"running",
|
|
3879
|
-
"completed",
|
|
3880
|
-
"suspended",
|
|
3881
|
-
"waiting_approval",
|
|
3882
|
-
"cancel_requested",
|
|
3883
|
-
"failed",
|
|
3884
|
-
"cancelled",
|
|
3885
|
-
"timed_out"
|
|
3886
|
-
]);
|
|
3887
4086
|
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
4087
|
+
// src/routing.ts
|
|
4088
|
+
var routeError = (value) => new Error(`Invalid route: ${value}. Use <profile>=<provider>[:<model>].`);
|
|
4089
|
+
var parseHarnessModelRoute = (value, registry = DEFAULT_PROVIDER_REGISTRY) => {
|
|
4090
|
+
const separator = value.indexOf("=");
|
|
4091
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
4092
|
+
throw routeError(value);
|
|
4093
|
+
}
|
|
4094
|
+
const profile = value.slice(0, separator).trim().toLowerCase();
|
|
4095
|
+
if (!HARNESS_SUBAGENT_PROFILES.includes(profile)) {
|
|
4096
|
+
throw routeError(value);
|
|
4097
|
+
}
|
|
4098
|
+
const target = value.slice(separator + 1).trim();
|
|
4099
|
+
const modelSeparator = target.indexOf(":");
|
|
4100
|
+
const providerInput = (modelSeparator === -1 ? target : target.slice(0, modelSeparator)).trim();
|
|
4101
|
+
const provider = registry.parse(providerInput);
|
|
4102
|
+
const configuredModel = modelSeparator === -1 ? "" : target.slice(modelSeparator + 1).trim();
|
|
4103
|
+
if (modelSeparator !== -1 && !configuredModel) {
|
|
4104
|
+
throw routeError(value);
|
|
4105
|
+
}
|
|
4106
|
+
const model = configuredModel || registry.descriptor(provider).defaultModel;
|
|
4107
|
+
if (/[\u0000-\u001f\u007f]/.test(model) || model.length > 256) {
|
|
4108
|
+
throw routeError(value);
|
|
4109
|
+
}
|
|
4110
|
+
return Object.freeze({
|
|
4111
|
+
profile,
|
|
4112
|
+
provider,
|
|
4113
|
+
model
|
|
4114
|
+
});
|
|
4115
|
+
};
|
|
4116
|
+
var resolveHarnessModelRoutes = (values = [], registry = DEFAULT_PROVIDER_REGISTRY) => {
|
|
4117
|
+
const routes = new Map;
|
|
4118
|
+
for (const value of values) {
|
|
4119
|
+
const route = parseHarnessModelRoute(value, registry);
|
|
4120
|
+
if (routes.has(route.profile)) {
|
|
4121
|
+
throw new Error(`Duplicate model route for ${route.profile}.`);
|
|
4122
|
+
}
|
|
4123
|
+
routes.set(route.profile, route);
|
|
4124
|
+
}
|
|
4125
|
+
return routes;
|
|
4126
|
+
};
|
|
4127
|
+
var createHarnessRouteModels = (routes, env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => Object.fromEntries([...routes].map(([profile, route]) => [
|
|
4128
|
+
profile,
|
|
4129
|
+
registry.createModel(route, env)
|
|
4130
|
+
]));
|
|
4131
|
+
var serializeHarnessModelRoutes = (routes) => Object.fromEntries([...routes].map(([profile, route]) => [
|
|
4132
|
+
profile,
|
|
4133
|
+
{ provider: route.provider, model: route.model }
|
|
4134
|
+
]));
|
|
4135
|
+
|
|
4136
|
+
// src/cli-stream.ts
|
|
4137
|
+
var CLI_JSON_SCHEMA_VERSION = 1;
|
|
4138
|
+
var CLI_EVENT_SCHEMA_VERSION = 1;
|
|
4139
|
+
var streamEventDocument = (event, sequence = 0) => {
|
|
4140
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0) {
|
|
4141
|
+
throw new Error("JSONL event sequence must be a non-negative integer.");
|
|
4142
|
+
}
|
|
4143
|
+
const base = {
|
|
4144
|
+
schemaVersion: CLI_EVENT_SCHEMA_VERSION,
|
|
4145
|
+
kind: "run-event",
|
|
4146
|
+
sequence,
|
|
4147
|
+
type: event.type
|
|
4148
|
+
};
|
|
4149
|
+
switch (event.type) {
|
|
4150
|
+
case "text-delta":
|
|
4151
|
+
return { ...base, textDelta: event.textDelta };
|
|
4152
|
+
case "tool-call":
|
|
4153
|
+
return { ...base, toolCallId: event.toolCall.id, toolName: event.toolCall.name };
|
|
4154
|
+
case "tool-result":
|
|
4155
|
+
return {
|
|
4156
|
+
...base,
|
|
4157
|
+
toolCallId: event.toolResult.toolCallId,
|
|
4158
|
+
toolName: event.toolResult.toolName,
|
|
4159
|
+
isError: event.toolResult.isError
|
|
4160
|
+
};
|
|
4161
|
+
case "tool-approval-request":
|
|
4162
|
+
case "agent-approval-request":
|
|
4163
|
+
return {
|
|
4164
|
+
...base,
|
|
4165
|
+
approvalRequestId: event.approval.id,
|
|
4166
|
+
approvalKind: event.approval.kind ?? "provider",
|
|
4167
|
+
toolName: event.approval.name
|
|
4168
|
+
};
|
|
4169
|
+
case "agent-approval-resolved":
|
|
4170
|
+
return {
|
|
4171
|
+
...base,
|
|
4172
|
+
approvalRequestId: event.approval.approvalRequestId,
|
|
4173
|
+
approved: event.approval.approve
|
|
4174
|
+
};
|
|
4175
|
+
case "provider-data":
|
|
4176
|
+
return { ...base, provider: event.provider };
|
|
4177
|
+
case "image-generation":
|
|
4178
|
+
return {
|
|
4179
|
+
...base,
|
|
4180
|
+
provider: event.provider,
|
|
4181
|
+
partial: event.partial,
|
|
4182
|
+
...event.id ? { id: event.id } : {},
|
|
4183
|
+
...event.index !== undefined ? { index: event.index } : {}
|
|
4184
|
+
};
|
|
4185
|
+
case "finish":
|
|
4186
|
+
return {
|
|
4187
|
+
...base,
|
|
4188
|
+
...event.finishReason ? { finishReason: event.finishReason } : {},
|
|
4189
|
+
...event.usage ? { usage: event.usage } : {}
|
|
4190
|
+
};
|
|
4191
|
+
case "error":
|
|
4192
|
+
return { ...base, error: "Provider stream failed." };
|
|
4193
|
+
case "agent-run-start":
|
|
4194
|
+
return { ...base, currentStep: event.currentStep, maxSteps: event.maxSteps };
|
|
4195
|
+
case "agent-step-start":
|
|
4196
|
+
return { ...base, stepIndex: event.stepIndex };
|
|
4197
|
+
case "agent-step-finish":
|
|
4198
|
+
return {
|
|
4199
|
+
...base,
|
|
4200
|
+
stepIndex: event.step.index,
|
|
4201
|
+
status: event.step.status,
|
|
4202
|
+
toolCalls: event.step.toolResults.length
|
|
4203
|
+
};
|
|
4204
|
+
case "agent-compaction":
|
|
4205
|
+
return {
|
|
4206
|
+
...base,
|
|
4207
|
+
compactionId: event.compaction.id,
|
|
4208
|
+
reasons: event.compaction.reasons,
|
|
4209
|
+
messageCountBefore: event.compaction.messageCountBefore,
|
|
4210
|
+
messageCountAfter: event.compaction.messageCountAfter
|
|
4211
|
+
};
|
|
4212
|
+
case "agent-run-finish":
|
|
4213
|
+
return {
|
|
4214
|
+
...base,
|
|
4215
|
+
runId: event.state.runId,
|
|
4216
|
+
status: event.status,
|
|
4217
|
+
provider: event.state.provider,
|
|
4218
|
+
model: event.state.modelId
|
|
4219
|
+
};
|
|
4220
|
+
}
|
|
4221
|
+
};
|
|
4222
|
+
var serializeStreamEvent = (event, sequence = 0) => JSON.stringify(streamEventDocument(event, sequence));
|
|
4223
|
+
var streamResultDocument = (result, sequence = 0) => {
|
|
4224
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0) {
|
|
4225
|
+
throw new Error("JSONL result sequence must be a non-negative integer.");
|
|
4226
|
+
}
|
|
4227
|
+
return {
|
|
4228
|
+
schemaVersion: CLI_EVENT_SCHEMA_VERSION,
|
|
4229
|
+
kind: "run-result",
|
|
4230
|
+
sequence,
|
|
4231
|
+
runId: result.runId,
|
|
4232
|
+
status: result.status,
|
|
4233
|
+
provider: result.provider,
|
|
4234
|
+
model: result.model,
|
|
4235
|
+
steps: result.steps,
|
|
4236
|
+
toolCalls: result.toolCalls,
|
|
4237
|
+
pendingApprovals: (result.pendingApprovals ?? []).map((approval) => ({
|
|
4238
|
+
id: approval.id,
|
|
4239
|
+
kind: approval.kind,
|
|
4240
|
+
name: approval.name,
|
|
4241
|
+
...approval.childRunId ? { childRunId: approval.childRunId } : {},
|
|
4242
|
+
...approval.childAgentId ? { childAgentId: approval.childAgentId } : {}
|
|
4243
|
+
})),
|
|
4244
|
+
children: (result.children ?? []).map((child) => ({
|
|
4245
|
+
runId: child.runId,
|
|
4246
|
+
status: child.status
|
|
4247
|
+
}))
|
|
4248
|
+
};
|
|
4249
|
+
};
|
|
4250
|
+
var serializeStreamResult = (result, sequence = 0) => JSON.stringify(streamResultDocument(result, sequence));
|
|
4251
|
+
|
|
4252
|
+
// src/sessions.ts
|
|
4253
|
+
import { createHash as createHash8, randomUUID as randomUUID4 } from "crypto";
|
|
4254
|
+
import { chmod as chmod3, lstat as lstat6, mkdir as mkdir4, open as open3, realpath as realpath4, stat as stat3 } from "fs/promises";
|
|
4255
|
+
import path8 from "path";
|
|
4256
|
+
import { createRedactionPolicy as createRedactionPolicy3 } from "@zhivex-ai/agents";
|
|
4257
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
4258
|
+
var HARNESS_SESSION_INDEX_FILE = "operations.sqlite";
|
|
4259
|
+
var HARNESS_SESSION_SCHEMA_VERSION = 1;
|
|
4260
|
+
var SESSION_RUN_STATUSES = [
|
|
4261
|
+
"created",
|
|
4262
|
+
"running",
|
|
4263
|
+
"waiting_approval",
|
|
4264
|
+
"cancel_requested",
|
|
4265
|
+
"completed",
|
|
4266
|
+
"failed",
|
|
4267
|
+
"cancelled",
|
|
4268
|
+
"timed_out"
|
|
4269
|
+
];
|
|
4270
|
+
var TERMINAL_SESSION_RUN_STATUSES = [
|
|
4271
|
+
"completed",
|
|
4272
|
+
"failed",
|
|
4273
|
+
"cancelled",
|
|
4274
|
+
"timed_out"
|
|
4275
|
+
];
|
|
4276
|
+
var DEFAULT_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
|
|
4277
|
+
var DEFAULT_MAX_SESSIONS = 200;
|
|
4278
|
+
var DEFAULT_MAX_RUNS = 1000;
|
|
4279
|
+
var DEFAULT_MAX_METADATA_BYTES = 4096;
|
|
4280
|
+
var DEFAULT_MAX_INDEX_BYTES = 16 * 1024 * 1024;
|
|
4281
|
+
var MAX_LIST_LIMIT = 200;
|
|
4282
|
+
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,255}$/;
|
|
4283
|
+
var TERMINAL_STATUSES2 = new Set(TERMINAL_SESSION_RUN_STATUSES);
|
|
4284
|
+
var ACTIVE_STATUSES = new Set([
|
|
4285
|
+
"created",
|
|
4286
|
+
"running",
|
|
4287
|
+
"waiting_approval",
|
|
4288
|
+
"cancel_requested"
|
|
4289
|
+
]);
|
|
4290
|
+
var STATUS_TRANSITIONS = {
|
|
4291
|
+
created: new Set(["created", "running", "waiting_approval", "completed", "failed", "cancelled", "timed_out"]),
|
|
4292
|
+
running: new Set(["running", "waiting_approval", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
|
|
4293
|
+
waiting_approval: new Set(["waiting_approval", "running", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
|
|
4294
|
+
cancel_requested: new Set(["cancel_requested", "cancelled", "failed", "timed_out"]),
|
|
4295
|
+
completed: new Set(["completed"]),
|
|
4296
|
+
failed: new Set(["failed"]),
|
|
4297
|
+
cancelled: new Set(["cancelled"]),
|
|
4298
|
+
timed_out: new Set(["timed_out"])
|
|
4299
|
+
};
|
|
4300
|
+
var metadataRedaction = createRedactionPolicy3({
|
|
4301
|
+
includeEmails: true,
|
|
4302
|
+
rules: [
|
|
4303
|
+
{ name: "private-key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
|
|
4304
|
+
{ name: "common-provider-key", pattern: /\b(?:sk|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{8,}\b/gi },
|
|
4305
|
+
{ name: "url-credentials", pattern: /:\/\/[^\s/@:]+:[^\s/@]+@/g, replacement: "://[REDACTED]@" }
|
|
4306
|
+
]
|
|
4307
|
+
});
|
|
4308
|
+
var byteLength = (value) => Buffer.byteLength(value, "utf8");
|
|
4309
|
+
var boundedInteger = (name, value, fallback, minimum) => {
|
|
4310
|
+
const resolved = value ?? fallback;
|
|
4311
|
+
if (!Number.isSafeInteger(resolved) || resolved < minimum) {
|
|
4312
|
+
throw new Error(`${name} must be an integer greater than or equal to ${minimum}.`);
|
|
4313
|
+
}
|
|
4314
|
+
return resolved;
|
|
4315
|
+
};
|
|
4316
|
+
var assertTimestamp = (name, value) => {
|
|
4317
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
4318
|
+
throw new Error(`${name} must be a non-negative integer timestamp.`);
|
|
4319
|
+
}
|
|
4320
|
+
};
|
|
4321
|
+
var assertIdentifier = (name, value) => {
|
|
4322
|
+
if (!ID_PATTERN.test(value)) {
|
|
4323
|
+
throw new Error(`${name} contains unsupported characters or exceeds 256 bytes.`);
|
|
4324
|
+
}
|
|
4325
|
+
if (metadataRedaction.redactText(value) !== value) {
|
|
4326
|
+
throw new Error(`${name} resembles sensitive metadata and cannot be stored in the session index.`);
|
|
4327
|
+
}
|
|
4328
|
+
return value;
|
|
4329
|
+
};
|
|
4330
|
+
var assertScopeSegment = (name, value) => {
|
|
4331
|
+
const normalized = value.trim();
|
|
4332
|
+
if (!normalized || normalized.length > 128 || /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
4333
|
+
throw new Error(`${name} must contain 1-128 printable characters.`);
|
|
4334
|
+
}
|
|
4335
|
+
return normalized;
|
|
4336
|
+
};
|
|
4337
|
+
var normalizeTitle = (value, maxMetadataBytes) => {
|
|
4338
|
+
if (value === undefined || value === null) {
|
|
4339
|
+
return value;
|
|
4340
|
+
}
|
|
4341
|
+
const normalized = metadataRedaction.redactText(value).replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
|
|
4342
|
+
if (!normalized) {
|
|
4343
|
+
throw new Error("Session title must not be empty.");
|
|
4344
|
+
}
|
|
4345
|
+
if (byteLength(normalized) > maxMetadataBytes) {
|
|
4346
|
+
throw new Error(`Session title exceeds the ${maxMetadataBytes}-byte metadata limit.`);
|
|
4347
|
+
}
|
|
4348
|
+
return normalized;
|
|
4349
|
+
};
|
|
4350
|
+
var normalizeScope = (scope) => {
|
|
4351
|
+
const tenantId = assertScopeSegment("scope.tenantId", scope.tenantId);
|
|
4352
|
+
const userId = scope.userId === undefined ? "" : assertScopeSegment("scope.userId", scope.userId);
|
|
4353
|
+
const namespace = scope.namespace === undefined ? "" : assertScopeSegment("scope.namespace", scope.namespace);
|
|
4354
|
+
return `${tenantId}\x00${userId}\x00${namespace}`;
|
|
4355
|
+
};
|
|
4356
|
+
var stableKey = (kind, value) => createHash8("sha256").update(kind).update("\x00").update(value).digest("hex");
|
|
4357
|
+
var sessionId = () => `ses_${randomUUID4()}`;
|
|
4358
|
+
var turnId = () => `turn_${randomUUID4()}`;
|
|
4359
|
+
var isMissing = (error) => error.code === "ENOENT";
|
|
4360
|
+
var ensurePrivateDatabase = async (workspace, stateDirectory) => {
|
|
4361
|
+
const requestedStateDirectory = path8.resolve(stateDirectory);
|
|
4362
|
+
await validateStateDirectory(workspace, requestedStateDirectory);
|
|
4363
|
+
await mkdir4(requestedStateDirectory, { recursive: true, mode: 448 });
|
|
4364
|
+
const directoryEntry = await lstat6(requestedStateDirectory);
|
|
4365
|
+
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
|
|
4366
|
+
throw new Error(`The session state directory must be a real directory: ${requestedStateDirectory}.`);
|
|
4367
|
+
}
|
|
4368
|
+
await chmod3(requestedStateDirectory, 448);
|
|
4369
|
+
const canonicalStateDirectory2 = await realpath4(requestedStateDirectory);
|
|
4370
|
+
const databasePath = path8.join(canonicalStateDirectory2, HARNESS_SESSION_INDEX_FILE);
|
|
4371
|
+
let databaseEntry;
|
|
4372
|
+
try {
|
|
4373
|
+
databaseEntry = await lstat6(databasePath);
|
|
4374
|
+
} catch (error) {
|
|
4375
|
+
if (!isMissing(error)) {
|
|
4376
|
+
throw error;
|
|
4377
|
+
}
|
|
4378
|
+
try {
|
|
4379
|
+
const handle = await open3(databasePath, "wx", 384);
|
|
4380
|
+
await handle.close();
|
|
4381
|
+
} catch (creationError) {
|
|
4382
|
+
if (creationError.code !== "EEXIST") {
|
|
4383
|
+
throw creationError;
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
databaseEntry = await lstat6(databasePath);
|
|
4387
|
+
}
|
|
4388
|
+
if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
|
|
4389
|
+
throw new Error(`The session SQLite path must be a real file: ${databasePath}.`);
|
|
4390
|
+
}
|
|
4391
|
+
await chmod3(databasePath, 384);
|
|
4392
|
+
return { databasePath, databaseEntry };
|
|
4393
|
+
};
|
|
4394
|
+
var toRunReference = (row) => ({
|
|
4395
|
+
turnId: row.turn_id,
|
|
4396
|
+
...row.source_turn_id ? { sourceTurnId: row.source_turn_id } : {},
|
|
4397
|
+
sequence: row.ordinal,
|
|
4398
|
+
runId: row.run_id,
|
|
4399
|
+
provider: row.provider,
|
|
4400
|
+
model: row.model,
|
|
4401
|
+
...row.role ? { role: row.role } : {},
|
|
4402
|
+
status: row.status,
|
|
4403
|
+
createdAt: row.created_at,
|
|
4404
|
+
updatedAt: row.updated_at,
|
|
4405
|
+
...row.completed_at === null ? {} : { completedAt: row.completed_at }
|
|
4406
|
+
});
|
|
4407
|
+
var metadataSize = (input) => Object.values(input).reduce((total, value) => total + (typeof value === "string" ? byteLength(value) : 0), 0);
|
|
4408
|
+
var isTerminal = (status) => TERMINAL_STATUSES2.has(status);
|
|
4409
|
+
var openCliSessionStore = async (options) => {
|
|
4410
|
+
const workspace = await realpath4(path8.resolve(options.workspace));
|
|
4411
|
+
const workspaceEntry = await stat3(workspace);
|
|
4412
|
+
if (!workspaceEntry.isDirectory()) {
|
|
4413
|
+
throw new Error(`The session workspace is not a directory: ${workspace}.`);
|
|
4414
|
+
}
|
|
4415
|
+
const scopeValue2 = normalizeScope(options.scope);
|
|
4416
|
+
const workspaceKey = stableKey("workspace", workspace);
|
|
4417
|
+
const scopeKey = stableKey("scope", scopeValue2);
|
|
4418
|
+
const retentionMs = boundedInteger("retentionMs", options.retentionMs, DEFAULT_RETENTION_MS, 0);
|
|
4419
|
+
const maxSessions = boundedInteger("maxSessionsPerWorkspace", options.maxSessionsPerWorkspace, DEFAULT_MAX_SESSIONS, 1);
|
|
4420
|
+
const maxRuns = boundedInteger("maxRunsPerSession", options.maxRunsPerSession, DEFAULT_MAX_RUNS, 1);
|
|
4421
|
+
const maxMetadataBytes = boundedInteger("maxMetadataBytes", options.maxMetadataBytes, DEFAULT_MAX_METADATA_BYTES, 32);
|
|
4422
|
+
const maxIndexBytes = boundedInteger("maxIndexBytes", options.maxIndexBytes, DEFAULT_MAX_INDEX_BYTES, 1024);
|
|
4423
|
+
const now = options.now ?? Date.now;
|
|
4424
|
+
const { databasePath, databaseEntry } = await ensurePrivateDatabase(workspace, options.stateDirectory);
|
|
4425
|
+
const database = new Database2(databasePath, { create: false, strict: true });
|
|
4426
|
+
let closed = false;
|
|
4427
|
+
const assertOpen = () => {
|
|
4428
|
+
if (closed) {
|
|
4429
|
+
throw new Error("The CLI session store is closed.");
|
|
4430
|
+
}
|
|
4431
|
+
};
|
|
4432
|
+
const transaction = (operation) => {
|
|
4433
|
+
assertOpen();
|
|
4434
|
+
database.exec("BEGIN IMMEDIATE");
|
|
4435
|
+
try {
|
|
4436
|
+
const result = operation();
|
|
4437
|
+
const size = database.query(`
|
|
4438
|
+
SELECT
|
|
4439
|
+
COALESCE((SELECT SUM(metadata_bytes) FROM zhivex_cli_sessions WHERE workspace_key = ?1 AND scope_key = ?2), 0) +
|
|
4440
|
+
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)
|
|
4441
|
+
AS size
|
|
4442
|
+
`).get(workspaceKey, scopeKey)?.size ?? 0;
|
|
4443
|
+
if (size > maxIndexBytes) {
|
|
4444
|
+
throw new Error(`Session index metadata exceeds the ${maxIndexBytes}-byte limit.`);
|
|
4445
|
+
}
|
|
4446
|
+
database.exec("COMMIT");
|
|
4447
|
+
return result;
|
|
4448
|
+
} catch (error) {
|
|
4449
|
+
try {
|
|
4450
|
+
database.exec("ROLLBACK");
|
|
4451
|
+
} catch {}
|
|
4452
|
+
throw error;
|
|
4453
|
+
}
|
|
4454
|
+
};
|
|
4455
|
+
const ensureRevision = (row, expected) => {
|
|
4456
|
+
if (expected !== undefined && row.revision !== expected) {
|
|
4457
|
+
throw new Error(`Session ${row.session_id} changed concurrently (expected revision ${expected}, found ${row.revision}).`);
|
|
4458
|
+
}
|
|
4459
|
+
};
|
|
4460
|
+
const selectSessionRow = (candidateSessionId, includeDeleted = false) => {
|
|
4461
|
+
assertIdentifier("sessionId", candidateSessionId);
|
|
4462
|
+
return database.query(`
|
|
4463
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
4464
|
+
created_at, updated_at, archived_at, deleted_at
|
|
4465
|
+
FROM zhivex_cli_sessions
|
|
4466
|
+
WHERE session_id = ?1 AND workspace_key = ?2 AND scope_key = ?3
|
|
4467
|
+
${includeDeleted ? "" : "AND deleted_at IS NULL"}
|
|
4468
|
+
`).get(candidateSessionId, workspaceKey, scopeKey);
|
|
4469
|
+
};
|
|
4470
|
+
const requireSessionRow = (candidateSessionId, includeDeleted = false) => {
|
|
4471
|
+
const row = selectSessionRow(candidateSessionId, includeDeleted);
|
|
4472
|
+
if (!row) {
|
|
4473
|
+
throw new Error(`Session ${candidateSessionId} was not found in this workspace and scope.`);
|
|
4474
|
+
}
|
|
4475
|
+
return row;
|
|
4476
|
+
};
|
|
4477
|
+
const selectRuns = (candidateSessionId) => database.query(`
|
|
4478
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
4479
|
+
created_at, updated_at, completed_at
|
|
4480
|
+
FROM zhivex_cli_session_runs
|
|
4481
|
+
WHERE session_id = ?1
|
|
4482
|
+
ORDER BY ordinal ASC
|
|
4483
|
+
`).all(candidateSessionId).map(toRunReference);
|
|
4484
|
+
const nextActivitySequence = () => database.query(`
|
|
4485
|
+
SELECT COALESCE(MAX(activity_seq), 0) + 1 AS count
|
|
4486
|
+
FROM zhivex_cli_sessions
|
|
4487
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4488
|
+
`).get(workspaceKey, scopeKey)?.count ?? 1;
|
|
4489
|
+
const sessionBase = (row) => ({
|
|
4490
|
+
schemaVersion: HARNESS_SESSION_SCHEMA_VERSION,
|
|
4491
|
+
sessionId: row.session_id,
|
|
4492
|
+
workspaceKey,
|
|
4493
|
+
scopeKey,
|
|
4494
|
+
...row.title ? { title: row.title } : {},
|
|
4495
|
+
...row.parent_session_id ? { parentSessionId: row.parent_session_id } : {},
|
|
4496
|
+
...row.forked_from_turn_id ? { forkedFromTurnId: row.forked_from_turn_id } : {},
|
|
4497
|
+
revision: row.revision,
|
|
4498
|
+
createdAt: row.created_at,
|
|
4499
|
+
updatedAt: row.updated_at,
|
|
4500
|
+
...row.archived_at === null ? {} : { archivedAt: row.archived_at },
|
|
4501
|
+
...row.deleted_at === null ? {} : { deletedAt: row.deleted_at }
|
|
4502
|
+
});
|
|
4503
|
+
const hydrate = (row) => ({
|
|
4504
|
+
...sessionBase(row),
|
|
4505
|
+
runs: selectRuns(row.session_id)
|
|
4506
|
+
});
|
|
4507
|
+
const latestRunRow = (candidateSessionId) => database.query(`
|
|
4508
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
4509
|
+
created_at, updated_at, completed_at
|
|
4510
|
+
FROM zhivex_cli_session_runs
|
|
4511
|
+
WHERE session_id = ?1
|
|
4512
|
+
ORDER BY ordinal DESC
|
|
4513
|
+
LIMIT 1
|
|
4514
|
+
`).get(candidateSessionId);
|
|
4515
|
+
const assertNoActiveRun = (candidateSessionId, action) => {
|
|
4516
|
+
const latest = latestRunRow(candidateSessionId);
|
|
4517
|
+
if (latest && ACTIVE_STATUSES.has(latest.status)) {
|
|
4518
|
+
throw new Error(`Cannot ${action} session ${candidateSessionId} while run ${latest.run_id} is ${latest.status}.`);
|
|
4519
|
+
}
|
|
4520
|
+
};
|
|
4521
|
+
const insertRun = (candidateSessionId, input, sequence, sourceTurnId) => {
|
|
4522
|
+
const runId = assertIdentifier("runId", input.runId);
|
|
4523
|
+
const provider = assertIdentifier("provider", input.provider);
|
|
4524
|
+
const model = assertIdentifier("model", input.model);
|
|
4525
|
+
const role = input.role === undefined ? null : assertIdentifier("role", input.role);
|
|
4526
|
+
const status = input.status ?? "created";
|
|
4527
|
+
if (!SESSION_RUN_STATUSES.includes(status)) {
|
|
4528
|
+
throw new Error(`Unsupported session run status: ${String(status)}.`);
|
|
4529
|
+
}
|
|
4530
|
+
const createdAt = input.createdAt ?? now();
|
|
4531
|
+
assertTimestamp("createdAt", createdAt);
|
|
4532
|
+
const entryBytes = metadataSize({ runId, provider, model, role });
|
|
4533
|
+
if (entryBytes > maxMetadataBytes) {
|
|
4534
|
+
throw new Error(`Run reference metadata exceeds the ${maxMetadataBytes}-byte limit.`);
|
|
4535
|
+
}
|
|
4536
|
+
const candidateTurnId = turnId();
|
|
4537
|
+
database.query(`
|
|
4538
|
+
INSERT INTO zhivex_cli_session_runs (
|
|
4539
|
+
turn_id, source_turn_id, session_id, ordinal, run_id, provider, model, role,
|
|
4540
|
+
status, created_at, updated_at, completed_at, metadata_bytes
|
|
4541
|
+
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10, ?11, ?12)
|
|
4542
|
+
`).run(candidateTurnId, sourceTurnId ?? null, candidateSessionId, sequence, runId, provider, model, role, status, createdAt, isTerminal(status) ? createdAt : null, entryBytes);
|
|
4543
|
+
};
|
|
4544
|
+
try {
|
|
4545
|
+
const openedEntry = await lstat6(databasePath);
|
|
4546
|
+
if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
|
|
4547
|
+
throw new Error(`The session SQLite path changed while it was being opened: ${databasePath}.`);
|
|
4548
|
+
}
|
|
4549
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
4550
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
4551
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
4552
|
+
database.exec(`
|
|
4553
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_session_schema (
|
|
4554
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
4555
|
+
version INTEGER NOT NULL
|
|
4556
|
+
);
|
|
4557
|
+
INSERT OR IGNORE INTO zhivex_cli_session_schema (singleton, version)
|
|
4558
|
+
VALUES (1, ${HARNESS_SESSION_SCHEMA_VERSION});
|
|
4559
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_sessions (
|
|
4560
|
+
session_id TEXT PRIMARY KEY,
|
|
4561
|
+
workspace_key TEXT NOT NULL,
|
|
4562
|
+
scope_key TEXT NOT NULL,
|
|
4563
|
+
title TEXT,
|
|
4564
|
+
parent_session_id TEXT,
|
|
4565
|
+
forked_from_turn_id TEXT,
|
|
4566
|
+
revision INTEGER NOT NULL DEFAULT 0,
|
|
4567
|
+
activity_seq INTEGER NOT NULL,
|
|
4568
|
+
created_at INTEGER NOT NULL,
|
|
4569
|
+
updated_at INTEGER NOT NULL,
|
|
4570
|
+
archived_at INTEGER,
|
|
4571
|
+
deleted_at INTEGER,
|
|
4572
|
+
metadata_bytes INTEGER NOT NULL,
|
|
4573
|
+
CHECK (length(session_id) <= 260),
|
|
4574
|
+
CHECK (metadata_bytes >= 0)
|
|
4575
|
+
);
|
|
4576
|
+
CREATE INDEX IF NOT EXISTS zhivex_cli_sessions_latest
|
|
4577
|
+
ON zhivex_cli_sessions (workspace_key, scope_key, deleted_at, archived_at, activity_seq DESC);
|
|
4578
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_session_runs (
|
|
4579
|
+
turn_id TEXT PRIMARY KEY,
|
|
4580
|
+
source_turn_id TEXT,
|
|
4581
|
+
session_id TEXT NOT NULL REFERENCES zhivex_cli_sessions(session_id) ON DELETE CASCADE,
|
|
4582
|
+
ordinal INTEGER NOT NULL,
|
|
4583
|
+
run_id TEXT NOT NULL,
|
|
4584
|
+
provider TEXT NOT NULL,
|
|
4585
|
+
model TEXT NOT NULL,
|
|
4586
|
+
role TEXT,
|
|
4587
|
+
status TEXT NOT NULL CHECK (status IN ('created', 'running', 'waiting_approval', 'cancel_requested', 'completed', 'failed', 'cancelled', 'timed_out')),
|
|
4588
|
+
created_at INTEGER NOT NULL,
|
|
4589
|
+
updated_at INTEGER NOT NULL,
|
|
4590
|
+
completed_at INTEGER,
|
|
4591
|
+
metadata_bytes INTEGER NOT NULL,
|
|
4592
|
+
UNIQUE (session_id, ordinal),
|
|
4593
|
+
UNIQUE (session_id, run_id),
|
|
4594
|
+
CHECK (ordinal >= 0),
|
|
4595
|
+
CHECK (metadata_bytes >= 0)
|
|
4596
|
+
);
|
|
4597
|
+
CREATE INDEX IF NOT EXISTS zhivex_cli_session_runs_latest
|
|
4598
|
+
ON zhivex_cli_session_runs (session_id, ordinal DESC);
|
|
4599
|
+
`);
|
|
4600
|
+
const schemaVersion = database.query("SELECT version FROM zhivex_cli_session_schema WHERE singleton = 1").get()?.version;
|
|
4601
|
+
if (schemaVersion !== HARNESS_SESSION_SCHEMA_VERSION) {
|
|
4602
|
+
throw new Error(`Unsupported CLI session schema version: ${String(schemaVersion)}.`);
|
|
4603
|
+
}
|
|
4604
|
+
await chmod3(databasePath, 384);
|
|
4605
|
+
} catch (error) {
|
|
4606
|
+
database.close(false);
|
|
4607
|
+
throw error;
|
|
4608
|
+
}
|
|
4609
|
+
const prune = async (input = {}) => transaction(() => {
|
|
4610
|
+
const before = input.before ?? Math.max(0, now() - retentionMs);
|
|
4611
|
+
assertTimestamp("before", before);
|
|
4612
|
+
const deletedSessions = database.query(`
|
|
4613
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
4614
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4615
|
+
AND updated_at < ?3
|
|
4616
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
4617
|
+
`).get(workspaceKey, scopeKey, before)?.count ?? 0;
|
|
4618
|
+
database.query(`
|
|
4619
|
+
DELETE FROM zhivex_cli_sessions
|
|
4620
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4621
|
+
AND updated_at < ?3
|
|
4622
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
4623
|
+
`).run(workspaceKey, scopeKey, before);
|
|
4624
|
+
return { before, deletedSessions };
|
|
4625
|
+
});
|
|
4626
|
+
const store = {
|
|
4627
|
+
databasePath,
|
|
4628
|
+
workspaceKey,
|
|
4629
|
+
scopeKey,
|
|
4630
|
+
async create(input = {}) {
|
|
4631
|
+
return transaction(() => {
|
|
4632
|
+
const cutoff = Math.max(0, now() - retentionMs);
|
|
4633
|
+
database.query(`
|
|
4634
|
+
DELETE FROM zhivex_cli_sessions
|
|
4635
|
+
WHERE workspace_key = ?1 AND scope_key = ?2 AND updated_at < ?3
|
|
4636
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
4637
|
+
`).run(workspaceKey, scopeKey, cutoff);
|
|
4638
|
+
const count = database.query(`
|
|
4639
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
4640
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4641
|
+
`).get(workspaceKey, scopeKey)?.count ?? 0;
|
|
4642
|
+
if (count >= maxSessions) {
|
|
4643
|
+
throw new Error(`Session limit reached for this workspace and scope (${maxSessions}). Archive and prune an older session.`);
|
|
4644
|
+
}
|
|
4645
|
+
const createdAt = now();
|
|
4646
|
+
assertTimestamp("now", createdAt);
|
|
4647
|
+
const title = normalizeTitle(input.title, maxMetadataBytes) ?? null;
|
|
4648
|
+
const candidateSessionId = sessionId();
|
|
4649
|
+
const activitySequence = nextActivitySequence();
|
|
4650
|
+
database.query(`
|
|
4651
|
+
INSERT INTO zhivex_cli_sessions (
|
|
4652
|
+
session_id, workspace_key, scope_key, title, revision, created_at,
|
|
4653
|
+
updated_at, activity_seq, metadata_bytes
|
|
4654
|
+
) VALUES (?1, ?2, ?3, ?4, 0, ?5, ?5, ?6, ?7)
|
|
4655
|
+
`).run(candidateSessionId, workspaceKey, scopeKey, title, createdAt, activitySequence, metadataSize({ title }));
|
|
4656
|
+
if (input.initialRun) {
|
|
4657
|
+
insertRun(candidateSessionId, input.initialRun, 0);
|
|
4658
|
+
}
|
|
4659
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
4660
|
+
});
|
|
4661
|
+
},
|
|
4662
|
+
async get(candidateSessionId, input = {}) {
|
|
4663
|
+
assertOpen();
|
|
4664
|
+
const row = selectSessionRow(candidateSessionId, input.includeDeleted ?? false);
|
|
4665
|
+
return row ? hydrate(row) : undefined;
|
|
4666
|
+
},
|
|
4667
|
+
async list(query = {}) {
|
|
4668
|
+
assertOpen();
|
|
4669
|
+
const limit = boundedInteger("limit", query.limit, 50, 1);
|
|
4670
|
+
if (limit > MAX_LIST_LIMIT) {
|
|
4671
|
+
throw new Error(`limit cannot exceed ${MAX_LIST_LIMIT}.`);
|
|
4672
|
+
}
|
|
4673
|
+
const rows = database.query(`
|
|
4674
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
4675
|
+
created_at, updated_at, archived_at, deleted_at
|
|
4676
|
+
FROM zhivex_cli_sessions
|
|
4677
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4678
|
+
${query.includeArchived ? "" : "AND archived_at IS NULL"}
|
|
4679
|
+
${query.includeDeleted ? "" : "AND deleted_at IS NULL"}
|
|
4680
|
+
ORDER BY activity_seq DESC
|
|
4681
|
+
LIMIT ?3
|
|
4682
|
+
`).all(workspaceKey, scopeKey, limit);
|
|
4683
|
+
return rows.map((row) => {
|
|
4684
|
+
const latest = latestRunRow(row.session_id);
|
|
4685
|
+
const runCount = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(row.session_id)?.count ?? 0;
|
|
4686
|
+
return {
|
|
4687
|
+
...sessionBase(row),
|
|
4688
|
+
runCount,
|
|
4689
|
+
...latest ? { latestRun: toRunReference(latest) } : {}
|
|
4690
|
+
};
|
|
4691
|
+
});
|
|
4692
|
+
},
|
|
4693
|
+
async latest(input = {}) {
|
|
4694
|
+
assertOpen();
|
|
4695
|
+
const row = database.query(`
|
|
4696
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
4697
|
+
created_at, updated_at, archived_at, deleted_at
|
|
4698
|
+
FROM zhivex_cli_sessions
|
|
4699
|
+
WHERE workspace_key = ?1 AND scope_key = ?2 AND deleted_at IS NULL
|
|
4700
|
+
${input.includeArchived ? "" : "AND archived_at IS NULL"}
|
|
4701
|
+
ORDER BY activity_seq DESC
|
|
4702
|
+
LIMIT 1
|
|
4703
|
+
`).get(workspaceKey, scopeKey);
|
|
4704
|
+
return row ? hydrate(row) : undefined;
|
|
4705
|
+
},
|
|
4706
|
+
async update(candidateSessionId, input) {
|
|
4707
|
+
return transaction(() => {
|
|
4708
|
+
const row = requireSessionRow(candidateSessionId);
|
|
4709
|
+
ensureRevision(row, input.expectedRevision);
|
|
4710
|
+
if (input.archived === true) {
|
|
4711
|
+
assertNoActiveRun(candidateSessionId, "archive");
|
|
4712
|
+
}
|
|
4713
|
+
const title = input.title === undefined ? row.title : normalizeTitle(input.title, maxMetadataBytes) ?? null;
|
|
4714
|
+
const updatedAt = now();
|
|
4715
|
+
assertTimestamp("now", updatedAt);
|
|
4716
|
+
const archivedAt = input.archived === undefined ? row.archived_at : input.archived ? updatedAt : null;
|
|
4717
|
+
const activitySequence = nextActivitySequence();
|
|
4718
|
+
database.query(`
|
|
4719
|
+
UPDATE zhivex_cli_sessions
|
|
4720
|
+
SET title = ?1, archived_at = ?2, updated_at = ?3, revision = revision + 1,
|
|
4721
|
+
metadata_bytes = ?4, activity_seq = ?5
|
|
4722
|
+
WHERE session_id = ?6 AND workspace_key = ?7 AND scope_key = ?8 AND revision = ?9
|
|
4723
|
+
`).run(title, archivedAt, updatedAt, metadataSize({ title }), activitySequence, candidateSessionId, workspaceKey, scopeKey, row.revision);
|
|
4724
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
4725
|
+
});
|
|
4726
|
+
},
|
|
4727
|
+
async rename(candidateSessionId, title, input = {}) {
|
|
4728
|
+
return store.update(candidateSessionId, {
|
|
4729
|
+
title,
|
|
4730
|
+
...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
|
|
4731
|
+
});
|
|
4732
|
+
},
|
|
4733
|
+
async appendRun(candidateSessionId, input, appendOptions = {}) {
|
|
4734
|
+
return transaction(() => {
|
|
4735
|
+
const row = requireSessionRow(candidateSessionId);
|
|
4736
|
+
ensureRevision(row, appendOptions.expectedRevision);
|
|
4737
|
+
if (row.archived_at !== null) {
|
|
4738
|
+
throw new Error(`Cannot append a run to archived session ${candidateSessionId}.`);
|
|
4739
|
+
}
|
|
4740
|
+
const existing = database.query(`
|
|
4741
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
4742
|
+
created_at, updated_at, completed_at
|
|
4743
|
+
FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
|
|
4744
|
+
`).get(candidateSessionId, input.runId);
|
|
4745
|
+
if (existing) {
|
|
4746
|
+
const requestedStatus = input.status ?? "created";
|
|
4747
|
+
if (existing.provider === input.provider && existing.model === input.model && (existing.role ?? undefined) === input.role && existing.status === requestedStatus) {
|
|
4748
|
+
return hydrate(row);
|
|
4749
|
+
}
|
|
4750
|
+
throw new Error(`Run ${input.runId} is already immutably bound to ${existing.provider}/${existing.model} in this session.`);
|
|
4751
|
+
}
|
|
4752
|
+
assertNoActiveRun(candidateSessionId, "start another run in");
|
|
4753
|
+
const count = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(candidateSessionId)?.count ?? 0;
|
|
4754
|
+
if (count >= maxRuns) {
|
|
4755
|
+
throw new Error(`Run-reference limit reached for session ${candidateSessionId} (${maxRuns}).`);
|
|
4756
|
+
}
|
|
4757
|
+
insertRun(candidateSessionId, input, count);
|
|
4758
|
+
const updatedAt = now();
|
|
4759
|
+
assertTimestamp("now", updatedAt);
|
|
4760
|
+
const activitySequence = nextActivitySequence();
|
|
4761
|
+
database.query(`
|
|
4762
|
+
UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
4763
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
4764
|
+
`).run(updatedAt, activitySequence, candidateSessionId, row.revision);
|
|
4765
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
4766
|
+
});
|
|
4767
|
+
},
|
|
4768
|
+
async updateRun(candidateSessionId, runId, input) {
|
|
4769
|
+
return transaction(() => {
|
|
4770
|
+
const session = requireSessionRow(candidateSessionId);
|
|
4771
|
+
ensureRevision(session, input.expectedSessionRevision);
|
|
4772
|
+
assertIdentifier("runId", runId);
|
|
4773
|
+
const run = database.query(`
|
|
4774
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
4775
|
+
created_at, updated_at, completed_at
|
|
4776
|
+
FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
|
|
4777
|
+
`).get(candidateSessionId, runId);
|
|
4778
|
+
if (!run) {
|
|
4779
|
+
throw new Error(`Run ${runId} is not linked to session ${candidateSessionId}.`);
|
|
4780
|
+
}
|
|
4781
|
+
if (!STATUS_TRANSITIONS[run.status].has(input.status)) {
|
|
4782
|
+
throw new Error(`Run ${runId} cannot transition from ${run.status} to ${input.status}.`);
|
|
4783
|
+
}
|
|
4784
|
+
const updatedAt = input.updatedAt ?? now();
|
|
4785
|
+
assertTimestamp("updatedAt", updatedAt);
|
|
4786
|
+
if (updatedAt < run.updated_at) {
|
|
4787
|
+
throw new Error(`Run ${runId} update timestamp cannot move backwards.`);
|
|
4788
|
+
}
|
|
4789
|
+
const completedAt = isTerminal(input.status) ? input.completedAt ?? updatedAt : null;
|
|
4790
|
+
if (completedAt !== null) {
|
|
4791
|
+
assertTimestamp("completedAt", completedAt);
|
|
4792
|
+
if (completedAt < run.created_at || completedAt > updatedAt) {
|
|
4793
|
+
throw new Error("completedAt must be between the run creation and update timestamps.");
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
database.query(`
|
|
4797
|
+
UPDATE zhivex_cli_session_runs
|
|
4798
|
+
SET status = ?1, updated_at = ?2, completed_at = ?3
|
|
4799
|
+
WHERE session_id = ?4 AND run_id = ?5
|
|
4800
|
+
`).run(input.status, updatedAt, completedAt, candidateSessionId, runId);
|
|
4801
|
+
const activitySequence = nextActivitySequence();
|
|
4802
|
+
database.query(`
|
|
4803
|
+
UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
4804
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
4805
|
+
`).run(updatedAt, activitySequence, candidateSessionId, session.revision);
|
|
4806
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
4807
|
+
});
|
|
4808
|
+
},
|
|
4809
|
+
async fork(candidateSessionId, input = {}) {
|
|
4810
|
+
return transaction(() => {
|
|
4811
|
+
const source = requireSessionRow(candidateSessionId);
|
|
4812
|
+
ensureRevision(source, input.expectedRevision);
|
|
4813
|
+
const sourceRuns = selectRuns(candidateSessionId);
|
|
4814
|
+
let forkRuns = sourceRuns;
|
|
4815
|
+
let forkedFromTurnId;
|
|
4816
|
+
if (input.atTurnId) {
|
|
4817
|
+
const index = sourceRuns.findIndex((run) => run.turnId === input.atTurnId);
|
|
4818
|
+
if (index < 0) {
|
|
4819
|
+
throw new Error(`Turn ${input.atTurnId} is not part of session ${candidateSessionId}.`);
|
|
4820
|
+
}
|
|
4821
|
+
forkRuns = sourceRuns.slice(0, index + 1);
|
|
4822
|
+
forkedFromTurnId = input.atTurnId;
|
|
4823
|
+
} else if (sourceRuns.length > 0) {
|
|
4824
|
+
forkedFromTurnId = sourceRuns.at(-1).turnId;
|
|
4825
|
+
}
|
|
4826
|
+
const branchPoint = forkRuns.at(-1);
|
|
4827
|
+
if (branchPoint && !isTerminal(branchPoint.status)) {
|
|
4828
|
+
throw new Error(`Cannot fork from non-terminal run ${branchPoint.runId} (${branchPoint.status}).`);
|
|
4829
|
+
}
|
|
4830
|
+
if (forkRuns.length > maxRuns) {
|
|
4831
|
+
throw new Error(`Fork history exceeds the ${maxRuns}-run limit.`);
|
|
4832
|
+
}
|
|
4833
|
+
const count = database.query(`
|
|
4834
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
4835
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
4836
|
+
`).get(workspaceKey, scopeKey)?.count ?? 0;
|
|
4837
|
+
if (count >= maxSessions) {
|
|
4838
|
+
throw new Error(`Session limit reached for this workspace and scope (${maxSessions}).`);
|
|
4839
|
+
}
|
|
4840
|
+
const createdAt = now();
|
|
4841
|
+
assertTimestamp("now", createdAt);
|
|
4842
|
+
const title = normalizeTitle(input.title, maxMetadataBytes) ?? source.title;
|
|
4843
|
+
const forkSessionId = sessionId();
|
|
4844
|
+
const activitySequence = nextActivitySequence();
|
|
4845
|
+
database.query(`
|
|
4846
|
+
INSERT INTO zhivex_cli_sessions (
|
|
4847
|
+
session_id, workspace_key, scope_key, title, parent_session_id,
|
|
4848
|
+
forked_from_turn_id, revision, created_at, updated_at, activity_seq, metadata_bytes
|
|
4849
|
+
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?8, ?9)
|
|
4850
|
+
`).run(forkSessionId, workspaceKey, scopeKey, title, candidateSessionId, forkedFromTurnId ?? null, createdAt, activitySequence, metadataSize({ title }));
|
|
4851
|
+
for (const [index, run] of forkRuns.entries()) {
|
|
4852
|
+
insertRun(forkSessionId, {
|
|
4853
|
+
runId: run.runId,
|
|
4854
|
+
provider: run.provider,
|
|
4855
|
+
model: run.model,
|
|
4856
|
+
...run.role ? { role: run.role } : {},
|
|
4857
|
+
status: run.status,
|
|
4858
|
+
createdAt: run.createdAt
|
|
4859
|
+
}, index, run.turnId);
|
|
4860
|
+
database.query(`
|
|
4861
|
+
UPDATE zhivex_cli_session_runs
|
|
4862
|
+
SET updated_at = ?1, completed_at = ?2
|
|
4863
|
+
WHERE session_id = ?3 AND ordinal = ?4
|
|
4864
|
+
`).run(run.updatedAt, run.completedAt ?? null, forkSessionId, index);
|
|
4865
|
+
}
|
|
4866
|
+
return hydrate(requireSessionRow(forkSessionId));
|
|
4867
|
+
});
|
|
4868
|
+
},
|
|
4869
|
+
async archive(candidateSessionId, input = {}) {
|
|
4870
|
+
return store.update(candidateSessionId, {
|
|
4871
|
+
archived: true,
|
|
4872
|
+
...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
|
|
4873
|
+
});
|
|
4874
|
+
},
|
|
4875
|
+
async delete(candidateSessionId, input = {}) {
|
|
4876
|
+
return transaction(() => {
|
|
4877
|
+
const row = requireSessionRow(candidateSessionId);
|
|
4878
|
+
ensureRevision(row, input.expectedRevision);
|
|
4879
|
+
assertNoActiveRun(candidateSessionId, "delete");
|
|
4880
|
+
const deletedAt = now();
|
|
4881
|
+
assertTimestamp("now", deletedAt);
|
|
4882
|
+
const activitySequence = nextActivitySequence();
|
|
4883
|
+
database.query(`
|
|
4884
|
+
UPDATE zhivex_cli_sessions
|
|
4885
|
+
SET deleted_at = ?1, updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
4886
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
4887
|
+
`).run(deletedAt, activitySequence, candidateSessionId, row.revision);
|
|
4888
|
+
return hydrate(requireSessionRow(candidateSessionId, true));
|
|
4889
|
+
});
|
|
4890
|
+
},
|
|
4891
|
+
prune,
|
|
4892
|
+
close() {
|
|
4893
|
+
if (!closed) {
|
|
4894
|
+
closed = true;
|
|
4895
|
+
database.close(false);
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
};
|
|
4899
|
+
return store;
|
|
4900
|
+
};
|
|
4901
|
+
var openSessionStore = openCliSessionStore;
|
|
4902
|
+
|
|
4903
|
+
// src/cli.ts
|
|
4904
|
+
var HARNESS_RESUME_METADATA_KEY = "zhivexHarnessResume";
|
|
4905
|
+
var HARNESS_RESUME_METADATA_SCHEMA_VERSION = 1;
|
|
4906
|
+
var CLI_EXIT_CODES = {
|
|
4907
|
+
success: 0,
|
|
4908
|
+
runtimeError: 1,
|
|
4909
|
+
usageError: 2,
|
|
4910
|
+
doctorFailed: 3
|
|
4911
|
+
};
|
|
4912
|
+
var COMMANDS = new Set(["run", "review", "chat", "providers", "doctor", "resume", "runs", "sessions", "help", "version"]);
|
|
4913
|
+
var RUNS_COMMANDS = new Set(["list", "inspect", "cancel", "cleanup", "export"]);
|
|
4914
|
+
var SESSIONS_COMMANDS = new Set(["list", "inspect", "rename", "fork", "archive"]);
|
|
4915
|
+
var RUN_STATUSES = new Set([
|
|
4916
|
+
"queued",
|
|
4917
|
+
"running",
|
|
4918
|
+
"completed",
|
|
4919
|
+
"suspended",
|
|
4920
|
+
"waiting_approval",
|
|
4921
|
+
"cancel_requested",
|
|
4922
|
+
"failed",
|
|
4923
|
+
"cancelled",
|
|
4924
|
+
"timed_out"
|
|
4925
|
+
]);
|
|
4926
|
+
|
|
4927
|
+
class CliUsageError extends Error {
|
|
4928
|
+
constructor(message) {
|
|
4929
|
+
super(message);
|
|
4930
|
+
this.name = "CliUsageError";
|
|
3892
4931
|
}
|
|
3893
4932
|
}
|
|
3894
4933
|
var harnessConfigInput = (config) => ({
|
|
@@ -3943,13 +4982,14 @@ var harnessConfigInput = (config) => ({
|
|
|
3943
4982
|
} : {},
|
|
3944
4983
|
...config.mcpConfigPath ? { mcpConfigPath: config.mcpConfigPath } : {}
|
|
3945
4984
|
});
|
|
3946
|
-
var createHarnessResumeMetadata = (config) => ({
|
|
4985
|
+
var createHarnessResumeMetadata = (config, routes) => ({
|
|
3947
4986
|
[HARNESS_RESUME_METADATA_KEY]: JSON.parse(JSON.stringify({
|
|
3948
4987
|
schemaVersion: HARNESS_RESUME_METADATA_SCHEMA_VERSION,
|
|
3949
|
-
config
|
|
4988
|
+
config,
|
|
4989
|
+
...routes && routes.size > 0 ? { routes: serializeHarnessModelRoutes(routes) } : {}
|
|
3950
4990
|
}))
|
|
3951
4991
|
});
|
|
3952
|
-
var
|
|
4992
|
+
var readHarnessResumeDocument = (state) => {
|
|
3953
4993
|
const value = state.metadata?.[HARNESS_RESUME_METADATA_KEY];
|
|
3954
4994
|
if (value === undefined)
|
|
3955
4995
|
return;
|
|
@@ -3960,12 +5000,39 @@ var readHarnessResumeConfig = (state) => {
|
|
|
3960
5000
|
if (document.schemaVersion !== HARNESS_RESUME_METADATA_SCHEMA_VERSION || !document.config || typeof document.config !== "object" || Array.isArray(document.config)) {
|
|
3961
5001
|
throw new Error("Persisted harness resume metadata is invalid.");
|
|
3962
5002
|
}
|
|
5003
|
+
return document;
|
|
5004
|
+
};
|
|
5005
|
+
var readHarnessResumeConfig = (state) => {
|
|
5006
|
+
const document = readHarnessResumeDocument(state);
|
|
5007
|
+
if (!document)
|
|
5008
|
+
return;
|
|
3963
5009
|
try {
|
|
3964
5010
|
return harnessConfigInput(document.config);
|
|
3965
5011
|
} catch {
|
|
3966
5012
|
throw new Error("Persisted harness resume metadata is invalid.");
|
|
3967
5013
|
}
|
|
3968
5014
|
};
|
|
5015
|
+
var readHarnessResumeRoutes = (state) => {
|
|
5016
|
+
const document = readHarnessResumeDocument(state);
|
|
5017
|
+
if (!document?.routes)
|
|
5018
|
+
return resolveHarnessModelRoutes();
|
|
5019
|
+
if (typeof document.routes !== "object" || Array.isArray(document.routes)) {
|
|
5020
|
+
throw new Error("Persisted harness routing metadata is invalid.");
|
|
5021
|
+
}
|
|
5022
|
+
try {
|
|
5023
|
+
const values = Object.entries(document.routes).map(([profile, target]) => {
|
|
5024
|
+
if (!target || typeof target !== "object" || Array.isArray(target))
|
|
5025
|
+
throw new Error("invalid");
|
|
5026
|
+
const route = target;
|
|
5027
|
+
if (typeof route.provider !== "string" || typeof route.model !== "string")
|
|
5028
|
+
throw new Error("invalid");
|
|
5029
|
+
return `${profile}=${route.provider}:${route.model}`;
|
|
5030
|
+
});
|
|
5031
|
+
return resolveHarnessModelRoutes(values);
|
|
5032
|
+
} catch {
|
|
5033
|
+
throw new Error("Persisted harness routing metadata is invalid.");
|
|
5034
|
+
}
|
|
5035
|
+
};
|
|
3969
5036
|
var shellArgument = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
3970
5037
|
var resumeCommand = (runId, config, approve = true) => [
|
|
3971
5038
|
"zhivex-harness",
|
|
@@ -3994,7 +5061,16 @@ var parseCliArgs = (argv) => {
|
|
|
3994
5061
|
let command = "run";
|
|
3995
5062
|
let commandWasExplicit = false;
|
|
3996
5063
|
const positional = [];
|
|
3997
|
-
const options = {
|
|
5064
|
+
const options = {
|
|
5065
|
+
command,
|
|
5066
|
+
cascade: false,
|
|
5067
|
+
final: false,
|
|
5068
|
+
yes: false,
|
|
5069
|
+
json: false,
|
|
5070
|
+
jsonl: false,
|
|
5071
|
+
continueSession: false,
|
|
5072
|
+
implicitCommand: true
|
|
5073
|
+
};
|
|
3998
5074
|
let positionalOnly = false;
|
|
3999
5075
|
for (let index = 0;index < argv.length; index += 1) {
|
|
4000
5076
|
const argument = argv[index];
|
|
@@ -4028,6 +5104,21 @@ var parseCliArgs = (argv) => {
|
|
|
4028
5104
|
options.model = optionValue(argv, index, argument);
|
|
4029
5105
|
index += 1;
|
|
4030
5106
|
break;
|
|
5107
|
+
case "--route": {
|
|
5108
|
+
const value = optionValue(argv, index, argument);
|
|
5109
|
+
try {
|
|
5110
|
+
parseHarnessModelRoute(value);
|
|
5111
|
+
} catch (error) {
|
|
5112
|
+
throw new CliUsageError(error instanceof Error ? error.message : String(error));
|
|
5113
|
+
}
|
|
5114
|
+
options.routes ??= [];
|
|
5115
|
+
options.routes.push(value);
|
|
5116
|
+
if (options.routes.length > HARNESS_SUBAGENT_PROFILES.length) {
|
|
5117
|
+
throw new CliUsageError(`--route cannot be repeated more than ${HARNESS_SUBAGENT_PROFILES.length} times.`);
|
|
5118
|
+
}
|
|
5119
|
+
index += 1;
|
|
5120
|
+
break;
|
|
5121
|
+
}
|
|
4031
5122
|
case "--workspace":
|
|
4032
5123
|
options.workspace = optionValue(argv, index, argument);
|
|
4033
5124
|
index += 1;
|
|
@@ -4251,6 +5342,16 @@ var parseCliArgs = (argv) => {
|
|
|
4251
5342
|
case "--json":
|
|
4252
5343
|
options.json = true;
|
|
4253
5344
|
break;
|
|
5345
|
+
case "--jsonl":
|
|
5346
|
+
options.jsonl = true;
|
|
5347
|
+
break;
|
|
5348
|
+
case "--session":
|
|
5349
|
+
options.sessionId = optionValue(argv, index, argument);
|
|
5350
|
+
index += 1;
|
|
5351
|
+
break;
|
|
5352
|
+
case "--continue":
|
|
5353
|
+
options.continueSession = true;
|
|
5354
|
+
break;
|
|
4254
5355
|
case "--status": {
|
|
4255
5356
|
const value = optionValue(argv, index, argument);
|
|
4256
5357
|
if (!RUN_STATUSES.has(value)) {
|
|
@@ -4312,6 +5413,16 @@ var parseCliArgs = (argv) => {
|
|
|
4312
5413
|
positional.push(argument);
|
|
4313
5414
|
}
|
|
4314
5415
|
}
|
|
5416
|
+
options.implicitCommand = !commandWasExplicit;
|
|
5417
|
+
if (options.json && options.jsonl) {
|
|
5418
|
+
throw new CliUsageError("You cannot combine --json and --jsonl.");
|
|
5419
|
+
}
|
|
5420
|
+
if (options.sessionId && options.continueSession) {
|
|
5421
|
+
throw new CliUsageError("You cannot combine --session and --continue.");
|
|
5422
|
+
}
|
|
5423
|
+
if (options.jsonl && options.command !== "run" && options.command !== "resume") {
|
|
5424
|
+
throw new CliUsageError("--jsonl is supported by run and resume.");
|
|
5425
|
+
}
|
|
4315
5426
|
if (options.command === "resume") {
|
|
4316
5427
|
const runId = positional.shift();
|
|
4317
5428
|
if (runId) {
|
|
@@ -4339,6 +5450,28 @@ var parseCliArgs = (argv) => {
|
|
|
4339
5450
|
if (positional.length > 0) {
|
|
4340
5451
|
throw new CliUsageError(`runs ${runsCommand} received unexpected positional arguments.`);
|
|
4341
5452
|
}
|
|
5453
|
+
} else if (options.command === "sessions") {
|
|
5454
|
+
const sessionsCommand = positional.shift();
|
|
5455
|
+
if (!sessionsCommand || !SESSIONS_COMMANDS.has(sessionsCommand)) {
|
|
5456
|
+
throw new CliUsageError("sessions requires one of: list, inspect, rename, fork, archive.");
|
|
5457
|
+
}
|
|
5458
|
+
options.sessionsCommand = sessionsCommand;
|
|
5459
|
+
if (sessionsCommand !== "list") {
|
|
5460
|
+
const sessionId2 = positional.shift();
|
|
5461
|
+
if (!sessionId2)
|
|
5462
|
+
throw new CliUsageError(`sessions ${sessionsCommand} requires a sessionId.`);
|
|
5463
|
+
options.sessionId = sessionId2;
|
|
5464
|
+
}
|
|
5465
|
+
if (sessionsCommand === "rename") {
|
|
5466
|
+
const title = positional.join(" ").trim();
|
|
5467
|
+
if (!title)
|
|
5468
|
+
throw new CliUsageError("sessions rename requires a title.");
|
|
5469
|
+
options.sessionTitle = title;
|
|
5470
|
+
positional.length = 0;
|
|
5471
|
+
}
|
|
5472
|
+
if (positional.length > 0) {
|
|
5473
|
+
throw new CliUsageError(`sessions ${sessionsCommand} received unexpected positional arguments.`);
|
|
5474
|
+
}
|
|
4342
5475
|
} else if (options.command === "run" || options.command === "review") {
|
|
4343
5476
|
const prompt = positional.join(" ").trim();
|
|
4344
5477
|
if (prompt) {
|
|
@@ -4352,21 +5485,24 @@ var parseCliArgs = (argv) => {
|
|
|
4352
5485
|
var help = `Zhivex Harness v${HARNESS_VERSION}
|
|
4353
5486
|
|
|
4354
5487
|
Usage:
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
5488
|
+
zhx Start the interactive console
|
|
5489
|
+
zhx run [options] "task"
|
|
5490
|
+
zhx review [options] "review task"
|
|
5491
|
+
zhx chat [options] [--continue|--session <id>]
|
|
5492
|
+
zhx providers [--json]
|
|
5493
|
+
zhx doctor [options] [--json]
|
|
5494
|
+
zhx resume [options] <runId> --approve|--deny
|
|
5495
|
+
zhx runs list [--status <status>] [--limit <n>] [--json]
|
|
5496
|
+
zhx sessions list|inspect|rename|fork|archive
|
|
5497
|
+
|
|
5498
|
+
The long command zhivex-harness remains supported for compatibility.
|
|
4366
5499
|
|
|
4367
5500
|
Options:
|
|
4368
|
-
--provider
|
|
5501
|
+
--provider <${PROVIDERS.join("|")}> Provider (default: openai)
|
|
4369
5502
|
--model <id> Override the default model
|
|
5503
|
+
--route <role=provider[:model]> Route a subagent role; repeatable
|
|
5504
|
+
--session <id> Open a durable interactive session
|
|
5505
|
+
--continue Open the latest durable interactive session
|
|
4370
5506
|
--workspace <path> Target workspace (default: cwd)
|
|
4371
5507
|
--state-dir <path> Durable run-state directory
|
|
4372
5508
|
--mcp-config <path> Declarative governed MCP JSON configuration
|
|
@@ -4408,6 +5544,7 @@ Options:
|
|
|
4408
5544
|
--max-parallel-reviews <1-4> Review group concurrency ceiling (default: 2)
|
|
4409
5545
|
--yes Automatically approve writes and checks
|
|
4410
5546
|
--json Emit structured final output
|
|
5547
|
+
--jsonl Stream redacted JSON Lines, then the final result
|
|
4411
5548
|
-h, --help Show this help
|
|
4412
5549
|
-v, --version Show the version
|
|
4413
5550
|
|
|
@@ -4420,7 +5557,8 @@ Exit codes:
|
|
|
4420
5557
|
Credentials:
|
|
4421
5558
|
OpenAI: OPENAI_API_KEY
|
|
4422
5559
|
Meta: MODEL_API_KEY
|
|
4423
|
-
Qwen: DASHSCOPE_API_KEY or QWEN_API_KEY
|
|
5560
|
+
Qwen: DASHSCOPE_API_KEY or QWEN_API_KEY
|
|
5561
|
+
Gemini: GEMINI_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY`;
|
|
4424
5562
|
var summarizeApproval = (approval) => {
|
|
4425
5563
|
const mustShowCompleteProposal = approval.name === "apply_patch";
|
|
4426
5564
|
const argumentsText = !mustShowCompleteProposal && approval.arguments.length > 1200 ? `${approval.arguments.slice(0, 1200)}\u2026` : approval.arguments;
|
|
@@ -4524,16 +5662,23 @@ var runResultDocument = (result, harness) => ({
|
|
|
4524
5662
|
},
|
|
4525
5663
|
stateDirectory: harness.config.stateDirectory
|
|
4526
5664
|
});
|
|
4527
|
-
var printTerminalResult = (result, harness,
|
|
4528
|
-
|
|
4529
|
-
|
|
5665
|
+
var printTerminalResult = (result, harness, output, tracker) => {
|
|
5666
|
+
const document = runResultDocument(result, harness);
|
|
5667
|
+
if (output.jsonl) {
|
|
5668
|
+
tracker.sequence = (tracker.sequence ?? 0) + 1;
|
|
5669
|
+
process.stdout.write(`${serializeStreamResult(document, tracker.sequence)}
|
|
4530
5670
|
`);
|
|
4531
5671
|
return;
|
|
4532
5672
|
}
|
|
4533
|
-
if (
|
|
5673
|
+
if (output.json) {
|
|
5674
|
+
process.stdout.write(`${JSON.stringify(document, null, 2)}
|
|
5675
|
+
`);
|
|
5676
|
+
return;
|
|
5677
|
+
}
|
|
5678
|
+
if (!tracker.streamedText && result.outputText) {
|
|
4534
5679
|
process.stdout.write(result.outputText);
|
|
4535
5680
|
}
|
|
4536
|
-
if (result.outputText || streamedText) {
|
|
5681
|
+
if (result.outputText || tracker.streamedText) {
|
|
4537
5682
|
process.stdout.write(`
|
|
4538
5683
|
`);
|
|
4539
5684
|
}
|
|
@@ -4551,8 +5696,14 @@ ${summarizeApproval(approval)}
|
|
|
4551
5696
|
`);
|
|
4552
5697
|
}
|
|
4553
5698
|
};
|
|
4554
|
-
var streamSink = (
|
|
4555
|
-
if (
|
|
5699
|
+
var streamSink = (output, tracker) => async (event) => {
|
|
5700
|
+
if (output.jsonl) {
|
|
5701
|
+
tracker.sequence = (tracker.sequence ?? 0) + 1;
|
|
5702
|
+
process.stdout.write(`${serializeStreamEvent(event, tracker.sequence)}
|
|
5703
|
+
`);
|
|
5704
|
+
return;
|
|
5705
|
+
}
|
|
5706
|
+
if (!output.json && event.type === "text-delta") {
|
|
4556
5707
|
tracker.streamedText = true;
|
|
4557
5708
|
process.stdout.write(event.textDelta);
|
|
4558
5709
|
}
|
|
@@ -4570,23 +5721,57 @@ subagent finish \xB7 ${event.childRun.agentId ?? event.childRun.toolName} \xB7 $
|
|
|
4570
5721
|
`);
|
|
4571
5722
|
}
|
|
4572
5723
|
};
|
|
5724
|
+
var resolvedRouting = (options) => {
|
|
5725
|
+
return resolveHarnessModelRoutes(options.routes);
|
|
5726
|
+
};
|
|
5727
|
+
var assertRoutePricingIsSafe = (routes, costBudget) => {
|
|
5728
|
+
if (routes.size > 0 && costBudget !== undefined) {
|
|
5729
|
+
throw new CliUsageError("Cost budgets cannot be combined with model routes until pricing is configured per role.");
|
|
5730
|
+
}
|
|
5731
|
+
};
|
|
5732
|
+
var withTemporaryHarnessProfiles = async (profiles, configure, operation) => {
|
|
5733
|
+
await configure(profiles);
|
|
5734
|
+
try {
|
|
5735
|
+
return await operation();
|
|
5736
|
+
} finally {
|
|
5737
|
+
await configure([]);
|
|
5738
|
+
}
|
|
5739
|
+
};
|
|
5740
|
+
var createConfiguredHarness = async (options, extraProfiles = [], persistedRoutes) => {
|
|
5741
|
+
const routes = persistedRoutes ?? resolvedRouting(options);
|
|
5742
|
+
const resolvedConfig = resolveHarnessConfig(options);
|
|
5743
|
+
assertRoutePricingIsSafe(routes, resolvedConfig.costBudget);
|
|
5744
|
+
const profiles = [...new Set([
|
|
5745
|
+
...options.subagentProfiles ?? [],
|
|
5746
|
+
...extraProfiles,
|
|
5747
|
+
...routes.keys()
|
|
5748
|
+
])];
|
|
5749
|
+
const quietTelemetry = options.json || options.jsonl;
|
|
5750
|
+
const harness = await createHarness({
|
|
5751
|
+
...options,
|
|
5752
|
+
subagentProfiles: profiles,
|
|
5753
|
+
subagentModels: createHarnessRouteModels(routes),
|
|
5754
|
+
onTelemetryEvent: orchestrationObserver(quietTelemetry)
|
|
5755
|
+
});
|
|
5756
|
+
return { harness, routes };
|
|
5757
|
+
};
|
|
4573
5758
|
var runOnce = async (options) => {
|
|
4574
5759
|
if (!options.prompt) {
|
|
4575
5760
|
throw new CliUsageError('Missing task. Example: zhivex-harness run "fix the tests".');
|
|
4576
5761
|
}
|
|
4577
|
-
const harness = await
|
|
5762
|
+
const { harness, routes } = await createConfiguredHarness(options);
|
|
4578
5763
|
try {
|
|
4579
5764
|
const tracker = { streamedText: false };
|
|
4580
5765
|
const result = await runHarness(harness, {
|
|
4581
5766
|
prompt: options.prompt,
|
|
4582
5767
|
scope: harness.config.scope,
|
|
4583
|
-
metadata: createHarnessResumeMetadata(harness.config),
|
|
5768
|
+
metadata: createHarnessResumeMetadata(harness.config, routes),
|
|
4584
5769
|
...options.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}
|
|
4585
5770
|
}, {
|
|
4586
|
-
onEvent: streamSink(options
|
|
5771
|
+
onEvent: streamSink(options, tracker),
|
|
4587
5772
|
resolveApprovals: terminalApprovalResolver(options.yes)
|
|
4588
5773
|
});
|
|
4589
|
-
printTerminalResult(result, harness, options
|
|
5774
|
+
printTerminalResult(result, harness, options, tracker);
|
|
4590
5775
|
if (result.status === "failed" || result.status === "timed_out") {
|
|
4591
5776
|
process.exitCode = CLI_EXIT_CODES.runtimeError;
|
|
4592
5777
|
}
|
|
@@ -4603,11 +5788,7 @@ var reviewOnce = async (options) => {
|
|
|
4603
5788
|
...options.subagentProfiles ?? [],
|
|
4604
5789
|
...requestedReviewers
|
|
4605
5790
|
])];
|
|
4606
|
-
const harness = await
|
|
4607
|
-
...options,
|
|
4608
|
-
subagentProfiles: enabledProfiles,
|
|
4609
|
-
onTelemetryEvent: orchestrationObserver(options.json)
|
|
4610
|
-
});
|
|
5791
|
+
const { harness } = await createConfiguredHarness(options, enabledProfiles);
|
|
4611
5792
|
try {
|
|
4612
5793
|
const result = await runHarnessReviewGroup(harness, { prompt: options.prompt, scope: harness.config.scope }, requestedReviewers);
|
|
4613
5794
|
const document = {
|
|
@@ -4678,22 +5859,31 @@ ${summarizeApproval(approval)}
|
|
|
4678
5859
|
`);
|
|
4679
5860
|
}
|
|
4680
5861
|
const persistedResumeInput = readHarnessResumeConfig(state);
|
|
5862
|
+
const persistedRoutes = readHarnessResumeRoutes(state);
|
|
4681
5863
|
let persistedResumeOptions = {};
|
|
5864
|
+
let resumeCostBudget = initialConfig.costBudget;
|
|
4682
5865
|
if (persistedResumeInput) {
|
|
4683
5866
|
const persistedConfig = resolveHarnessConfig(persistedResumeInput);
|
|
4684
5867
|
if (persistedConfig.workspace !== initialConfig.workspace || persistedConfig.stateDirectory !== initialConfig.stateDirectory || persistedConfig.storeBackend !== initialConfig.storeBackend || JSON.stringify(persistedConfig.scope) !== JSON.stringify(initialConfig.scope)) {
|
|
4685
5868
|
throw new Error("Resume locator options do not match the persisted workspace, state directory, store, or scope.");
|
|
4686
5869
|
}
|
|
4687
5870
|
persistedResumeOptions = harnessConfigInput(persistedConfig);
|
|
5871
|
+
resumeCostBudget = persistedConfig.costBudget;
|
|
4688
5872
|
}
|
|
5873
|
+
assertRoutePricingIsSafe(persistedRoutes, resumeCostBudget);
|
|
4689
5874
|
const harness = await createHarness({
|
|
4690
5875
|
...persistedResumeOptions,
|
|
4691
5876
|
...options,
|
|
4692
5877
|
provider: state.provider,
|
|
4693
5878
|
model: state.modelId,
|
|
5879
|
+
subagentProfiles: [...new Set([
|
|
5880
|
+
...persistedResumeOptions.subagentProfiles ?? [],
|
|
5881
|
+
...persistedRoutes.keys()
|
|
5882
|
+
])],
|
|
5883
|
+
subagentModels: createHarnessRouteModels(persistedRoutes),
|
|
4694
5884
|
store: persistence.store,
|
|
4695
5885
|
memory: persistence.memory,
|
|
4696
|
-
onTelemetryEvent: orchestrationObserver(options.json)
|
|
5886
|
+
onTelemetryEvent: orchestrationObserver(options.json || options.jsonl)
|
|
4697
5887
|
});
|
|
4698
5888
|
const approve = options.approve;
|
|
4699
5889
|
const tracker = { streamedText: false };
|
|
@@ -4701,10 +5891,11 @@ ${summarizeApproval(approval)}
|
|
|
4701
5891
|
state,
|
|
4702
5892
|
approvals: approvalResponses(state.pendingApprovals, approve, approve ? "Approved by resume --approve." : "Denied by resume --deny.")
|
|
4703
5893
|
}, {
|
|
4704
|
-
onEvent: streamSink(options
|
|
5894
|
+
onEvent: streamSink(options, tracker),
|
|
4705
5895
|
resolveApprovals: async (approvals) => approvalResponses(approvals, approve, approve ? "Approved by resume --approve." : "Denied by resume --deny.")
|
|
4706
5896
|
});
|
|
4707
|
-
|
|
5897
|
+
await updateIndexedSessionRun(harness.config, result.state.runId, result.status);
|
|
5898
|
+
printTerminalResult(result, harness, options, tracker);
|
|
4708
5899
|
if (result.status === "failed" || result.status === "timed_out") {
|
|
4709
5900
|
process.exitCode = CLI_EXIT_CODES.runtimeError;
|
|
4710
5901
|
}
|
|
@@ -4716,11 +5907,87 @@ var chat = async (options) => {
|
|
|
4716
5907
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4717
5908
|
throw new Error("Chat mode requires an interactive terminal. Use run for automation.");
|
|
4718
5909
|
}
|
|
4719
|
-
const
|
|
5910
|
+
const baseConfig = resolveHarnessConfig(options);
|
|
5911
|
+
const sessionStore = await openSessionStoreForConfig(baseConfig);
|
|
4720
5912
|
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
5913
|
+
let runtimeOptions = { ...options };
|
|
5914
|
+
let routes = resolvedRouting(options);
|
|
5915
|
+
const selectedSession = options.sessionId ? await sessionStore.get(options.sessionId) : options.continueSession ? await sessionStore.latest() : undefined;
|
|
5916
|
+
if (options.sessionId && !selectedSession) {
|
|
5917
|
+
sessionStore.close();
|
|
5918
|
+
readline.close();
|
|
5919
|
+
throw new Error(`Session ${options.sessionId} was not found.`);
|
|
5920
|
+
}
|
|
5921
|
+
let session = selectedSession ?? await sessionStore.create();
|
|
4721
5922
|
let messages = [];
|
|
5923
|
+
const latestState = async (selected) => {
|
|
5924
|
+
const latest = selected.runs.at(-1);
|
|
5925
|
+
if (!latest)
|
|
5926
|
+
return;
|
|
5927
|
+
const persistence = await openHarnessPersistence(baseConfig);
|
|
5928
|
+
try {
|
|
5929
|
+
const state = await persistence.store.load(latest.runId, baseConfig.scope);
|
|
5930
|
+
if (!state)
|
|
5931
|
+
throw new Error(`Run ${latest.runId} referenced by session ${selected.sessionId} was not found.`);
|
|
5932
|
+
return state;
|
|
5933
|
+
} finally {
|
|
5934
|
+
persistence.close();
|
|
5935
|
+
}
|
|
5936
|
+
};
|
|
5937
|
+
let harness;
|
|
5938
|
+
try {
|
|
5939
|
+
const restored = await latestState(session);
|
|
5940
|
+
if (restored) {
|
|
5941
|
+
runtimeOptions = { ...runtimeOptions, provider: restored.provider, model: restored.modelId };
|
|
5942
|
+
routes = readHarnessResumeRoutes(restored);
|
|
5943
|
+
messages = restored.messages;
|
|
5944
|
+
}
|
|
5945
|
+
harness = (await createConfiguredHarness(runtimeOptions, [], routes)).harness;
|
|
5946
|
+
} catch (error) {
|
|
5947
|
+
readline.close();
|
|
5948
|
+
sessionStore.close();
|
|
5949
|
+
throw error;
|
|
5950
|
+
}
|
|
5951
|
+
const refreshSession = async () => {
|
|
5952
|
+
const refreshed = await sessionStore.get(session.sessionId);
|
|
5953
|
+
if (!refreshed)
|
|
5954
|
+
throw new Error(`Session ${session.sessionId} was not found.`);
|
|
5955
|
+
session = refreshed;
|
|
5956
|
+
return session;
|
|
5957
|
+
};
|
|
5958
|
+
const hasActiveTurn = async () => {
|
|
5959
|
+
const latest = (await refreshSession()).runs.at(-1);
|
|
5960
|
+
return latest && !["completed", "failed", "cancelled", "timed_out"].includes(latest.status) ? latest : undefined;
|
|
5961
|
+
};
|
|
5962
|
+
const replaceHarness = async (nextOptions, nextRoutes, extraProfiles = []) => {
|
|
5963
|
+
const created = await createConfiguredHarness(nextOptions, extraProfiles, nextRoutes);
|
|
5964
|
+
harness.close();
|
|
5965
|
+
harness = created.harness;
|
|
5966
|
+
runtimeOptions = nextOptions;
|
|
5967
|
+
routes = new Map(nextRoutes);
|
|
5968
|
+
};
|
|
5969
|
+
const withTemporaryProfiles = async (profiles, operation) => {
|
|
5970
|
+
const normalOptions = runtimeOptions;
|
|
5971
|
+
const normalRoutes = new Map(routes);
|
|
5972
|
+
return withTemporaryHarnessProfiles(profiles, (temporaryProfiles) => replaceHarness(normalOptions, normalRoutes, temporaryProfiles), operation);
|
|
5973
|
+
};
|
|
5974
|
+
const restoreSession = async (selected) => {
|
|
5975
|
+
const state = await latestState(selected);
|
|
5976
|
+
const nextOptions = state ? { ...runtimeOptions, provider: state.provider, model: state.modelId } : runtimeOptions;
|
|
5977
|
+
const nextRoutes = state ? readHarnessResumeRoutes(state) : resolveHarnessModelRoutes();
|
|
5978
|
+
await replaceHarness(nextOptions, nextRoutes);
|
|
5979
|
+
session = selected;
|
|
5980
|
+
messages = state?.messages ?? [];
|
|
5981
|
+
};
|
|
5982
|
+
const statusLine = async () => {
|
|
5983
|
+
const current = await refreshSession();
|
|
5984
|
+
const latest = current.runs.at(-1);
|
|
5985
|
+
const routeText = routes.size === 0 ? "default" : [...routes.values()].map((route) => `${route.profile}=${route.provider}:${route.model}`).join(", ");
|
|
5986
|
+
return `session ${current.sessionId} \xB7 ${harness.config.provider}/${harness.config.model}` + ` \xB7 ${messages.length} messages \xB7 routes ${routeText}` + `${latest ? ` \xB7 last ${latest.runId} (${latest.status})` : ""}`;
|
|
5987
|
+
};
|
|
4722
5988
|
process.stderr.write(`Zhivex Harness ${HARNESS_VERSION} \xB7 ${harness.config.provider}/${harness.config.model}
|
|
4723
|
-
` + `
|
|
5989
|
+
` + `session ${session.sessionId}
|
|
5990
|
+
` + `Type /help for console commands.
|
|
4724
5991
|
`);
|
|
4725
5992
|
try {
|
|
4726
5993
|
for (;; ) {
|
|
@@ -4732,39 +5999,253 @@ var chat = async (options) => {
|
|
|
4732
5999
|
if (prompt === "/exit" || prompt === "/quit") {
|
|
4733
6000
|
break;
|
|
4734
6001
|
}
|
|
6002
|
+
if (prompt === "/help") {
|
|
6003
|
+
process.stderr.write(`/provider [id] \xB7 /model [id] \xB7 /route [role=provider[:model]] \xB7 /status
|
|
6004
|
+
` + `/diff \xB7 /review <task> \xB7 /resume <last|sessionId> \xB7 /compact
|
|
6005
|
+
` + `/new [title] \xB7 /rename <title> \xB7 /clear \xB7 /exit
|
|
6006
|
+
`);
|
|
6007
|
+
continue;
|
|
6008
|
+
}
|
|
4735
6009
|
if (prompt === "/clear") {
|
|
4736
6010
|
messages = [];
|
|
4737
6011
|
process.stderr.write(`Context cleared.
|
|
4738
6012
|
`);
|
|
4739
6013
|
continue;
|
|
4740
6014
|
}
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
6015
|
+
if (prompt === "/status") {
|
|
6016
|
+
process.stderr.write(`${await statusLine()}
|
|
6017
|
+
`);
|
|
6018
|
+
continue;
|
|
6019
|
+
}
|
|
6020
|
+
if (prompt === "/diff") {
|
|
6021
|
+
const diff = await harness.workspace.gitDiff();
|
|
6022
|
+
const output = `${diff.status.stdout}${diff.diff.stdout}${diff.staged.stdout}`;
|
|
6023
|
+
process.stdout.write(output || `No workspace changes.
|
|
6024
|
+
`);
|
|
6025
|
+
continue;
|
|
6026
|
+
}
|
|
6027
|
+
if (prompt === "/compact") {
|
|
6028
|
+
messages = compactHarnessMessages(messages);
|
|
6029
|
+
process.stderr.write(`Context compacted to ${messages.length} redacted message(s).
|
|
6030
|
+
`);
|
|
6031
|
+
continue;
|
|
6032
|
+
}
|
|
6033
|
+
if (prompt === "/provider" || prompt.startsWith("/provider ")) {
|
|
6034
|
+
const value = prompt.slice("/provider".length).trim();
|
|
6035
|
+
if (!value) {
|
|
6036
|
+
process.stderr.write(`${providerAvailability().map((provider2) => `${provider2.id}${provider2.id === harness.config.provider ? "*" : ""}`).join(" ")}
|
|
6037
|
+
`);
|
|
6038
|
+
continue;
|
|
6039
|
+
}
|
|
6040
|
+
const active2 = await hasActiveTurn();
|
|
6041
|
+
if (active2) {
|
|
6042
|
+
process.stderr.write(`Cannot switch provider while run ${active2.runId} is ${active2.status}.
|
|
6043
|
+
`);
|
|
6044
|
+
continue;
|
|
6045
|
+
}
|
|
6046
|
+
const provider = parseProvider(value);
|
|
6047
|
+
const model = DEFAULT_PROVIDER_REGISTRY.descriptor(provider).defaultModel;
|
|
6048
|
+
const portableMessages = compactHarnessMessages(messages);
|
|
6049
|
+
await replaceHarness({ ...runtimeOptions, provider, model }, routes);
|
|
6050
|
+
messages = portableMessages;
|
|
6051
|
+
process.stderr.write(`Next turn: ${provider}/${model}; context was compacted for a safe handoff.
|
|
6052
|
+
`);
|
|
6053
|
+
continue;
|
|
6054
|
+
}
|
|
6055
|
+
if (prompt === "/model" || prompt.startsWith("/model ")) {
|
|
6056
|
+
const value = prompt.slice("/model".length).trim();
|
|
6057
|
+
if (!value) {
|
|
6058
|
+
process.stderr.write(`${harness.config.provider}/${harness.config.model}
|
|
6059
|
+
`);
|
|
6060
|
+
continue;
|
|
6061
|
+
}
|
|
6062
|
+
const active2 = await hasActiveTurn();
|
|
6063
|
+
if (active2) {
|
|
6064
|
+
process.stderr.write(`Cannot switch model while run ${active2.runId} is ${active2.status}.
|
|
6065
|
+
`);
|
|
6066
|
+
continue;
|
|
6067
|
+
}
|
|
6068
|
+
const portableMessages = compactHarnessMessages(messages);
|
|
6069
|
+
await replaceHarness({ ...runtimeOptions, model: value }, routes);
|
|
6070
|
+
messages = portableMessages;
|
|
6071
|
+
process.stderr.write(`Next turn: ${harness.config.provider}/${value}; context was compacted.
|
|
6072
|
+
`);
|
|
6073
|
+
continue;
|
|
6074
|
+
}
|
|
6075
|
+
if (prompt === "/route" || prompt.startsWith("/route ")) {
|
|
6076
|
+
const value = prompt.slice("/route".length).trim();
|
|
6077
|
+
if (!value) {
|
|
6078
|
+
process.stderr.write(`${JSON.stringify(serializeHarnessModelRoutes(routes))}
|
|
6079
|
+
`);
|
|
6080
|
+
continue;
|
|
6081
|
+
}
|
|
6082
|
+
const active2 = await hasActiveTurn();
|
|
6083
|
+
if (active2) {
|
|
6084
|
+
process.stderr.write(`Cannot change routes while run ${active2.runId} is ${active2.status}.
|
|
6085
|
+
`);
|
|
6086
|
+
continue;
|
|
6087
|
+
}
|
|
6088
|
+
const nextRoutes = new Map(routes);
|
|
6089
|
+
if (value === "clear") {
|
|
6090
|
+
nextRoutes.clear();
|
|
6091
|
+
} else if (value.startsWith("clear ")) {
|
|
6092
|
+
const profile = value.slice("clear ".length).trim();
|
|
6093
|
+
if (!HARNESS_SUBAGENT_PROFILES.includes(profile)) {
|
|
6094
|
+
process.stderr.write(`Unknown subagent profile: ${profile}.
|
|
6095
|
+
`);
|
|
6096
|
+
continue;
|
|
6097
|
+
}
|
|
6098
|
+
nextRoutes.delete(profile);
|
|
6099
|
+
} else {
|
|
6100
|
+
const route = parseHarnessModelRoute(value);
|
|
6101
|
+
nextRoutes.set(route.profile, route);
|
|
6102
|
+
}
|
|
6103
|
+
await replaceHarness(runtimeOptions, nextRoutes);
|
|
6104
|
+
process.stderr.write(`Routes: ${JSON.stringify(serializeHarnessModelRoutes(routes))}
|
|
6105
|
+
`);
|
|
6106
|
+
continue;
|
|
6107
|
+
}
|
|
6108
|
+
if (prompt === "/new" || prompt.startsWith("/new ")) {
|
|
6109
|
+
const active2 = await hasActiveTurn();
|
|
6110
|
+
if (active2) {
|
|
6111
|
+
process.stderr.write(`Cannot leave session while run ${active2.runId} is ${active2.status}.
|
|
6112
|
+
`);
|
|
6113
|
+
continue;
|
|
6114
|
+
}
|
|
6115
|
+
const title = prompt.slice("/new".length).trim();
|
|
6116
|
+
const created = await sessionStore.create(title ? { title } : undefined);
|
|
6117
|
+
await restoreSession(created);
|
|
6118
|
+
process.stderr.write(`Created session ${session.sessionId}.
|
|
6119
|
+
`);
|
|
6120
|
+
continue;
|
|
6121
|
+
}
|
|
6122
|
+
if (prompt === "/rename" || prompt.startsWith("/rename ")) {
|
|
6123
|
+
const title = prompt.slice("/rename".length).trim();
|
|
6124
|
+
if (!title) {
|
|
6125
|
+
process.stderr.write(`Usage: /rename <title>
|
|
6126
|
+
`);
|
|
6127
|
+
continue;
|
|
6128
|
+
}
|
|
6129
|
+
session = await sessionStore.rename(session.sessionId, title);
|
|
6130
|
+
process.stderr.write(`Renamed session to ${session.title}.
|
|
6131
|
+
`);
|
|
6132
|
+
continue;
|
|
6133
|
+
}
|
|
6134
|
+
if (prompt === "/resume" || prompt.startsWith("/resume ")) {
|
|
6135
|
+
const selector = prompt.slice("/resume".length).trim() || "last";
|
|
6136
|
+
const selected = selector === "last" ? await sessionStore.latest({ includeArchived: true }) : await sessionStore.get(selector);
|
|
6137
|
+
if (!selected) {
|
|
6138
|
+
process.stderr.write(`Session ${selector} was not found.
|
|
6139
|
+
`);
|
|
6140
|
+
continue;
|
|
6141
|
+
}
|
|
6142
|
+
await restoreSession(selected);
|
|
6143
|
+
const active2 = await hasActiveTurn();
|
|
6144
|
+
process.stderr.write(active2 ? `Session ${session.sessionId} has run ${active2.runId} waiting in ${active2.status}; use zhx resume ${active2.runId} --approve|--deny.
|
|
6145
|
+
` : `Resumed session ${session.sessionId}.
|
|
6146
|
+
`);
|
|
6147
|
+
continue;
|
|
6148
|
+
}
|
|
6149
|
+
if (prompt === "/review" || prompt.startsWith("/review ")) {
|
|
6150
|
+
const reviewPrompt = prompt.slice("/review".length).trim();
|
|
6151
|
+
if (!reviewPrompt) {
|
|
6152
|
+
process.stderr.write(`Usage: /review <task>
|
|
6153
|
+
`);
|
|
6154
|
+
continue;
|
|
6155
|
+
}
|
|
6156
|
+
const active2 = await hasActiveTurn();
|
|
6157
|
+
if (active2) {
|
|
6158
|
+
process.stderr.write(`Cannot start a review while run ${active2.runId} is ${active2.status}.
|
|
6159
|
+
`);
|
|
6160
|
+
continue;
|
|
6161
|
+
}
|
|
6162
|
+
const review = await withTemporaryProfiles(["explorer", "reviewer"], () => runHarnessReviewGroup(harness, { prompt: reviewPrompt, scope: harness.config.scope }, ["explorer", "reviewer"]));
|
|
6163
|
+
for (const output of review.outputs) {
|
|
6164
|
+
process.stdout.write(`
|
|
6165
|
+
[${output.name ?? output.agentId ?? "reviewer"}] ${output.status}
|
|
6166
|
+
`);
|
|
6167
|
+
if (output.output?.outputText)
|
|
6168
|
+
process.stdout.write(`${output.output.outputText}
|
|
6169
|
+
`);
|
|
6170
|
+
}
|
|
6171
|
+
continue;
|
|
6172
|
+
}
|
|
6173
|
+
const active = await hasActiveTurn();
|
|
6174
|
+
if (active) {
|
|
6175
|
+
process.stderr.write(`Run ${active.runId} is ${active.status}; resolve it with zhx resume ${active.runId} --approve|--deny before a new turn.
|
|
6176
|
+
`);
|
|
6177
|
+
continue;
|
|
6178
|
+
}
|
|
6179
|
+
const runId = `run_${randomUUID5()}`;
|
|
6180
|
+
session = await sessionStore.appendRun(session.sessionId, {
|
|
6181
|
+
runId,
|
|
6182
|
+
provider: harness.config.provider,
|
|
6183
|
+
model: harness.config.model,
|
|
6184
|
+
status: "created"
|
|
4753
6185
|
});
|
|
6186
|
+
const turn = session.runs.at(-1);
|
|
6187
|
+
const tracker = { streamedText: false };
|
|
6188
|
+
let markedRunning = false;
|
|
6189
|
+
let result;
|
|
6190
|
+
try {
|
|
6191
|
+
result = await runHarness(harness, messages.length === 0 ? {
|
|
6192
|
+
runId,
|
|
6193
|
+
prompt,
|
|
6194
|
+
scope: harness.config.scope,
|
|
6195
|
+
metadata: {
|
|
6196
|
+
...createHarnessResumeMetadata(harness.config, routes),
|
|
6197
|
+
zhivexCliSession: {
|
|
6198
|
+
schemaVersion: 1,
|
|
6199
|
+
sessionId: session.sessionId,
|
|
6200
|
+
turnId: turn.turnId,
|
|
6201
|
+
sequence: turn.sequence
|
|
6202
|
+
}
|
|
6203
|
+
}
|
|
6204
|
+
} : {
|
|
6205
|
+
runId,
|
|
6206
|
+
messages: appendUserMessage(messages, prompt),
|
|
6207
|
+
scope: harness.config.scope,
|
|
6208
|
+
metadata: {
|
|
6209
|
+
...createHarnessResumeMetadata(harness.config, routes),
|
|
6210
|
+
zhivexCliSession: {
|
|
6211
|
+
schemaVersion: 1,
|
|
6212
|
+
sessionId: session.sessionId,
|
|
6213
|
+
turnId: turn.turnId,
|
|
6214
|
+
sequence: turn.sequence
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
}, {
|
|
6218
|
+
onEvent: async (event) => {
|
|
6219
|
+
if (!markedRunning && event.type === "agent-run-start") {
|
|
6220
|
+
session = await sessionStore.updateRun(session.sessionId, runId, { status: "running" });
|
|
6221
|
+
markedRunning = true;
|
|
6222
|
+
}
|
|
6223
|
+
await streamSink({ json: false, jsonl: false }, tracker)(event);
|
|
6224
|
+
},
|
|
6225
|
+
resolveApprovals: terminalApprovalResolver(options.yes, (question) => readline.question(question))
|
|
6226
|
+
});
|
|
6227
|
+
} catch (error) {
|
|
6228
|
+
session = await sessionStore.updateRun(session.sessionId, runId, { status: "failed" });
|
|
6229
|
+
throw error;
|
|
6230
|
+
}
|
|
4754
6231
|
if (!tracker.streamedText && result.outputText) {
|
|
4755
6232
|
process.stdout.write(result.outputText);
|
|
4756
6233
|
}
|
|
4757
6234
|
process.stdout.write(`
|
|
4758
6235
|
`);
|
|
4759
6236
|
messages = result.messages;
|
|
6237
|
+
session = await sessionStore.updateRun(session.sessionId, runId, {
|
|
6238
|
+
status: sessionStatus(result.status)
|
|
6239
|
+
});
|
|
4760
6240
|
if (result.status === "waiting_approval") {
|
|
4761
|
-
process.stderr.write(`Run paused: ${result.state.runId}
|
|
6241
|
+
process.stderr.write(`Run paused: ${result.state.runId}. Resume with zhx resume ${result.state.runId} --approve|--deny.
|
|
4762
6242
|
`);
|
|
4763
6243
|
}
|
|
4764
6244
|
}
|
|
4765
6245
|
} finally {
|
|
4766
6246
|
readline.close();
|
|
4767
6247
|
harness.close();
|
|
6248
|
+
sessionStore.close();
|
|
4768
6249
|
}
|
|
4769
6250
|
};
|
|
4770
6251
|
var providersDocument = (env = process.env) => ({
|
|
@@ -4819,7 +6300,7 @@ var satisfiesBunEngine = (version, range) => {
|
|
|
4819
6300
|
var diagnostic = (id, status, message, details = {}) => ({ id, status, message, details });
|
|
4820
6301
|
var inspectWorkspace = async (workspace) => {
|
|
4821
6302
|
try {
|
|
4822
|
-
const workspaceStat = await
|
|
6303
|
+
const workspaceStat = await stat4(workspace);
|
|
4823
6304
|
if (!workspaceStat.isDirectory()) {
|
|
4824
6305
|
return diagnostic("workspace", "fail", "Workspace is not a directory.", { path: workspace });
|
|
4825
6306
|
}
|
|
@@ -4861,9 +6342,9 @@ var inspectGit = async (workspace) => {
|
|
|
4861
6342
|
}
|
|
4862
6343
|
};
|
|
4863
6344
|
var inspectScripts = async (workspace, allowedChecks) => {
|
|
4864
|
-
const packagePath =
|
|
6345
|
+
const packagePath = path9.join(workspace, "package.json");
|
|
4865
6346
|
try {
|
|
4866
|
-
const packageStat = await
|
|
6347
|
+
const packageStat = await stat4(packagePath);
|
|
4867
6348
|
if (!packageStat.isFile() || packageStat.size > 1024 * 1024) {
|
|
4868
6349
|
return diagnostic("scripts", "warn", "package.json is not a readable regular file.", {
|
|
4869
6350
|
packageJson: false,
|
|
@@ -4885,17 +6366,17 @@ var inspectScripts = async (workspace, allowedChecks) => {
|
|
|
4885
6366
|
}
|
|
4886
6367
|
};
|
|
4887
6368
|
var isInsidePath2 = (root, candidate) => {
|
|
4888
|
-
const relative =
|
|
4889
|
-
return relative === "" || !relative.startsWith(`..${
|
|
6369
|
+
const relative = path9.relative(root, candidate);
|
|
6370
|
+
return relative === "" || !relative.startsWith(`..${path9.sep}`) && relative !== ".." && !path9.isAbsolute(relative);
|
|
4890
6371
|
};
|
|
4891
6372
|
var containsSymlink = async (candidate, trustedPrefix) => {
|
|
4892
|
-
const parsed =
|
|
4893
|
-
const parts = trustedPrefix ?
|
|
4894
|
-
let current = trustedPrefix ??
|
|
6373
|
+
const parsed = path9.parse(candidate);
|
|
6374
|
+
const parts = trustedPrefix ? path9.relative(trustedPrefix, candidate).split(path9.sep).filter(Boolean) : candidate.slice(parsed.root.length).split(path9.sep).filter(Boolean).slice(1);
|
|
6375
|
+
let current = trustedPrefix ?? path9.join(parsed.root, candidate.slice(parsed.root.length).split(path9.sep).filter(Boolean)[0] ?? "");
|
|
4895
6376
|
for (const part of parts) {
|
|
4896
|
-
current =
|
|
6377
|
+
current = path9.join(current, part);
|
|
4897
6378
|
try {
|
|
4898
|
-
if ((await
|
|
6379
|
+
if ((await lstat7(current)).isSymbolicLink()) {
|
|
4899
6380
|
return true;
|
|
4900
6381
|
}
|
|
4901
6382
|
} catch (error) {
|
|
@@ -4911,12 +6392,12 @@ var nearestExistingPath = async (candidate) => {
|
|
|
4911
6392
|
let current = candidate;
|
|
4912
6393
|
for (;; ) {
|
|
4913
6394
|
try {
|
|
4914
|
-
return { path: current, stat: await
|
|
6395
|
+
return { path: current, stat: await stat4(current) };
|
|
4915
6396
|
} catch (error) {
|
|
4916
6397
|
if (error.code !== "ENOENT") {
|
|
4917
6398
|
throw error;
|
|
4918
6399
|
}
|
|
4919
|
-
const parent =
|
|
6400
|
+
const parent = path9.dirname(current);
|
|
4920
6401
|
if (parent === current) {
|
|
4921
6402
|
throw error;
|
|
4922
6403
|
}
|
|
@@ -4932,7 +6413,7 @@ var inspectStateDirectory = async (workspace, stateDirectory) => {
|
|
|
4932
6413
|
});
|
|
4933
6414
|
}
|
|
4934
6415
|
if (isInsidePath2(workspace, stateDirectory)) {
|
|
4935
|
-
const segments =
|
|
6416
|
+
const segments = path9.relative(workspace, stateDirectory).split(path9.sep).filter(Boolean);
|
|
4936
6417
|
const sensitiveSegment = segments.find(isSensitiveStateSegment);
|
|
4937
6418
|
if (sensitiveSegment) {
|
|
4938
6419
|
return diagnostic("state-directory", "fail", "State directory is inside a sensitive workspace path.", {
|
|
@@ -4979,9 +6460,9 @@ var inspectOperationsStore = async (stateDirectory, storeBackend) => {
|
|
|
4979
6460
|
migrationAvailable: true
|
|
4980
6461
|
});
|
|
4981
6462
|
}
|
|
4982
|
-
const databasePath =
|
|
6463
|
+
const databasePath = path9.join(stateDirectory, "operations.sqlite");
|
|
4983
6464
|
try {
|
|
4984
|
-
const entry = await
|
|
6465
|
+
const entry = await lstat7(databasePath);
|
|
4985
6466
|
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
4986
6467
|
return diagnostic("operations-store", "fail", "SQLite state path is not a regular file.", {
|
|
4987
6468
|
backend: storeBackend,
|
|
@@ -5269,6 +6750,95 @@ var manageRuns = async (options) => {
|
|
|
5269
6750
|
persistence.close();
|
|
5270
6751
|
}
|
|
5271
6752
|
};
|
|
6753
|
+
var openSessionStoreForConfig = (config) => openCliSessionStore({
|
|
6754
|
+
workspace: config.workspace,
|
|
6755
|
+
stateDirectory: config.stateDirectory,
|
|
6756
|
+
scope: config.scope
|
|
6757
|
+
});
|
|
6758
|
+
var printSessionDocument = (document, json) => {
|
|
6759
|
+
if (json) {
|
|
6760
|
+
process.stdout.write(`${JSON.stringify(document, null, 2)}
|
|
6761
|
+
`);
|
|
6762
|
+
return;
|
|
6763
|
+
}
|
|
6764
|
+
const record = document;
|
|
6765
|
+
if (record.kind === "session-list") {
|
|
6766
|
+
for (const session2 of record.sessions ?? []) {
|
|
6767
|
+
const latest = session2.runs.at(-1);
|
|
6768
|
+
process.stdout.write(`${session2.sessionId} ${session2.title ?? "(untitled)"} \xB7 ${session2.runs.length} runs` + `${latest ? ` \xB7 ${latest.provider}/${latest.model} \xB7 ${latest.status}` : ""}
|
|
6769
|
+
`);
|
|
6770
|
+
}
|
|
6771
|
+
return;
|
|
6772
|
+
}
|
|
6773
|
+
const session = record.session;
|
|
6774
|
+
if (session) {
|
|
6775
|
+
process.stdout.write(`${session.sessionId} \xB7 ${session.title ?? "(untitled)"} \xB7 ${session.runs.length} runs` + `${session.archivedAt ? " \xB7 archived" : ""}
|
|
6776
|
+
`);
|
|
6777
|
+
return;
|
|
6778
|
+
}
|
|
6779
|
+
process.stdout.write(`${JSON.stringify(document, null, 2)}
|
|
6780
|
+
`);
|
|
6781
|
+
};
|
|
6782
|
+
var manageSessions = async (options) => {
|
|
6783
|
+
const config = resolveHarnessConfig(options);
|
|
6784
|
+
const store = await openSessionStoreForConfig(config);
|
|
6785
|
+
try {
|
|
6786
|
+
let session;
|
|
6787
|
+
let document;
|
|
6788
|
+
switch (options.sessionsCommand) {
|
|
6789
|
+
case "list": {
|
|
6790
|
+
const summaries = await store.list(options.limit ? { limit: options.limit } : undefined);
|
|
6791
|
+
const sessions = (await Promise.all(summaries.map((summary) => store.get(summary.sessionId)))).filter((value) => Boolean(value));
|
|
6792
|
+
document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session-list", sessions };
|
|
6793
|
+
break;
|
|
6794
|
+
}
|
|
6795
|
+
case "inspect":
|
|
6796
|
+
session = await store.get(options.sessionId);
|
|
6797
|
+
if (!session)
|
|
6798
|
+
throw new Error(`Session ${options.sessionId} was not found.`);
|
|
6799
|
+
document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
|
|
6800
|
+
break;
|
|
6801
|
+
case "rename":
|
|
6802
|
+
session = await store.rename(options.sessionId, options.sessionTitle);
|
|
6803
|
+
document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
|
|
6804
|
+
break;
|
|
6805
|
+
case "fork":
|
|
6806
|
+
session = await store.fork(options.sessionId);
|
|
6807
|
+
document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
|
|
6808
|
+
break;
|
|
6809
|
+
case "archive":
|
|
6810
|
+
session = await store.archive(options.sessionId);
|
|
6811
|
+
document = { schemaVersion: CLI_JSON_SCHEMA_VERSION, kind: "session", session };
|
|
6812
|
+
break;
|
|
6813
|
+
default:
|
|
6814
|
+
throw new CliUsageError("sessions requires one of: list, inspect, rename, fork, archive.");
|
|
6815
|
+
}
|
|
6816
|
+
printSessionDocument(document, options.json);
|
|
6817
|
+
} finally {
|
|
6818
|
+
store.close();
|
|
6819
|
+
}
|
|
6820
|
+
};
|
|
6821
|
+
var sessionStatus = (status) => {
|
|
6822
|
+
if (status === "queued")
|
|
6823
|
+
return "created";
|
|
6824
|
+
if (status === "suspended")
|
|
6825
|
+
return "waiting_approval";
|
|
6826
|
+
return status;
|
|
6827
|
+
};
|
|
6828
|
+
var updateIndexedSessionRun = async (config, runId, status) => {
|
|
6829
|
+
const store = await openSessionStoreForConfig(config);
|
|
6830
|
+
try {
|
|
6831
|
+
for (const summary of await store.list({ limit: 200, includeArchived: true })) {
|
|
6832
|
+
const session = await store.get(summary.sessionId);
|
|
6833
|
+
if (session?.runs.some((run) => run.runId === runId)) {
|
|
6834
|
+
await store.updateRun(session.sessionId, runId, { status: sessionStatus(status) });
|
|
6835
|
+
return;
|
|
6836
|
+
}
|
|
6837
|
+
}
|
|
6838
|
+
} finally {
|
|
6839
|
+
store.close();
|
|
6840
|
+
}
|
|
6841
|
+
};
|
|
5272
6842
|
var cliExitCodeForError = (error) => error instanceof CliUsageError ? CLI_EXIT_CODES.usageError : CLI_EXIT_CODES.runtimeError;
|
|
5273
6843
|
var main = async (argv = process.argv.slice(2)) => {
|
|
5274
6844
|
const options = parseCliArgs(argv);
|
|
@@ -5296,7 +6866,14 @@ var main = async (argv = process.argv.slice(2)) => {
|
|
|
5296
6866
|
case "runs":
|
|
5297
6867
|
await manageRuns(options);
|
|
5298
6868
|
return;
|
|
6869
|
+
case "sessions":
|
|
6870
|
+
await manageSessions(options);
|
|
6871
|
+
return;
|
|
5299
6872
|
case "run":
|
|
6873
|
+
if (options.implicitCommand && !options.prompt && process.stdin.isTTY && process.stdout.isTTY) {
|
|
6874
|
+
await chat(options);
|
|
6875
|
+
return;
|
|
6876
|
+
}
|
|
5300
6877
|
await runOnce(options);
|
|
5301
6878
|
return;
|
|
5302
6879
|
case "review":
|
|
@@ -5312,9 +6889,11 @@ if (import.meta.main) {
|
|
|
5312
6889
|
});
|
|
5313
6890
|
}
|
|
5314
6891
|
export {
|
|
6892
|
+
withTemporaryHarnessProfiles,
|
|
5315
6893
|
summarizeApproval,
|
|
5316
6894
|
runResultDocument,
|
|
5317
6895
|
resumeCommand,
|
|
6896
|
+
readHarnessResumeRoutes,
|
|
5318
6897
|
readHarnessResumeConfig,
|
|
5319
6898
|
providersDocument,
|
|
5320
6899
|
parseCliArgs,
|