@zhivex-ai/harness 1.2.0-rc.12 → 1.2.0-rc.13
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 +8 -0
- package/README.md +4 -4
- package/dist/acp-cli.js +29 -1
- package/dist/{cli-3qwxrh91.js → cli-3jmy4h5h.js} +212 -22
- package/dist/{cli-entry-b0dtz99m.js → cli-entry-c2f58zj2.js} +30 -2
- package/dist/cli.js +1 -1
- package/dist/hostile-repository-demo.js +1 -1
- package/dist/index.js +242 -26
- package/dist/persistence/operations.d.ts +5 -1
- package/dist/persistence/state-backup-stream.d.ts +4 -0
- package/dist/service-cli.js +26 -1
- package/dist/{service-client-cli-50sympxb.js → service-client-cli-yx56n0hc.js} +1 -1
- package/dist/verify-historical-migrations.js +62 -19
- package/dist/workspace/file-security.d.ts +6 -0
- package/dist/zhx.js +1 -1
- package/docs/DURABLE_OPERATIONS.md +8 -0
- package/docs/EXTENSIBILITY.md +1 -1
- package/docs/FIRST_USE.md +6 -6
- package/evaluations/representative-assembly-matrix.json +10 -1
- package/examples/README.md +2 -2
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to Zhivex Harness are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The project follows Semantic Versioning. During `0.x`, minor releases may change user-facing contracts when the change is documented with a migration note. Patch releases remain backwards compatible bug fixes.
|
|
6
6
|
|
|
7
|
+
## 1.2.0-rc.13 - 2026-09-26
|
|
8
|
+
|
|
9
|
+
- Store SQLite run history incrementally with published Core 1.26.0 and Agents 1.10.1. Keep the 4 MiB limit on the active checkpoint while retaining steps, tool results and compaction history separately; unchanged history is not rewritten.
|
|
10
|
+
- Export backups above 64 MiB through a bounded, versioned segment stream, preserving legacy JSON imports, checksums and atomic publication.
|
|
11
|
+
- Preserve full history through reopen and portable backup/restore, including legacy inline states. Report state-limit failures with bounded diagnostics and retain the SDK terminal failure checkpoint.
|
|
12
|
+
- Add a 200-read, 99-compaction regression covering more than 11 MB of hydrated history with a checkpoint below 48 KB. Full hydrated loads and disk retention remain proportional to history size.
|
|
13
|
+
- Prepare fresh exact-artifact, live and representative certification for RC13; RC12 publication evidence remains historical.
|
|
14
|
+
|
|
7
15
|
## 1.2.0-rc.12 - 2026-09-26
|
|
8
16
|
|
|
9
17
|
- Preserve optional edit/retry fields in OpenAI Responses tool schemas; keep runtime validation, read-bound digests and approval checks unchanged.
|
package/README.md
CHANGED
|
@@ -11,12 +11,12 @@ implement a change. Conversation context retains decisions, hypotheses and bound
|
|
|
11
11
|
diagnostic excerpts across compaction. Edits and command execution keep their
|
|
12
12
|
own permission checks; a conversational summary never grants approval.
|
|
13
13
|
|
|
14
|
-
Version `1.2.0-rc.
|
|
15
|
-
with
|
|
14
|
+
Version `1.2.0-rc.13` is prepared as an unpublished release candidate for npm `next`,
|
|
15
|
+
with incremental durable history and bounded active checkpoints for long conversations. Certification
|
|
16
16
|
and publication remain pending. The
|
|
17
17
|
[repository release status](https://raw.githubusercontent.com/Zhivex/zhivex-harness/main/release-status.json)
|
|
18
18
|
preserves historical stable evidence. The installation commands below target
|
|
19
|
-
1.2.0-rc.
|
|
19
|
+
1.2.0-rc.13 once published; stable users can install `@zhivex-ai/harness@latest`.
|
|
20
20
|
Desktop retains its separate private alpha status.
|
|
21
21
|
|
|
22
22
|
## Quick start
|
|
@@ -24,7 +24,7 @@ Desktop retains its separate private alpha status.
|
|
|
24
24
|
Requires Node.js 22.13.0 or newer, Git and a provider API key.
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
|
-
npm install -g @zhivex-ai/harness@1.2.0-rc.
|
|
27
|
+
npm install -g @zhivex-ai/harness@1.2.0-rc.13
|
|
28
28
|
cd /path/to/your/project
|
|
29
29
|
zhx
|
|
30
30
|
```
|
package/dist/acp-cli.js
CHANGED
|
@@ -2484,6 +2484,31 @@ var readRegularFileNoFollow = async (target, options) => {
|
|
|
2484
2484
|
await handle.close();
|
|
2485
2485
|
}
|
|
2486
2486
|
};
|
|
2487
|
+
async function* readRegularFileChunksNoFollow(target, options) {
|
|
2488
|
+
const handle = await openRegularFileNoFollow(target, options.label);
|
|
2489
|
+
try {
|
|
2490
|
+
const before = await handle.stat();
|
|
2491
|
+
if (!before.isFile() || options.requireSingleLink && before.nlink !== 1)
|
|
2492
|
+
throw invalidFileError(options.label);
|
|
2493
|
+
if (options.requirePrivate && (before.mode & 63) !== 0)
|
|
2494
|
+
throw new Error(`${options.label} permissions must not grant group or other access.`);
|
|
2495
|
+
let offset = 0;
|
|
2496
|
+
while (offset < before.size) {
|
|
2497
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, before.size - offset));
|
|
2498
|
+
const { bytesRead: bytesRead2 } = await handle.read(buffer, 0, buffer.length, offset);
|
|
2499
|
+
if (!bytesRead2)
|
|
2500
|
+
throw new FileChangedWhileReadingError(options.label);
|
|
2501
|
+
offset += bytesRead2;
|
|
2502
|
+
yield buffer.subarray(0, bytesRead2);
|
|
2503
|
+
}
|
|
2504
|
+
const probe = Buffer.allocUnsafe(1);
|
|
2505
|
+
const { bytesRead } = await handle.read(probe, 0, 1, offset);
|
|
2506
|
+
if (bytesRead || !sameOpenFile(before, await handle.stat()))
|
|
2507
|
+
throw new FileChangedWhileReadingError(options.label);
|
|
2508
|
+
} finally {
|
|
2509
|
+
await handle.close();
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2487
2512
|
|
|
2488
2513
|
// src/cli/cli-profiles.ts
|
|
2489
2514
|
var CLI_PROFILE_SCHEMA_VERSION = 1;
|
|
@@ -11786,7 +11811,7 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
11786
11811
|
database.exec("PRAGMA foreign_keys = ON");
|
|
11787
11812
|
await chmod3(databasePath, 384);
|
|
11788
11813
|
const databaseLike = sqliteAdapter(database);
|
|
11789
|
-
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
11814
|
+
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
11790
11815
|
const memory = createSqliteAgentMemoryStore({ db: databaseLike, scope: config.scope });
|
|
11791
11816
|
const migration = shouldMigrateLegacyRuns(config, options.migrateLegacyFileStore) ? await migrateLegacyFileRuns(config.stateDirectory, store, config) : emptyMigration();
|
|
11792
11817
|
return {
|
|
@@ -14855,6 +14880,9 @@ var terminalRunFailure = (error) => {
|
|
|
14855
14880
|
const streamOverflow = /^Stream replay buffer exceeded its limit of (\d+) events\.$/.exec(message);
|
|
14856
14881
|
if (streamOverflow)
|
|
14857
14882
|
return `event replay limit reached · ${streamOverflow[1]} events`;
|
|
14883
|
+
const stateOverflow = /^Agent run state is (\d+) bytes and exceeds maxStateBytes=(\d+)\. Offload large tool outputs to artifacts or raise the explicit limit\.$/.exec(message);
|
|
14884
|
+
if (stateOverflow)
|
|
14885
|
+
return `durable state limit reached · ${stateOverflow[1]} / ${stateOverflow[2]} bytes · inspect /status before /continue`;
|
|
14858
14886
|
if (/^Tool is not registered \(sha256:[a-f0-9]{64}\)\.$/.test(message)) {
|
|
14859
14887
|
return "requested tool is not registered · use an available tool with its exact name";
|
|
14860
14888
|
}
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
navigateConsole,
|
|
58
58
|
openCliSessionStore,
|
|
59
59
|
openHarnessPersistence,
|
|
60
|
+
readRegularFileChunksNoFollow,
|
|
60
61
|
readRegularFileNoFollow,
|
|
61
62
|
resolvePackageManager,
|
|
62
63
|
resolveTerminalApprovals,
|
|
@@ -67,6 +68,7 @@ import {
|
|
|
67
68
|
sanitizeTerminalText,
|
|
68
69
|
serializeStreamEvent,
|
|
69
70
|
serializeStreamResult,
|
|
71
|
+
sqliteAdapter,
|
|
70
72
|
taskSources,
|
|
71
73
|
terminalContinuationMessages,
|
|
72
74
|
terminalRunFailure,
|
|
@@ -75,7 +77,7 @@ import {
|
|
|
75
77
|
validateStateDirectory,
|
|
76
78
|
verifyChangeEnvelope,
|
|
77
79
|
workspaceFilePathSchema
|
|
78
|
-
} from "./cli-entry-
|
|
80
|
+
} from "./cli-entry-c2f58zj2.js";
|
|
79
81
|
import {
|
|
80
82
|
BUN_ENGINE_RANGE,
|
|
81
83
|
HARNESS_VERSION,
|
|
@@ -4568,6 +4570,175 @@ var manageChanges = async (options) => {
|
|
|
4568
4570
|
process.exitCode = CLI_EXIT_CODES.runtimeError;
|
|
4569
4571
|
};
|
|
4570
4572
|
|
|
4573
|
+
// src/persistence/state-backup-stream.ts
|
|
4574
|
+
var MAGIC = `ZHIVEX-STATE-SEGMENTS-1
|
|
4575
|
+
`;
|
|
4576
|
+
var STRING_CHARS = 8192;
|
|
4577
|
+
var MAX_FRAME_BYTES = 64 * 1024;
|
|
4578
|
+
function* canonicalChunks(value) {
|
|
4579
|
+
if (typeof value === "string") {
|
|
4580
|
+
yield '"';
|
|
4581
|
+
for (let i = 0;i < value.length; ) {
|
|
4582
|
+
let end = Math.min(i + STRING_CHARS, value.length);
|
|
4583
|
+
if (end < value.length && /[\uD800-\uDBFF]/.test(value[end - 1]))
|
|
4584
|
+
end--;
|
|
4585
|
+
yield JSON.stringify(value.slice(i, end)).slice(1, -1);
|
|
4586
|
+
i = end;
|
|
4587
|
+
}
|
|
4588
|
+
yield '"';
|
|
4589
|
+
} else if (value === null || typeof value !== "object") {
|
|
4590
|
+
yield JSON.stringify(value);
|
|
4591
|
+
} else if (Array.isArray(value)) {
|
|
4592
|
+
yield "[";
|
|
4593
|
+
for (let i = 0;i < value.length; i++) {
|
|
4594
|
+
if (i)
|
|
4595
|
+
yield ",";
|
|
4596
|
+
yield* canonicalChunks(value[i]);
|
|
4597
|
+
}
|
|
4598
|
+
yield "]";
|
|
4599
|
+
} else {
|
|
4600
|
+
yield "{";
|
|
4601
|
+
let first = true;
|
|
4602
|
+
for (const key of Object.keys(value).sort()) {
|
|
4603
|
+
if (!first)
|
|
4604
|
+
yield ",";
|
|
4605
|
+
first = false;
|
|
4606
|
+
yield* canonicalChunks(key);
|
|
4607
|
+
yield ":";
|
|
4608
|
+
yield* canonicalChunks(value[key]);
|
|
4609
|
+
}
|
|
4610
|
+
yield "}";
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
function* tokens(value) {
|
|
4614
|
+
if (typeof value === "string") {
|
|
4615
|
+
for (let i = 0;i < value.length; i += STRING_CHARS)
|
|
4616
|
+
yield ["text", value.slice(i, i + STRING_CHARS)];
|
|
4617
|
+
yield ["string"];
|
|
4618
|
+
} else if (value === null || typeof value !== "object")
|
|
4619
|
+
yield ["value", value];
|
|
4620
|
+
else {
|
|
4621
|
+
yield [Array.isArray(value) ? "array" : "object"];
|
|
4622
|
+
if (Array.isArray(value))
|
|
4623
|
+
for (const item of value)
|
|
4624
|
+
yield* tokens(item);
|
|
4625
|
+
else
|
|
4626
|
+
for (const [key, item] of Object.entries(value)) {
|
|
4627
|
+
yield* tokens(key);
|
|
4628
|
+
yield* tokens(item);
|
|
4629
|
+
}
|
|
4630
|
+
yield ["end"];
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
function* segmentedChunks(value) {
|
|
4634
|
+
yield MAGIC;
|
|
4635
|
+
for (const token of tokens(value))
|
|
4636
|
+
yield `${JSON.stringify(token)}
|
|
4637
|
+
`;
|
|
4638
|
+
}
|
|
4639
|
+
async function readBackupTransport(source, legacyMaxBytes) {
|
|
4640
|
+
const fail = () => {
|
|
4641
|
+
throw new Error("Harness state backup segment stream is invalid or incomplete.");
|
|
4642
|
+
};
|
|
4643
|
+
const stack = [];
|
|
4644
|
+
let root;
|
|
4645
|
+
let hasRoot = false;
|
|
4646
|
+
let pieces = [];
|
|
4647
|
+
let pending = Buffer.alloc(0);
|
|
4648
|
+
let segmented;
|
|
4649
|
+
const legacy = [];
|
|
4650
|
+
let legacyBytes = 0;
|
|
4651
|
+
const append = (value) => {
|
|
4652
|
+
const parent = stack.at(-1);
|
|
4653
|
+
if (!parent) {
|
|
4654
|
+
if (hasRoot)
|
|
4655
|
+
fail();
|
|
4656
|
+
root = value;
|
|
4657
|
+
hasRoot = true;
|
|
4658
|
+
} else if (Array.isArray(parent.value))
|
|
4659
|
+
parent.value.push(value);
|
|
4660
|
+
else if (parent.key === undefined) {
|
|
4661
|
+
if (typeof value !== "string" || Object.hasOwn(parent.value, value))
|
|
4662
|
+
fail();
|
|
4663
|
+
parent.key = value;
|
|
4664
|
+
} else {
|
|
4665
|
+
Object.defineProperty(parent.value, parent.key, { value, enumerable: true, writable: true, configurable: true });
|
|
4666
|
+
delete parent.key;
|
|
4667
|
+
}
|
|
4668
|
+
};
|
|
4669
|
+
const consume = (line) => {
|
|
4670
|
+
if (line.length > MAX_FRAME_BYTES)
|
|
4671
|
+
fail();
|
|
4672
|
+
const token = JSON.parse(line.toString("utf8"));
|
|
4673
|
+
if (!Array.isArray(token))
|
|
4674
|
+
fail();
|
|
4675
|
+
const frame = token;
|
|
4676
|
+
const [kind, value] = frame;
|
|
4677
|
+
if (kind === "text" && frame.length === 2 && typeof value === "string") {
|
|
4678
|
+
pieces.push(value);
|
|
4679
|
+
return;
|
|
4680
|
+
}
|
|
4681
|
+
if (kind === "string" && frame.length === 1) {
|
|
4682
|
+
append(pieces.join(""));
|
|
4683
|
+
pieces = [];
|
|
4684
|
+
return;
|
|
4685
|
+
}
|
|
4686
|
+
if (pieces.length)
|
|
4687
|
+
fail();
|
|
4688
|
+
if (kind === "value" && frame.length === 2 && (value === null || typeof value === "boolean" || typeof value === "number"))
|
|
4689
|
+
append(value);
|
|
4690
|
+
else if ((kind === "array" || kind === "object") && frame.length === 1) {
|
|
4691
|
+
if (stack.length >= 512)
|
|
4692
|
+
fail();
|
|
4693
|
+
const value2 = kind === "array" ? [] : {};
|
|
4694
|
+
append(value2);
|
|
4695
|
+
stack.push({ value: value2 });
|
|
4696
|
+
} else if (kind === "end" && frame.length === 1) {
|
|
4697
|
+
const parent = stack.pop();
|
|
4698
|
+
if (!parent || parent.key !== undefined)
|
|
4699
|
+
fail();
|
|
4700
|
+
} else
|
|
4701
|
+
fail();
|
|
4702
|
+
};
|
|
4703
|
+
for await (const chunk of readRegularFileChunksNoFollow(source, {
|
|
4704
|
+
label: "Harness state backup",
|
|
4705
|
+
requireSingleLink: true,
|
|
4706
|
+
requirePrivate: true
|
|
4707
|
+
})) {
|
|
4708
|
+
pending = Buffer.concat([pending, chunk]);
|
|
4709
|
+
if (segmented === undefined) {
|
|
4710
|
+
if (pending.length < Buffer.byteLength(MAGIC))
|
|
4711
|
+
continue;
|
|
4712
|
+
segmented = pending.subarray(0, MAGIC.length).toString() === MAGIC;
|
|
4713
|
+
if (segmented)
|
|
4714
|
+
pending = pending.subarray(MAGIC.length);
|
|
4715
|
+
}
|
|
4716
|
+
if (!segmented) {
|
|
4717
|
+
legacyBytes += pending.length;
|
|
4718
|
+
if (legacyBytes > legacyMaxBytes)
|
|
4719
|
+
throw new Error("Harness state backup exceeds the legacy size limit.");
|
|
4720
|
+
legacy.push(pending);
|
|
4721
|
+
pending = Buffer.alloc(0);
|
|
4722
|
+
continue;
|
|
4723
|
+
}
|
|
4724
|
+
let newline;
|
|
4725
|
+
while ((newline = pending.indexOf(10)) !== -1) {
|
|
4726
|
+
consume(pending.subarray(0, newline));
|
|
4727
|
+
pending = pending.subarray(newline + 1);
|
|
4728
|
+
}
|
|
4729
|
+
if (pending.length > MAX_FRAME_BYTES)
|
|
4730
|
+
fail();
|
|
4731
|
+
}
|
|
4732
|
+
if (!segmented) {
|
|
4733
|
+
if (legacyBytes + pending.length > legacyMaxBytes)
|
|
4734
|
+
throw new Error("Harness state backup exceeds the legacy size limit.");
|
|
4735
|
+
return JSON.parse(Buffer.concat([...legacy, pending]).toString("utf8"));
|
|
4736
|
+
}
|
|
4737
|
+
if (pending.length || stack.length || pieces.length || !hasRoot)
|
|
4738
|
+
fail();
|
|
4739
|
+
return root;
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4571
4742
|
// src/persistence/workspace-checkpoints.ts
|
|
4572
4743
|
import { createHash as createHash2, randomUUID as randomUUID3 } from "node:crypto";
|
|
4573
4744
|
import { z as z3 } from "zod";
|
|
@@ -4599,6 +4770,7 @@ import { chmod, link, lstat as lstat3, open as open2, realpath, unlink as unlink
|
|
|
4599
4770
|
import path5 from "node:path";
|
|
4600
4771
|
import { z as z4 } from "zod";
|
|
4601
4772
|
import { normalizeAgentRunState } from "@zhivex-ai/agents";
|
|
4773
|
+
import { createSqliteAgentRunStore } from "@zhivex-ai/agents/ops";
|
|
4602
4774
|
var HARNESS_STATE_BACKUP_SCHEMA_VERSION = 1;
|
|
4603
4775
|
var HARNESS_STATE_BACKUP_MAX_BYTES = 64 * 1024 * 1024;
|
|
4604
4776
|
var terminalRunStatuses = new Set(["completed", "failed", "cancelled", "timed_out"]);
|
|
@@ -4733,7 +4905,12 @@ var canonicalJson = (value) => {
|
|
|
4733
4905
|
return `[${value.map(canonicalJson).join(",")}]`;
|
|
4734
4906
|
return `{${Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
4735
4907
|
};
|
|
4736
|
-
var checksumPayload = (payload) =>
|
|
4908
|
+
var checksumPayload = (payload) => {
|
|
4909
|
+
const hash = createHash3("sha256");
|
|
4910
|
+
for (const chunk of canonicalChunks(payload))
|
|
4911
|
+
hash.update(chunk);
|
|
4912
|
+
return `sha256:${hash.digest("hex")}`;
|
|
4913
|
+
};
|
|
4737
4914
|
var stableKey = (kind, value) => createHash3("sha256").update(kind).update("\x00").update(value).digest("hex");
|
|
4738
4915
|
var scopeValue = (config) => `${config.scope.tenantId}\x00${config.scope.userId ?? ""}\x00${config.scope.namespace ?? ""}`;
|
|
4739
4916
|
var scopePrefix = (config) => `${encodeURIComponent(config.scope.namespace ?? "default")}:${encodeURIComponent(config.scope.tenantId)}:${encodeURIComponent(config.scope.userId ?? "*")}:`;
|
|
@@ -4765,8 +4942,8 @@ var validateBudgetLedger = (config, run, identities) => {
|
|
|
4765
4942
|
if (!/^sha256:[a-f0-9]{64}$/.test(id) || !value || typeof value !== "object" || Array.isArray(value) || value.status !== "confirmed") {
|
|
4766
4943
|
throw new HarnessStateConflictError("Budget ledger retains unresolved reservations or unknown consumption.");
|
|
4767
4944
|
}
|
|
4768
|
-
const
|
|
4769
|
-
if (!
|
|
4945
|
+
const tokens2 = value.tokens;
|
|
4946
|
+
if (!tokens2 || typeof tokens2 !== "object" || Array.isArray(tokens2) || ![tokens2.inputTokens, tokens2.outputTokens, tokens2.totalTokens].every((amount) => typeof amount === "number" && Number.isSafeInteger(amount) && amount >= 0) || tokens2.totalTokens < tokens2.inputTokens + tokens2.outputTokens) {
|
|
4770
4947
|
throw new HarnessStateConflictError("Invalid budget ledger token receipt.");
|
|
4771
4948
|
}
|
|
4772
4949
|
}
|
|
@@ -4868,8 +5045,13 @@ var readPayload = async (config, recordedWorkspace, accessLease) => {
|
|
|
4868
5045
|
if (recordedWorkspace && database.query("SELECT version FROM zhivex_cli_session_schema WHERE singleton=1").get()?.version !== HARNESS_SESSION_SCHEMA_VERSION)
|
|
4869
5046
|
throw new HarnessStateConflictError("Archived session schema is incompatible.");
|
|
4870
5047
|
const allRunRows = database.query("SELECT run_id, state_json, updated_at_ms FROM zhivex_agent_runs ORDER BY run_id").all();
|
|
5048
|
+
let historyReader;
|
|
4871
5049
|
const runRows = allRunRows.flatMap((row) => {
|
|
4872
|
-
const
|
|
5050
|
+
const record = parseJsonRecord(row.state_json, `Run ${row.run_id}`);
|
|
5051
|
+
const hydrated = record.checkpointHistory ? (historyReader ??= createSqliteAgentRunStore({ db: sqliteAdapter(database), history: "incremental" })).load(String(record.runId), record.scope) : record;
|
|
5052
|
+
if (hydrated instanceof Promise || !hydrated)
|
|
5053
|
+
throw new HarnessStateConflictError("Cannot hydrate state backup history.");
|
|
5054
|
+
const state = validatedRunState(hydrated, `Run ${row.run_id}`);
|
|
4873
5055
|
return stateMatchesScope(config, state) ? [{ row, state }] : [];
|
|
4874
5056
|
});
|
|
4875
5057
|
const runs = runRows.map(({ row, state }) => {
|
|
@@ -5029,7 +5211,19 @@ var writePrivateBackup = async (target, contents) => {
|
|
|
5029
5211
|
try {
|
|
5030
5212
|
handle = await open2(staged, fsConstants3.O_WRONLY | fsConstants3.O_CREAT | fsConstants3.O_EXCL | fsConstants3.O_NOFOLLOW, 384);
|
|
5031
5213
|
stagedExists = true;
|
|
5032
|
-
|
|
5214
|
+
let pending = [];
|
|
5215
|
+
let pendingBytes = 0;
|
|
5216
|
+
for (const chunk of contents) {
|
|
5217
|
+
pending.push(chunk);
|
|
5218
|
+
pendingBytes += Buffer.byteLength(chunk);
|
|
5219
|
+
if (pendingBytes >= 64 * 1024) {
|
|
5220
|
+
await handle.writeFile(pending.join(""), "utf8");
|
|
5221
|
+
pending = [];
|
|
5222
|
+
pendingBytes = 0;
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
5225
|
+
if (pending.length)
|
|
5226
|
+
await handle.writeFile(pending.join(""), "utf8");
|
|
5033
5227
|
await handle.sync();
|
|
5034
5228
|
await chmod(staged, 384);
|
|
5035
5229
|
await handle.close();
|
|
@@ -5056,12 +5250,13 @@ var writePrivateBackup = async (target, contents) => {
|
|
|
5056
5250
|
};
|
|
5057
5251
|
var exportHarnessStateBackup = async (config, target) => {
|
|
5058
5252
|
const bundle = await createHarnessStateBackup(config);
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5253
|
+
let bytes = 0;
|
|
5254
|
+
for (const chunk of canonicalChunks(bundle)) {
|
|
5255
|
+
bytes += Buffer.byteLength(chunk);
|
|
5256
|
+
if (bytes > HARNESS_STATE_BACKUP_MAX_BYTES)
|
|
5257
|
+
break;
|
|
5063
5258
|
}
|
|
5064
|
-
await writePrivateBackup(target,
|
|
5259
|
+
await writePrivateBackup(target, bytes > HARNESS_STATE_BACKUP_MAX_BYTES ? segmentedChunks(bundle) : canonicalChunks(bundle));
|
|
5065
5260
|
return {
|
|
5066
5261
|
schemaVersion: HARNESS_STATE_BACKUP_SCHEMA_VERSION,
|
|
5067
5262
|
kind: "state-export",
|
|
@@ -5071,19 +5266,14 @@ var exportHarnessStateBackup = async (config, target) => {
|
|
|
5071
5266
|
};
|
|
5072
5267
|
};
|
|
5073
5268
|
var readHarnessStateBackup = async (source) => {
|
|
5074
|
-
const { contents, stat: stat2 } = await readRegularFileNoFollow(path5.resolve(source), {
|
|
5075
|
-
label: "Harness state backup",
|
|
5076
|
-
maxBytes: HARNESS_STATE_BACKUP_MAX_BYTES,
|
|
5077
|
-
requireSingleLink: true
|
|
5078
|
-
});
|
|
5079
|
-
if ((stat2.mode & 63) !== 0) {
|
|
5080
|
-
throw new HarnessWorkspaceError("Harness state backup permissions must not grant group or other access.");
|
|
5081
|
-
}
|
|
5082
5269
|
let input;
|
|
5083
5270
|
try {
|
|
5084
|
-
input =
|
|
5271
|
+
input = await readBackupTransport(path5.resolve(source), HARNESS_STATE_BACKUP_MAX_BYTES);
|
|
5085
5272
|
} catch (error) {
|
|
5086
|
-
|
|
5273
|
+
if (error instanceof SyntaxError) {
|
|
5274
|
+
throw new HarnessStateConflictError("Harness state backup is not valid JSON.", { cause: error });
|
|
5275
|
+
}
|
|
5276
|
+
throw error;
|
|
5087
5277
|
}
|
|
5088
5278
|
return validateStateBackupBundle(input);
|
|
5089
5279
|
};
|
|
@@ -5484,7 +5674,7 @@ var main = async (argv = process.argv.slice(2)) => {
|
|
|
5484
5674
|
parsedOptions = { ...parsedOptions, prompt: await readStdinTask() };
|
|
5485
5675
|
}
|
|
5486
5676
|
if (parsedOptions.serviceFile) {
|
|
5487
|
-
const { runServiceCli } = await import("./service-client-cli-
|
|
5677
|
+
const { runServiceCli } = await import("./service-client-cli-yx56n0hc.js");
|
|
5488
5678
|
await runServiceCli(parsedOptions, annotateCliStreamError);
|
|
5489
5679
|
return;
|
|
5490
5680
|
}
|
|
@@ -2075,6 +2075,9 @@ var terminalRunFailure = (error) => {
|
|
|
2075
2075
|
const streamOverflow = /^Stream replay buffer exceeded its limit of (\d+) events\.$/.exec(message);
|
|
2076
2076
|
if (streamOverflow)
|
|
2077
2077
|
return `event replay limit reached · ${streamOverflow[1]} events`;
|
|
2078
|
+
const stateOverflow = /^Agent run state is (\d+) bytes and exceeds maxStateBytes=(\d+)\. Offload large tool outputs to artifacts or raise the explicit limit\.$/.exec(message);
|
|
2079
|
+
if (stateOverflow)
|
|
2080
|
+
return `durable state limit reached · ${stateOverflow[1]} / ${stateOverflow[2]} bytes · inspect /status before /continue`;
|
|
2078
2081
|
if (/^Tool is not registered \(sha256:[a-f0-9]{64}\)\.$/.test(message)) {
|
|
2079
2082
|
return "requested tool is not registered · use an available tool with its exact name";
|
|
2080
2083
|
}
|
|
@@ -3503,6 +3506,31 @@ var readRegularFileNoFollow = async (target, options) => {
|
|
|
3503
3506
|
await handle.close();
|
|
3504
3507
|
}
|
|
3505
3508
|
};
|
|
3509
|
+
async function* readRegularFileChunksNoFollow(target, options) {
|
|
3510
|
+
const handle = await openRegularFileNoFollow(target, options.label);
|
|
3511
|
+
try {
|
|
3512
|
+
const before = await handle.stat();
|
|
3513
|
+
if (!before.isFile() || options.requireSingleLink && before.nlink !== 1)
|
|
3514
|
+
throw invalidFileError(options.label);
|
|
3515
|
+
if (options.requirePrivate && (before.mode & 63) !== 0)
|
|
3516
|
+
throw new Error(`${options.label} permissions must not grant group or other access.`);
|
|
3517
|
+
let offset = 0;
|
|
3518
|
+
while (offset < before.size) {
|
|
3519
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, before.size - offset));
|
|
3520
|
+
const { bytesRead: bytesRead2 } = await handle.read(buffer, 0, buffer.length, offset);
|
|
3521
|
+
if (!bytesRead2)
|
|
3522
|
+
throw new FileChangedWhileReadingError(options.label);
|
|
3523
|
+
offset += bytesRead2;
|
|
3524
|
+
yield buffer.subarray(0, bytesRead2);
|
|
3525
|
+
}
|
|
3526
|
+
const probe = Buffer.allocUnsafe(1);
|
|
3527
|
+
const { bytesRead } = await handle.read(probe, 0, 1, offset);
|
|
3528
|
+
if (bytesRead || !sameOpenFile(before, await handle.stat()))
|
|
3529
|
+
throw new FileChangedWhileReadingError(options.label);
|
|
3530
|
+
} finally {
|
|
3531
|
+
await handle.close();
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3506
3534
|
|
|
3507
3535
|
// src/integrations/mcp.ts
|
|
3508
3536
|
import { lstat as lstat5, realpath as realpath2 } from "node:fs/promises";
|
|
@@ -9347,7 +9375,7 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
9347
9375
|
database.exec("PRAGMA foreign_keys = ON");
|
|
9348
9376
|
await chmod3(databasePath, 384);
|
|
9349
9377
|
const databaseLike = sqliteAdapter(database);
|
|
9350
|
-
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
9378
|
+
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
9351
9379
|
const memory = createSqliteAgentMemoryStore({ db: databaseLike, scope: config.scope });
|
|
9352
9380
|
const migration = shouldMigrateLegacyRuns(config, options.migrateLegacyFileStore) ? await migrateLegacyFileRuns(config.stateDirectory, store, config) : emptyMigration();
|
|
9353
9381
|
return {
|
|
@@ -13571,4 +13599,4 @@ var verifyChangeEnvelope = (input, options = {}) => {
|
|
|
13571
13599
|
};
|
|
13572
13600
|
};
|
|
13573
13601
|
|
|
13574
|
-
export { UnsafeFileTypeError, FileSizeLimitError, FileChangedWhileReadingError, readRegularFileNoFollow, ToolActivity, compactToolResult, SqliteDatabase, validateStateDirectory, HARNESS_SESSION_SCHEMA_VERSION, openCliSessionStore, inspectRuntimeDiagnostics, USAGE_LEDGER_KEY, usagePricingSchema, inspectUsageLedger, formatUsageLedger, CLI_JSON_SCHEMA_VERSION, CLI_EVENT_SCHEMA_VERSION, serializeStreamEvent, serializeStreamResult, runResultDocument, sanitizeTerminalText, formatApproval, resolveTerminalApprovals, terminalSupportsColor, formatVerificationSummary, terminalRunFailure, formatTerminalEvent, formatConsoleHelp, ConsoleInput, chooseReasoning, loadModelCatalog, navigateConsole, loadHarnessMcpConfiguration, resolvePackageManager, fileDigestSchema, workspaceFilePathSchema, runPortableProcess, Workspace, CliOciRuntimeAdapter, cleanupHarnessExecutionArtifacts, DEFAULT_HARNESS_CONTEXT_MANIFEST, MAX_HARNESS_CONTEXT_FILE_BYTES, MAX_HARNESS_CONTEXT_TOTAL_BYTES, loadHarnessProjectContext, ActivityHistory, formatAppliedFiles, harnessToolExecution, terminalContinuationMessages, TASK_SOURCE_KEY, taskSources, formatConsoleWelcome, estimateMessages, HARNESS_SQLITE_FILE, HARNESS_OPERATIONS_SCHEMA_VERSION, listHarnessRuns, inspectHarnessRun, cancelHarnessRun, cleanupHarnessRuns, openHarnessPersistence, runHarnessReviewGroup, compactHarnessMessages, createHarness, runHarness, appendUserMessage, changeEnvelopeSchema, digestChangeEnvelopeArtifact, createChangeEnvelope, canonicalizeChangeEnvelope, changeEnvelopePreconditionsSchema, verifyChangeEnvelope };
|
|
13602
|
+
export { UnsafeFileTypeError, FileSizeLimitError, FileChangedWhileReadingError, readRegularFileNoFollow, readRegularFileChunksNoFollow, ToolActivity, compactToolResult, SqliteDatabase, validateStateDirectory, HARNESS_SESSION_SCHEMA_VERSION, openCliSessionStore, inspectRuntimeDiagnostics, USAGE_LEDGER_KEY, usagePricingSchema, inspectUsageLedger, formatUsageLedger, CLI_JSON_SCHEMA_VERSION, CLI_EVENT_SCHEMA_VERSION, serializeStreamEvent, serializeStreamResult, runResultDocument, sanitizeTerminalText, formatApproval, resolveTerminalApprovals, terminalSupportsColor, formatVerificationSummary, terminalRunFailure, formatTerminalEvent, formatConsoleHelp, ConsoleInput, chooseReasoning, loadModelCatalog, navigateConsole, loadHarnessMcpConfiguration, resolvePackageManager, fileDigestSchema, workspaceFilePathSchema, runPortableProcess, Workspace, CliOciRuntimeAdapter, cleanupHarnessExecutionArtifacts, DEFAULT_HARNESS_CONTEXT_MANIFEST, MAX_HARNESS_CONTEXT_FILE_BYTES, MAX_HARNESS_CONTEXT_TOTAL_BYTES, loadHarnessProjectContext, ActivityHistory, formatAppliedFiles, harnessToolExecution, terminalContinuationMessages, TASK_SOURCE_KEY, taskSources, formatConsoleWelcome, estimateMessages, HARNESS_SQLITE_FILE, HARNESS_OPERATIONS_SCHEMA_VERSION, listHarnessRuns, inspectHarnessRun, cancelHarnessRun, cleanupHarnessRuns, sqliteAdapter, openHarnessPersistence, runHarnessReviewGroup, compactHarnessMessages, createHarness, runHarness, appendUserMessage, changeEnvelopeSchema, digestChangeEnvelopeArtifact, createChangeEnvelope, canonicalizeChangeEnvelope, changeEnvelopePreconditionsSchema, verifyChangeEnvelope };
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,6 @@ if (argv.length === 1 && ["--version", "-v", "version"].includes(argv[0])) {
|
|
|
18
18
|
process.stdout.write(`${shortCliHelp(HARNESS_VERSION)}
|
|
19
19
|
`);
|
|
20
20
|
} else {
|
|
21
|
-
const { runCli } = await import("./cli-
|
|
21
|
+
const { runCli } = await import("./cli-3jmy4h5h.js");
|
|
22
22
|
await runCli(argv);
|
|
23
23
|
}
|
|
@@ -10397,7 +10397,7 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
10397
10397
|
database.exec("PRAGMA foreign_keys = ON");
|
|
10398
10398
|
await chmod3(databasePath, 384);
|
|
10399
10399
|
const databaseLike = sqliteAdapter(database);
|
|
10400
|
-
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
10400
|
+
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
10401
10401
|
const memory = createSqliteAgentMemoryStore({ db: databaseLike, scope: config.scope });
|
|
10402
10402
|
const migration = shouldMigrateLegacyRuns(config, options.migrateLegacyFileStore) ? await migrateLegacyFileRuns(config.stateDirectory, store, config) : emptyMigration();
|
|
10403
10403
|
return {
|
package/dist/index.js
CHANGED
|
@@ -3885,6 +3885,9 @@ var terminalRunFailure = (error) => {
|
|
|
3885
3885
|
const streamOverflow = /^Stream replay buffer exceeded its limit of (\d+) events\.$/.exec(message);
|
|
3886
3886
|
if (streamOverflow)
|
|
3887
3887
|
return `event replay limit reached · ${streamOverflow[1]} events`;
|
|
3888
|
+
const stateOverflow = /^Agent run state is (\d+) bytes and exceeds maxStateBytes=(\d+)\. Offload large tool outputs to artifacts or raise the explicit limit\.$/.exec(message);
|
|
3889
|
+
if (stateOverflow)
|
|
3890
|
+
return `durable state limit reached · ${stateOverflow[1]} / ${stateOverflow[2]} bytes · inspect /status before /continue`;
|
|
3888
3891
|
if (/^Tool is not registered \(sha256:[a-f0-9]{64}\)\.$/.test(message)) {
|
|
3889
3892
|
return "requested tool is not registered · use an available tool with its exact name";
|
|
3890
3893
|
}
|
|
@@ -4899,11 +4902,6 @@ var openCliSessionStore = async (options) => {
|
|
|
4899
4902
|
return store;
|
|
4900
4903
|
};
|
|
4901
4904
|
var openSessionStore = openCliSessionStore;
|
|
4902
|
-
// src/persistence/workspace-checkpoints.ts
|
|
4903
|
-
import { createHash as createHash7, randomUUID as randomUUID2 } from "node:crypto";
|
|
4904
|
-
import path9 from "node:path";
|
|
4905
|
-
import { z as z6 } from "zod";
|
|
4906
|
-
|
|
4907
4905
|
// src/workspace/file-security.ts
|
|
4908
4906
|
import { constants as fsConstants } from "node:fs";
|
|
4909
4907
|
import { open as open3 } from "node:fs/promises";
|
|
@@ -5014,6 +5012,205 @@ var readRegularFileNoFollow = async (target, options) => {
|
|
|
5014
5012
|
await handle.close();
|
|
5015
5013
|
}
|
|
5016
5014
|
};
|
|
5015
|
+
async function* readRegularFileChunksNoFollow(target, options) {
|
|
5016
|
+
const handle = await openRegularFileNoFollow(target, options.label);
|
|
5017
|
+
try {
|
|
5018
|
+
const before = await handle.stat();
|
|
5019
|
+
if (!before.isFile() || options.requireSingleLink && before.nlink !== 1)
|
|
5020
|
+
throw invalidFileError(options.label);
|
|
5021
|
+
if (options.requirePrivate && (before.mode & 63) !== 0)
|
|
5022
|
+
throw new Error(`${options.label} permissions must not grant group or other access.`);
|
|
5023
|
+
let offset = 0;
|
|
5024
|
+
while (offset < before.size) {
|
|
5025
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, before.size - offset));
|
|
5026
|
+
const { bytesRead: bytesRead2 } = await handle.read(buffer, 0, buffer.length, offset);
|
|
5027
|
+
if (!bytesRead2)
|
|
5028
|
+
throw new FileChangedWhileReadingError(options.label);
|
|
5029
|
+
offset += bytesRead2;
|
|
5030
|
+
yield buffer.subarray(0, bytesRead2);
|
|
5031
|
+
}
|
|
5032
|
+
const probe = Buffer.allocUnsafe(1);
|
|
5033
|
+
const { bytesRead } = await handle.read(probe, 0, 1, offset);
|
|
5034
|
+
if (bytesRead || !sameOpenFile(before, await handle.stat()))
|
|
5035
|
+
throw new FileChangedWhileReadingError(options.label);
|
|
5036
|
+
} finally {
|
|
5037
|
+
await handle.close();
|
|
5038
|
+
}
|
|
5039
|
+
}
|
|
5040
|
+
|
|
5041
|
+
// src/persistence/state-backup-stream.ts
|
|
5042
|
+
var MAGIC = `ZHIVEX-STATE-SEGMENTS-1
|
|
5043
|
+
`;
|
|
5044
|
+
var STRING_CHARS = 8192;
|
|
5045
|
+
var MAX_FRAME_BYTES = 64 * 1024;
|
|
5046
|
+
function* canonicalChunks(value) {
|
|
5047
|
+
if (typeof value === "string") {
|
|
5048
|
+
yield '"';
|
|
5049
|
+
for (let i = 0;i < value.length; ) {
|
|
5050
|
+
let end = Math.min(i + STRING_CHARS, value.length);
|
|
5051
|
+
if (end < value.length && /[\uD800-\uDBFF]/.test(value[end - 1]))
|
|
5052
|
+
end--;
|
|
5053
|
+
yield JSON.stringify(value.slice(i, end)).slice(1, -1);
|
|
5054
|
+
i = end;
|
|
5055
|
+
}
|
|
5056
|
+
yield '"';
|
|
5057
|
+
} else if (value === null || typeof value !== "object") {
|
|
5058
|
+
yield JSON.stringify(value);
|
|
5059
|
+
} else if (Array.isArray(value)) {
|
|
5060
|
+
yield "[";
|
|
5061
|
+
for (let i = 0;i < value.length; i++) {
|
|
5062
|
+
if (i)
|
|
5063
|
+
yield ",";
|
|
5064
|
+
yield* canonicalChunks(value[i]);
|
|
5065
|
+
}
|
|
5066
|
+
yield "]";
|
|
5067
|
+
} else {
|
|
5068
|
+
yield "{";
|
|
5069
|
+
let first = true;
|
|
5070
|
+
for (const key of Object.keys(value).sort()) {
|
|
5071
|
+
if (!first)
|
|
5072
|
+
yield ",";
|
|
5073
|
+
first = false;
|
|
5074
|
+
yield* canonicalChunks(key);
|
|
5075
|
+
yield ":";
|
|
5076
|
+
yield* canonicalChunks(value[key]);
|
|
5077
|
+
}
|
|
5078
|
+
yield "}";
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
function* tokens(value) {
|
|
5082
|
+
if (typeof value === "string") {
|
|
5083
|
+
for (let i = 0;i < value.length; i += STRING_CHARS)
|
|
5084
|
+
yield ["text", value.slice(i, i + STRING_CHARS)];
|
|
5085
|
+
yield ["string"];
|
|
5086
|
+
} else if (value === null || typeof value !== "object")
|
|
5087
|
+
yield ["value", value];
|
|
5088
|
+
else {
|
|
5089
|
+
yield [Array.isArray(value) ? "array" : "object"];
|
|
5090
|
+
if (Array.isArray(value))
|
|
5091
|
+
for (const item of value)
|
|
5092
|
+
yield* tokens(item);
|
|
5093
|
+
else
|
|
5094
|
+
for (const [key, item] of Object.entries(value)) {
|
|
5095
|
+
yield* tokens(key);
|
|
5096
|
+
yield* tokens(item);
|
|
5097
|
+
}
|
|
5098
|
+
yield ["end"];
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
function* segmentedChunks(value) {
|
|
5102
|
+
yield MAGIC;
|
|
5103
|
+
for (const token of tokens(value))
|
|
5104
|
+
yield `${JSON.stringify(token)}
|
|
5105
|
+
`;
|
|
5106
|
+
}
|
|
5107
|
+
async function readBackupTransport(source, legacyMaxBytes) {
|
|
5108
|
+
const fail = () => {
|
|
5109
|
+
throw new Error("Harness state backup segment stream is invalid or incomplete.");
|
|
5110
|
+
};
|
|
5111
|
+
const stack = [];
|
|
5112
|
+
let root;
|
|
5113
|
+
let hasRoot = false;
|
|
5114
|
+
let pieces = [];
|
|
5115
|
+
let pending = Buffer.alloc(0);
|
|
5116
|
+
let segmented;
|
|
5117
|
+
const legacy = [];
|
|
5118
|
+
let legacyBytes = 0;
|
|
5119
|
+
const append = (value) => {
|
|
5120
|
+
const parent = stack.at(-1);
|
|
5121
|
+
if (!parent) {
|
|
5122
|
+
if (hasRoot)
|
|
5123
|
+
fail();
|
|
5124
|
+
root = value;
|
|
5125
|
+
hasRoot = true;
|
|
5126
|
+
} else if (Array.isArray(parent.value))
|
|
5127
|
+
parent.value.push(value);
|
|
5128
|
+
else if (parent.key === undefined) {
|
|
5129
|
+
if (typeof value !== "string" || Object.hasOwn(parent.value, value))
|
|
5130
|
+
fail();
|
|
5131
|
+
parent.key = value;
|
|
5132
|
+
} else {
|
|
5133
|
+
Object.defineProperty(parent.value, parent.key, { value, enumerable: true, writable: true, configurable: true });
|
|
5134
|
+
delete parent.key;
|
|
5135
|
+
}
|
|
5136
|
+
};
|
|
5137
|
+
const consume = (line) => {
|
|
5138
|
+
if (line.length > MAX_FRAME_BYTES)
|
|
5139
|
+
fail();
|
|
5140
|
+
const token = JSON.parse(line.toString("utf8"));
|
|
5141
|
+
if (!Array.isArray(token))
|
|
5142
|
+
fail();
|
|
5143
|
+
const frame = token;
|
|
5144
|
+
const [kind, value] = frame;
|
|
5145
|
+
if (kind === "text" && frame.length === 2 && typeof value === "string") {
|
|
5146
|
+
pieces.push(value);
|
|
5147
|
+
return;
|
|
5148
|
+
}
|
|
5149
|
+
if (kind === "string" && frame.length === 1) {
|
|
5150
|
+
append(pieces.join(""));
|
|
5151
|
+
pieces = [];
|
|
5152
|
+
return;
|
|
5153
|
+
}
|
|
5154
|
+
if (pieces.length)
|
|
5155
|
+
fail();
|
|
5156
|
+
if (kind === "value" && frame.length === 2 && (value === null || typeof value === "boolean" || typeof value === "number"))
|
|
5157
|
+
append(value);
|
|
5158
|
+
else if ((kind === "array" || kind === "object") && frame.length === 1) {
|
|
5159
|
+
if (stack.length >= 512)
|
|
5160
|
+
fail();
|
|
5161
|
+
const value2 = kind === "array" ? [] : {};
|
|
5162
|
+
append(value2);
|
|
5163
|
+
stack.push({ value: value2 });
|
|
5164
|
+
} else if (kind === "end" && frame.length === 1) {
|
|
5165
|
+
const parent = stack.pop();
|
|
5166
|
+
if (!parent || parent.key !== undefined)
|
|
5167
|
+
fail();
|
|
5168
|
+
} else
|
|
5169
|
+
fail();
|
|
5170
|
+
};
|
|
5171
|
+
for await (const chunk of readRegularFileChunksNoFollow(source, {
|
|
5172
|
+
label: "Harness state backup",
|
|
5173
|
+
requireSingleLink: true,
|
|
5174
|
+
requirePrivate: true
|
|
5175
|
+
})) {
|
|
5176
|
+
pending = Buffer.concat([pending, chunk]);
|
|
5177
|
+
if (segmented === undefined) {
|
|
5178
|
+
if (pending.length < Buffer.byteLength(MAGIC))
|
|
5179
|
+
continue;
|
|
5180
|
+
segmented = pending.subarray(0, MAGIC.length).toString() === MAGIC;
|
|
5181
|
+
if (segmented)
|
|
5182
|
+
pending = pending.subarray(MAGIC.length);
|
|
5183
|
+
}
|
|
5184
|
+
if (!segmented) {
|
|
5185
|
+
legacyBytes += pending.length;
|
|
5186
|
+
if (legacyBytes > legacyMaxBytes)
|
|
5187
|
+
throw new Error("Harness state backup exceeds the legacy size limit.");
|
|
5188
|
+
legacy.push(pending);
|
|
5189
|
+
pending = Buffer.alloc(0);
|
|
5190
|
+
continue;
|
|
5191
|
+
}
|
|
5192
|
+
let newline;
|
|
5193
|
+
while ((newline = pending.indexOf(10)) !== -1) {
|
|
5194
|
+
consume(pending.subarray(0, newline));
|
|
5195
|
+
pending = pending.subarray(newline + 1);
|
|
5196
|
+
}
|
|
5197
|
+
if (pending.length > MAX_FRAME_BYTES)
|
|
5198
|
+
fail();
|
|
5199
|
+
}
|
|
5200
|
+
if (!segmented) {
|
|
5201
|
+
if (legacyBytes + pending.length > legacyMaxBytes)
|
|
5202
|
+
throw new Error("Harness state backup exceeds the legacy size limit.");
|
|
5203
|
+
return JSON.parse(Buffer.concat([...legacy, pending]).toString("utf8"));
|
|
5204
|
+
}
|
|
5205
|
+
if (pending.length || stack.length || pieces.length || !hasRoot)
|
|
5206
|
+
fail();
|
|
5207
|
+
return root;
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5210
|
+
// src/persistence/workspace-checkpoints.ts
|
|
5211
|
+
import { createHash as createHash7, randomUUID as randomUUID2 } from "node:crypto";
|
|
5212
|
+
import path9 from "node:path";
|
|
5213
|
+
import { z as z6 } from "zod";
|
|
5017
5214
|
|
|
5018
5215
|
// src/workspace/workspace-mutation-lock.ts
|
|
5019
5216
|
import { createHash as createHash6 } from "node:crypto";
|
|
@@ -5277,6 +5474,7 @@ import { chmod as chmod4, link as link2, lstat as lstat10, open as open7, realpa
|
|
|
5277
5474
|
import path14 from "node:path";
|
|
5278
5475
|
import { z as z16 } from "zod";
|
|
5279
5476
|
import { normalizeAgentRunState } from "@zhivex-ai/agents";
|
|
5477
|
+
import { createSqliteAgentRunStore as createSqliteAgentRunStore2 } from "@zhivex-ai/agents/ops";
|
|
5280
5478
|
|
|
5281
5479
|
// src/runtime/runtime-policy.ts
|
|
5282
5480
|
import { createBudgetGuard, createProductionSafetyPolicy } from "@zhivex-ai/agents";
|
|
@@ -10344,7 +10542,7 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
10344
10542
|
database.exec("PRAGMA foreign_keys = ON");
|
|
10345
10543
|
await chmod3(databasePath, 384);
|
|
10346
10544
|
const databaseLike = sqliteAdapter(database);
|
|
10347
|
-
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
10545
|
+
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
10348
10546
|
const memory = createSqliteAgentMemoryStore({ db: databaseLike, scope: config.scope });
|
|
10349
10547
|
const migration = shouldMigrateLegacyRuns(config, options.migrateLegacyFileStore) ? await migrateLegacyFileRuns(config.stateDirectory, store, config) : emptyMigration();
|
|
10350
10548
|
return {
|
|
@@ -10496,7 +10694,12 @@ var canonicalJson2 = (value) => {
|
|
|
10496
10694
|
return `[${value.map(canonicalJson2).join(",")}]`;
|
|
10497
10695
|
return `{${Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson2(entry)}`).join(",")}}`;
|
|
10498
10696
|
};
|
|
10499
|
-
var checksumPayload = (payload) =>
|
|
10697
|
+
var checksumPayload = (payload) => {
|
|
10698
|
+
const hash2 = createHash13("sha256");
|
|
10699
|
+
for (const chunk of canonicalChunks(payload))
|
|
10700
|
+
hash2.update(chunk);
|
|
10701
|
+
return `sha256:${hash2.digest("hex")}`;
|
|
10702
|
+
};
|
|
10500
10703
|
var stableKey2 = (kind, value) => createHash13("sha256").update(kind).update("\x00").update(value).digest("hex");
|
|
10501
10704
|
var scopeValue2 = (config) => `${config.scope.tenantId}\x00${config.scope.userId ?? ""}\x00${config.scope.namespace ?? ""}`;
|
|
10502
10705
|
var scopePrefix = (config) => `${encodeURIComponent(config.scope.namespace ?? "default")}:${encodeURIComponent(config.scope.tenantId)}:${encodeURIComponent(config.scope.userId ?? "*")}:`;
|
|
@@ -10528,8 +10731,8 @@ var validateBudgetLedger = (config, run, identities) => {
|
|
|
10528
10731
|
if (!/^sha256:[a-f0-9]{64}$/.test(id) || !value || typeof value !== "object" || Array.isArray(value) || value.status !== "confirmed") {
|
|
10529
10732
|
throw new HarnessStateConflictError("Budget ledger retains unresolved reservations or unknown consumption.");
|
|
10530
10733
|
}
|
|
10531
|
-
const
|
|
10532
|
-
if (!
|
|
10734
|
+
const tokens2 = value.tokens;
|
|
10735
|
+
if (!tokens2 || typeof tokens2 !== "object" || Array.isArray(tokens2) || ![tokens2.inputTokens, tokens2.outputTokens, tokens2.totalTokens].every((amount) => typeof amount === "number" && Number.isSafeInteger(amount) && amount >= 0) || tokens2.totalTokens < tokens2.inputTokens + tokens2.outputTokens) {
|
|
10533
10736
|
throw new HarnessStateConflictError("Invalid budget ledger token receipt.");
|
|
10534
10737
|
}
|
|
10535
10738
|
}
|
|
@@ -10631,8 +10834,13 @@ var readPayload = async (config, recordedWorkspace, accessLease) => {
|
|
|
10631
10834
|
if (recordedWorkspace && database.query("SELECT version FROM zhivex_cli_session_schema WHERE singleton=1").get()?.version !== HARNESS_SESSION_SCHEMA_VERSION)
|
|
10632
10835
|
throw new HarnessStateConflictError("Archived session schema is incompatible.");
|
|
10633
10836
|
const allRunRows = database.query("SELECT run_id, state_json, updated_at_ms FROM zhivex_agent_runs ORDER BY run_id").all();
|
|
10837
|
+
let historyReader;
|
|
10634
10838
|
const runRows = allRunRows.flatMap((row) => {
|
|
10635
|
-
const
|
|
10839
|
+
const record2 = parseJsonRecord(row.state_json, `Run ${row.run_id}`);
|
|
10840
|
+
const hydrated = record2.checkpointHistory ? (historyReader ??= createSqliteAgentRunStore2({ db: sqliteAdapter(database), history: "incremental" })).load(String(record2.runId), record2.scope) : record2;
|
|
10841
|
+
if (hydrated instanceof Promise || !hydrated)
|
|
10842
|
+
throw new HarnessStateConflictError("Cannot hydrate state backup history.");
|
|
10843
|
+
const state = validatedRunState(hydrated, `Run ${row.run_id}`);
|
|
10636
10844
|
return stateMatchesScope(config, state) ? [{ row, state }] : [];
|
|
10637
10845
|
});
|
|
10638
10846
|
const runs = runRows.map(({ row, state }) => {
|
|
@@ -10792,7 +11000,19 @@ var writePrivateBackup = async (target, contents) => {
|
|
|
10792
11000
|
try {
|
|
10793
11001
|
handle = await open7(staged, fsConstants3.O_WRONLY | fsConstants3.O_CREAT | fsConstants3.O_EXCL | fsConstants3.O_NOFOLLOW, 384);
|
|
10794
11002
|
stagedExists = true;
|
|
10795
|
-
|
|
11003
|
+
let pending = [];
|
|
11004
|
+
let pendingBytes = 0;
|
|
11005
|
+
for (const chunk of contents) {
|
|
11006
|
+
pending.push(chunk);
|
|
11007
|
+
pendingBytes += Buffer.byteLength(chunk);
|
|
11008
|
+
if (pendingBytes >= 64 * 1024) {
|
|
11009
|
+
await handle.writeFile(pending.join(""), "utf8");
|
|
11010
|
+
pending = [];
|
|
11011
|
+
pendingBytes = 0;
|
|
11012
|
+
}
|
|
11013
|
+
}
|
|
11014
|
+
if (pending.length)
|
|
11015
|
+
await handle.writeFile(pending.join(""), "utf8");
|
|
10796
11016
|
await handle.sync();
|
|
10797
11017
|
await chmod4(staged, 384);
|
|
10798
11018
|
await handle.close();
|
|
@@ -10819,12 +11039,13 @@ var writePrivateBackup = async (target, contents) => {
|
|
|
10819
11039
|
};
|
|
10820
11040
|
var exportHarnessStateBackup = async (config, target) => {
|
|
10821
11041
|
const bundle = await createHarnessStateBackup(config);
|
|
10822
|
-
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
11042
|
+
let bytes = 0;
|
|
11043
|
+
for (const chunk of canonicalChunks(bundle)) {
|
|
11044
|
+
bytes += Buffer.byteLength(chunk);
|
|
11045
|
+
if (bytes > HARNESS_STATE_BACKUP_MAX_BYTES)
|
|
11046
|
+
break;
|
|
10826
11047
|
}
|
|
10827
|
-
await writePrivateBackup(target,
|
|
11048
|
+
await writePrivateBackup(target, bytes > HARNESS_STATE_BACKUP_MAX_BYTES ? segmentedChunks(bundle) : canonicalChunks(bundle));
|
|
10828
11049
|
return {
|
|
10829
11050
|
schemaVersion: HARNESS_STATE_BACKUP_SCHEMA_VERSION,
|
|
10830
11051
|
kind: "state-export",
|
|
@@ -10834,19 +11055,14 @@ var exportHarnessStateBackup = async (config, target) => {
|
|
|
10834
11055
|
};
|
|
10835
11056
|
};
|
|
10836
11057
|
var readHarnessStateBackup = async (source) => {
|
|
10837
|
-
const { contents, stat: stat4 } = await readRegularFileNoFollow(path14.resolve(source), {
|
|
10838
|
-
label: "Harness state backup",
|
|
10839
|
-
maxBytes: HARNESS_STATE_BACKUP_MAX_BYTES,
|
|
10840
|
-
requireSingleLink: true
|
|
10841
|
-
});
|
|
10842
|
-
if ((stat4.mode & 63) !== 0) {
|
|
10843
|
-
throw new HarnessWorkspaceError("Harness state backup permissions must not grant group or other access.");
|
|
10844
|
-
}
|
|
10845
11058
|
let input;
|
|
10846
11059
|
try {
|
|
10847
|
-
input =
|
|
11060
|
+
input = await readBackupTransport(path14.resolve(source), HARNESS_STATE_BACKUP_MAX_BYTES);
|
|
10848
11061
|
} catch (error) {
|
|
10849
|
-
|
|
11062
|
+
if (error instanceof SyntaxError) {
|
|
11063
|
+
throw new HarnessStateConflictError("Harness state backup is not valid JSON.", { cause: error });
|
|
11064
|
+
}
|
|
11065
|
+
throw error;
|
|
10850
11066
|
}
|
|
10851
11067
|
return validateStateBackupBundle(input);
|
|
10852
11068
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type AgentRunState, type AgentStatus } from "@zhivex-ai/agents";
|
|
2
|
-
import type { AgentMemoryStore, AgentRunStore } from "@zhivex-ai/agents/ops";
|
|
2
|
+
import type { AgentMemoryStore, AgentRunStore, SqliteAgentRunStoreOptions } from "@zhivex-ai/agents/ops";
|
|
3
3
|
import { type HarnessConfig, type HarnessStoreBackend } from "../runtime/config.js";
|
|
4
|
+
import { SqliteDatabase } from "./sqlite-database.js";
|
|
5
|
+
type SqliteDatabaseLike = SqliteAgentRunStoreOptions["db"];
|
|
4
6
|
export declare const HARNESS_SQLITE_FILE = "operations.sqlite";
|
|
5
7
|
export declare const HARNESS_OPERATIONS_SCHEMA_VERSION: 1;
|
|
6
8
|
export declare const TERMINAL_RUN_STATUSES: readonly ["completed", "failed", "cancelled", "timed_out"];
|
|
@@ -313,7 +315,9 @@ export declare const cleanupHarnessRuns: (store: AgentRunStore, config: HarnessC
|
|
|
313
315
|
statuses: AgentStatus[];
|
|
314
316
|
deleted: number;
|
|
315
317
|
}>;
|
|
318
|
+
export declare const sqliteAdapter: (database: SqliteDatabase) => SqliteDatabaseLike;
|
|
316
319
|
export declare const migrateLegacyFileRuns: (directory: string, target: AgentRunStore, config: HarnessConfig) => Promise<HarnessMigrationResult>;
|
|
317
320
|
export declare const openHarnessPersistence: (config: HarnessConfig, options?: {
|
|
318
321
|
migrateLegacyFileStore?: boolean;
|
|
319
322
|
}) => Promise<HarnessPersistence>;
|
|
323
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Canonical JSON without allocating a serialized copy of the entire bundle. */
|
|
2
|
+
export declare function canonicalChunks(value: unknown): Generator<string>;
|
|
3
|
+
export declare function segmentedChunks(value: unknown): Generator<string>;
|
|
4
|
+
export declare function readBackupTransport(source: string, legacyMaxBytes: number): Promise<unknown>;
|
package/dist/service-cli.js
CHANGED
|
@@ -4579,6 +4579,31 @@ var readRegularFileNoFollow = async (target, options) => {
|
|
|
4579
4579
|
await handle.close();
|
|
4580
4580
|
}
|
|
4581
4581
|
};
|
|
4582
|
+
async function* readRegularFileChunksNoFollow(target, options) {
|
|
4583
|
+
const handle = await openRegularFileNoFollow(target, options.label);
|
|
4584
|
+
try {
|
|
4585
|
+
const before = await handle.stat();
|
|
4586
|
+
if (!before.isFile() || options.requireSingleLink && before.nlink !== 1)
|
|
4587
|
+
throw invalidFileError(options.label);
|
|
4588
|
+
if (options.requirePrivate && (before.mode & 63) !== 0)
|
|
4589
|
+
throw new Error(`${options.label} permissions must not grant group or other access.`);
|
|
4590
|
+
let offset = 0;
|
|
4591
|
+
while (offset < before.size) {
|
|
4592
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, before.size - offset));
|
|
4593
|
+
const { bytesRead: bytesRead2 } = await handle.read(buffer, 0, buffer.length, offset);
|
|
4594
|
+
if (!bytesRead2)
|
|
4595
|
+
throw new FileChangedWhileReadingError(options.label);
|
|
4596
|
+
offset += bytesRead2;
|
|
4597
|
+
yield buffer.subarray(0, bytesRead2);
|
|
4598
|
+
}
|
|
4599
|
+
const probe = Buffer.allocUnsafe(1);
|
|
4600
|
+
const { bytesRead } = await handle.read(probe, 0, 1, offset);
|
|
4601
|
+
if (bytesRead || !sameOpenFile(before, await handle.stat()))
|
|
4602
|
+
throw new FileChangedWhileReadingError(options.label);
|
|
4603
|
+
} finally {
|
|
4604
|
+
await handle.close();
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4582
4607
|
|
|
4583
4608
|
// src/workspace/workspace-mutation-lock.ts
|
|
4584
4609
|
init_errors();
|
|
@@ -10587,7 +10612,7 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
10587
10612
|
database.exec("PRAGMA foreign_keys = ON");
|
|
10588
10613
|
await chmod3(databasePath, 384);
|
|
10589
10614
|
const databaseLike = sqliteAdapter(database);
|
|
10590
|
-
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
10615
|
+
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
10591
10616
|
const memory = createSqliteAgentMemoryStore({ db: databaseLike, scope: config.scope });
|
|
10592
10617
|
const migration = shouldMigrateLegacyRuns(config, options.migrateLegacyFileStore) ? await migrateLegacyFileRuns(config.stateDirectory, store, config) : emptyMigration();
|
|
10593
10618
|
return {
|
|
@@ -8163,7 +8163,7 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
8163
8163
|
database.exec("PRAGMA foreign_keys = ON");
|
|
8164
8164
|
await chmod3(databasePath, 384);
|
|
8165
8165
|
const databaseLike = sqliteAdapter(database);
|
|
8166
|
-
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
8166
|
+
const store = withBudgetLedgerStore(createSqliteAgentRunStore({ db: databaseLike, scope: config.scope, history: "incremental" }), createSqliteAgentRunStore({ db: databaseLike, scope: budgetScopeFor(config) }), config);
|
|
8167
8167
|
const memory = createSqliteAgentMemoryStore({ db: databaseLike, scope: config.scope });
|
|
8168
8168
|
const migration = shouldMigrateLegacyRuns(config, options.migrateLegacyFileStore) ? await migrateLegacyFileRuns(config.stateDirectory, store, config) : emptyMigration();
|
|
8169
8169
|
return {
|
|
@@ -8180,6 +8180,45 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
8180
8180
|
}
|
|
8181
8181
|
};
|
|
8182
8182
|
|
|
8183
|
+
// src/persistence/state-backup-stream.ts
|
|
8184
|
+
var STRING_CHARS = 8192;
|
|
8185
|
+
var MAX_FRAME_BYTES = 64 * 1024;
|
|
8186
|
+
function* canonicalChunks(value) {
|
|
8187
|
+
if (typeof value === "string") {
|
|
8188
|
+
yield '"';
|
|
8189
|
+
for (let i = 0;i < value.length; ) {
|
|
8190
|
+
let end = Math.min(i + STRING_CHARS, value.length);
|
|
8191
|
+
if (end < value.length && /[\uD800-\uDBFF]/.test(value[end - 1]))
|
|
8192
|
+
end--;
|
|
8193
|
+
yield JSON.stringify(value.slice(i, end)).slice(1, -1);
|
|
8194
|
+
i = end;
|
|
8195
|
+
}
|
|
8196
|
+
yield '"';
|
|
8197
|
+
} else if (value === null || typeof value !== "object") {
|
|
8198
|
+
yield JSON.stringify(value);
|
|
8199
|
+
} else if (Array.isArray(value)) {
|
|
8200
|
+
yield "[";
|
|
8201
|
+
for (let i = 0;i < value.length; i++) {
|
|
8202
|
+
if (i)
|
|
8203
|
+
yield ",";
|
|
8204
|
+
yield* canonicalChunks(value[i]);
|
|
8205
|
+
}
|
|
8206
|
+
yield "]";
|
|
8207
|
+
} else {
|
|
8208
|
+
yield "{";
|
|
8209
|
+
let first = true;
|
|
8210
|
+
for (const key of Object.keys(value).sort()) {
|
|
8211
|
+
if (!first)
|
|
8212
|
+
yield ",";
|
|
8213
|
+
first = false;
|
|
8214
|
+
yield* canonicalChunks(key);
|
|
8215
|
+
yield ":";
|
|
8216
|
+
yield* canonicalChunks(value[key]);
|
|
8217
|
+
}
|
|
8218
|
+
yield "}";
|
|
8219
|
+
}
|
|
8220
|
+
}
|
|
8221
|
+
|
|
8183
8222
|
// src/persistence/workspace-checkpoints.ts
|
|
8184
8223
|
import { createHash as createHash11, randomUUID as randomUUID6 } from "node:crypto";
|
|
8185
8224
|
import { z as z13 } from "zod";
|
|
@@ -8210,6 +8249,7 @@ import { chmod as chmod4, link as link2, lstat as lstat10, open as open7, realpa
|
|
|
8210
8249
|
import path13 from "node:path";
|
|
8211
8250
|
import { z as z14 } from "zod";
|
|
8212
8251
|
import { normalizeAgentRunState } from "@zhivex-ai/agents";
|
|
8252
|
+
import { createSqliteAgentRunStore as createSqliteAgentRunStore2 } from "@zhivex-ai/agents/ops";
|
|
8213
8253
|
var HARNESS_STATE_BACKUP_SCHEMA_VERSION = 1;
|
|
8214
8254
|
var HARNESS_STATE_BACKUP_MAX_BYTES = 64 * 1024 * 1024;
|
|
8215
8255
|
var terminalRunStatuses = new Set(["completed", "failed", "cancelled", "timed_out"]);
|
|
@@ -8336,14 +8376,12 @@ var stateBackupPayloadSchema = z14.object({
|
|
|
8336
8376
|
}).strict()
|
|
8337
8377
|
}).strict();
|
|
8338
8378
|
var stateBackupBundleSchema = stateBackupPayloadSchema.extend({ checksum: digestSchema2 }).strict();
|
|
8339
|
-
var
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
return `{${Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
8379
|
+
var checksumPayload = (payload) => {
|
|
8380
|
+
const hash2 = createHash12("sha256");
|
|
8381
|
+
for (const chunk of canonicalChunks(payload))
|
|
8382
|
+
hash2.update(chunk);
|
|
8383
|
+
return `sha256:${hash2.digest("hex")}`;
|
|
8345
8384
|
};
|
|
8346
|
-
var checksumPayload = (payload) => `sha256:${createHash12("sha256").update(canonicalJson(payload)).digest("hex")}`;
|
|
8347
8385
|
var stableKey2 = (kind, value) => createHash12("sha256").update(kind).update("\x00").update(value).digest("hex");
|
|
8348
8386
|
var scopeValue2 = (config) => `${config.scope.tenantId}\x00${config.scope.userId ?? ""}\x00${config.scope.namespace ?? ""}`;
|
|
8349
8387
|
var scopePrefix = (config) => `${encodeURIComponent(config.scope.namespace ?? "default")}:${encodeURIComponent(config.scope.tenantId)}:${encodeURIComponent(config.scope.userId ?? "*")}:`;
|
|
@@ -8478,8 +8516,13 @@ var readPayload = async (config, recordedWorkspace, accessLease) => {
|
|
|
8478
8516
|
if (recordedWorkspace && database.query("SELECT version FROM zhivex_cli_session_schema WHERE singleton=1").get()?.version !== HARNESS_SESSION_SCHEMA_VERSION)
|
|
8479
8517
|
throw new HarnessStateConflictError("Archived session schema is incompatible.");
|
|
8480
8518
|
const allRunRows = database.query("SELECT run_id, state_json, updated_at_ms FROM zhivex_agent_runs ORDER BY run_id").all();
|
|
8519
|
+
let historyReader;
|
|
8481
8520
|
const runRows = allRunRows.flatMap((row) => {
|
|
8482
|
-
const
|
|
8521
|
+
const record2 = parseJsonRecord(row.state_json, `Run ${row.run_id}`);
|
|
8522
|
+
const hydrated = record2.checkpointHistory ? (historyReader ??= createSqliteAgentRunStore2({ db: sqliteAdapter(database), history: "incremental" })).load(String(record2.runId), record2.scope) : record2;
|
|
8523
|
+
if (hydrated instanceof Promise || !hydrated)
|
|
8524
|
+
throw new HarnessStateConflictError("Cannot hydrate state backup history.");
|
|
8525
|
+
const state = validatedRunState(hydrated, `Run ${row.run_id}`);
|
|
8483
8526
|
return stateMatchesScope(config, state) ? [{ row, state }] : [];
|
|
8484
8527
|
});
|
|
8485
8528
|
const runs = runRows.map(({ row, state }) => {
|
|
@@ -10030,11 +10073,11 @@ var createEmptyHarnessContextBundle = () => {
|
|
|
10030
10073
|
schemaVersion: HARNESS_CONTEXT_BUNDLE_SCHEMA_VERSION,
|
|
10031
10074
|
scope: "project",
|
|
10032
10075
|
...identity,
|
|
10033
|
-
fingerprint: digestBytes2(
|
|
10076
|
+
fingerprint: digestBytes2(canonicalJson(harnessContextFingerprintInput(identity)))
|
|
10034
10077
|
};
|
|
10035
10078
|
};
|
|
10036
10079
|
var digestBytes2 = (value) => `sha256:${createHash16("sha256").update(value).digest("hex")}`;
|
|
10037
|
-
var
|
|
10080
|
+
var canonicalJson = (value) => JSON.stringify(value);
|
|
10038
10081
|
var validateRelativePath = (value) => {
|
|
10039
10082
|
const parsed = workspaceFilePathSchema.safeParse(value);
|
|
10040
10083
|
if (!parsed.success) {
|
|
@@ -10276,7 +10319,7 @@ var loadHarnessProjectContext = async (workspace, options = {}) => {
|
|
|
10276
10319
|
sources,
|
|
10277
10320
|
skills
|
|
10278
10321
|
};
|
|
10279
|
-
const fingerprint = digestBytes2(
|
|
10322
|
+
const fingerprint = digestBytes2(canonicalJson(harnessContextFingerprintInput(identity)));
|
|
10280
10323
|
return {
|
|
10281
10324
|
schemaVersion: HARNESS_CONTEXT_BUNDLE_SCHEMA_VERSION,
|
|
10282
10325
|
scope: "project",
|
|
@@ -12649,16 +12692,16 @@ ${contextInstructions}` : ""}${enabledDelegations}`,
|
|
|
12649
12692
|
}
|
|
12650
12693
|
};
|
|
12651
12694
|
};
|
|
12652
|
-
var
|
|
12695
|
+
var canonicalJson2 = (value) => {
|
|
12653
12696
|
if (value === null || typeof value !== "object")
|
|
12654
12697
|
return JSON.stringify(value);
|
|
12655
12698
|
if (Array.isArray(value))
|
|
12656
|
-
return `[${value.map(
|
|
12699
|
+
return `[${value.map(canonicalJson2).join(",")}]`;
|
|
12657
12700
|
const record4 = value;
|
|
12658
|
-
return `{${Object.keys(record4).sort().map((key) => `${JSON.stringify(key)}:${
|
|
12701
|
+
return `{${Object.keys(record4).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson2(record4[key])}`).join(",")}}`;
|
|
12659
12702
|
};
|
|
12660
12703
|
var localApprovalResolutionPayload = (inputDigest, approve, reason) => JSON.stringify({ inputDigest, approve, reason: reason ?? null });
|
|
12661
|
-
var terminalToolCallId = (runId, step, providerToolCallId, toolName2, input) => `tool_${createHash22("sha256").update(`${runId}\x00${step}\x00${providerToolCallId}\x00${toolName2}\x00${
|
|
12704
|
+
var terminalToolCallId = (runId, step, providerToolCallId, toolName2, input) => `tool_${createHash22("sha256").update(`${runId}\x00${step}\x00${providerToolCallId}\x00${toolName2}\x00${canonicalJson2(input)}`).digest("hex")}`;
|
|
12662
12705
|
var recoverableTerminalStaleDigest = (toolName2, message) => toolName2 === "verify_and_apply_reviewed_edits" && /^Stale patch rejected for .+\.$/.test(message);
|
|
12663
12706
|
var patchIdMismatch = (error) => {
|
|
12664
12707
|
for (let depth = 0;depth < 8 && error instanceof Error; depth++, error = error.cause) {
|
|
@@ -12736,7 +12779,7 @@ var executeTerminalReceiptToolOwned = async (harness, waiting, approval, respons
|
|
|
12736
12779
|
definition.approvalVersion,
|
|
12737
12780
|
`environment:${session.binding.fingerprint}`
|
|
12738
12781
|
].filter(Boolean).join("|") || "1";
|
|
12739
|
-
const inputDigest = createHash22("sha256").update(
|
|
12782
|
+
const inputDigest = createHash22("sha256").update(canonicalJson2({
|
|
12740
12783
|
runId: waiting.state.runId,
|
|
12741
12784
|
step: approval.step,
|
|
12742
12785
|
toolCallId: approval.toolCallId,
|
|
@@ -12744,7 +12787,7 @@ var executeTerminalReceiptToolOwned = async (harness, waiting, approval, respons
|
|
|
12744
12787
|
input: serializedInput,
|
|
12745
12788
|
toolVersion
|
|
12746
12789
|
})).digest("hex");
|
|
12747
|
-
if (approval.id !== `approval_${inputDigest}` || approval.inputDigest !== inputDigest || approval.toolVersion !== toolVersion || approval.arguments !==
|
|
12790
|
+
if (approval.id !== `approval_${inputDigest}` || approval.inputDigest !== inputDigest || approval.toolVersion !== toolVersion || approval.arguments !== canonicalJson2(serializedInput)) {
|
|
12748
12791
|
await session.release?.({ status: "failed", error: { message: "Approval binding mismatch." } });
|
|
12749
12792
|
throw new Error("The terminal tool approval is stale or does not match the current runtime binding.");
|
|
12750
12793
|
}
|
|
@@ -12941,7 +12984,7 @@ var executeTerminalReceiptToolOwned = async (harness, waiting, approval, respons
|
|
|
12941
12984
|
finishedAt: now,
|
|
12942
12985
|
toolResults: [...step.toolResults, toolResult]
|
|
12943
12986
|
} : step);
|
|
12944
|
-
const outputText =
|
|
12987
|
+
const outputText = canonicalJson2(output);
|
|
12945
12988
|
const previousRevision = waiting.state.revision ?? 0;
|
|
12946
12989
|
const state = {
|
|
12947
12990
|
...waiting.state,
|
|
@@ -27,3 +27,9 @@ export declare const statRegularFileNoFollow: (target: string, options: Pick<Rea
|
|
|
27
27
|
* time-of-check/time-of-use windows.
|
|
28
28
|
*/
|
|
29
29
|
export declare const readRegularFileNoFollow: (target: string, options: ReadRegularFileNoFollowOptions) => Promise<StableRegularFile>;
|
|
30
|
+
/** Descriptor-bound bounded reads; identity is revalidated before successful EOF. */
|
|
31
|
+
export declare function readRegularFileChunksNoFollow(target: string, options: {
|
|
32
|
+
label: string;
|
|
33
|
+
requireSingleLink?: boolean;
|
|
34
|
+
requirePrivate?: boolean;
|
|
35
|
+
}): AsyncGenerator<Buffer>;
|
package/dist/zhx.js
CHANGED
|
@@ -18,6 +18,6 @@ if (argv.length === 1 && ["--version", "-v", "version"].includes(argv[0])) {
|
|
|
18
18
|
process.stdout.write(`${shortCliHelp(HARNESS_VERSION)}
|
|
19
19
|
`);
|
|
20
20
|
} else {
|
|
21
|
-
const { runCli } = await import("./cli-
|
|
21
|
+
const { runCli } = await import("./cli-3jmy4h5h.js");
|
|
22
22
|
await runCli(argv);
|
|
23
23
|
}
|
|
@@ -64,6 +64,14 @@ zhx sessions archive <sessionId>
|
|
|
64
64
|
|
|
65
65
|
The store uses SQLite WAL transactions, optimistic revisions, workspace/scope hashes, owner-only permissions, soft deletion/retention, and bounded session/run/metadata/index sizes. It rejects a new run while the latest session run is active or waiting for approval. Fork and archive operations also require a terminal branch point.
|
|
66
66
|
|
|
67
|
+
SQLite runs store steps, tool results and compaction records separately from the active checkpoint. Large repeated text is retained in per-run content-addressed artifacts. Checkpoint and history changes commit in the same transaction; unchanged historical records are not rewritten. The 4 MiB state limit applies to the active checkpoint, including current messages, approvals, metadata and counters. Conversation compaction still controls model input size independently.
|
|
68
|
+
|
|
69
|
+
Loading a run reconstructs its complete state for compatibility with approvals, recovery and verification. Programmatic history inspection can use `store.loadHistory(runId, { field: "toolResults", offset: 0, limit: 50 }, scope)` to read a page. Full hydrated states and total disk usage can grow with the run; run retention remains necessary. File and in-memory stores retain the full-state size limit.
|
|
70
|
+
|
|
71
|
+
Logical backups contain hydrated history rather than database references, so restoration does not require the original artifact tables. Exports up to 64 MiB retain the legacy JSON format; larger exports use the versioned `ZHIVEX-STATE-SEGMENTS-1` transport with bounded frames, including split strings. RC13 reads both formats and verifies the same logical checksum before import. Older versions cannot read segmented exports. File I/O and checksum serialization are incremental; the public backup/import APIs still hydrate the complete logical bundle in memory. Segmented files have no aggregate 64 MiB ceiling. A state-limit failure records a bounded terminal diagnostic on the last durable checkpoint; the unsaved payload is not claimed as preserved. Use `/status` to inspect the failed run before `/continue`. Recovery retains tool journal receipts and never re-executes an operation itself.
|
|
72
|
+
|
|
73
|
+
Incremental SQLite history uses the published Core 1.26.0 and Agents 1.10.1 packages. No local dependency patch is required. Install this checkout with `bun install --frozen-lockfile`.
|
|
74
|
+
|
|
67
75
|
## Provider handoff safety
|
|
68
76
|
|
|
69
77
|
Every console turn preallocates a new run ID and binds it permanently to one provider/model. Provider or model changes take effect only on the next run and are blocked while an approval is pending. Before transferring context to another provider/model, the console creates a bounded deterministic redacted summary: text is truncated and common credentials are removed, while tool inputs, outputs, and provider payloads are replaced by tool/type names.
|
package/docs/EXTENSIBILITY.md
CHANGED
|
@@ -8,7 +8,7 @@ Built-in registrations are Meta, Qwen, OpenAI, and Gemini. Each registration own
|
|
|
8
8
|
|
|
9
9
|
Library callers can use `DEFAULT_PROVIDER_REGISTRY`, `BUILTIN_PROVIDER_REGISTRATIONS`, `createProviderRegistry`, or `DEFAULT_PROVIDER_REGISTRY.extend(...)`, then pass the registry to `resolveHarnessConfig`, `createProviderModel`, provider diagnostics, or `createHarness({ providerRegistry })`. Registration IDs, environment-variable names, defaults, diagnostics, and factories are validated before they become selectable.
|
|
10
10
|
|
|
11
|
-
The coordinated stable SDK batch pins `@zhivex-ai/agents@1.10.
|
|
11
|
+
The coordinated stable SDK batch pins `@zhivex-ai/agents@1.10.1`, `@zhivex-ai/core@1.26.0`, `@zhivex-ai/meta@0.2.8`, `@zhivex-ai/qwen@0.16.2`, `@zhivex-ai/openai@0.13.6`, and `@zhivex-ai/gemini@0.12.3`. Qwen 0.16.2 types malformed Responses tool arguments and malformed SSE events; Core 1.26.0 satisfies its published dependency range. These exact versions are pinned in the lockfile. Stable SDK publication does not replace Harness release-bound certification. Qwen `0.11.x` normalizes missing, placeholder, and continuation tool-call identifiers inside the adapter; Harness retains its deterministic provider-boundary normalizer as defense in depth until the exact candidate passes the complete release-bound representative matrix. A Core override keeps one runtime contract identity across every adapter, while Core `1.13.0` corrects compaction usage accounting and interaction-group retention. OpenAI `0.11.2` preserves synthetic assistant history in Responses, so the harness no longer requires its compaction transport or accounting workarounds.
|
|
12
12
|
|
|
13
13
|
OpenAI is based on the GPT-5.6 family: `gpt-5.6-luna` is the default, with `gpt-5.6-terra` and `gpt-5.6-sol` available through `--model`. Availability remains organization-dependent while the upstream family is in limited preview, and the adapter uses the Responses API by default.
|
|
14
14
|
|
package/docs/FIRST_USE.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# First use
|
|
2
2
|
|
|
3
|
-
This guide targets the unpublished `1.2.0-rc.
|
|
3
|
+
This guide targets the unpublished `1.2.0-rc.13` candidate. Installation commands become available after protected publication to npm `next`; stable users can install `@zhivex-ai/harness@latest`.
|
|
4
4
|
|
|
5
5
|
## Install and start
|
|
6
6
|
|
|
7
7
|
Install Node.js 22.13.0 or newer and Git. Then:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npm install -g @zhivex-ai/harness@1.2.0-rc.
|
|
10
|
+
npm install -g @zhivex-ai/harness@1.2.0-rc.13
|
|
11
11
|
cd /path/to/your/project
|
|
12
12
|
zhx
|
|
13
13
|
```
|
|
@@ -82,16 +82,16 @@ For command execution, configure [OCI isolation](EXECUTION_ENVIRONMENTS.md).
|
|
|
82
82
|
With Bun 1.4.0 or newer and a supported Node runtime:
|
|
83
83
|
|
|
84
84
|
```sh
|
|
85
|
-
bun add --global @zhivex-ai/harness@1.2.0-rc.
|
|
85
|
+
bun add --global @zhivex-ai/harness@1.2.0-rc.13
|
|
86
86
|
zhx
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
For a disposable version/help/environment check without a global installation:
|
|
90
90
|
|
|
91
91
|
```sh
|
|
92
|
-
bunx @zhivex-ai/harness@1.2.0-rc.
|
|
93
|
-
bunx @zhivex-ai/harness@1.2.0-rc.
|
|
94
|
-
bunx @zhivex-ai/harness@1.2.0-rc.
|
|
92
|
+
bunx @zhivex-ai/harness@1.2.0-rc.13 --version
|
|
93
|
+
bunx @zhivex-ai/harness@1.2.0-rc.13 --help
|
|
94
|
+
bunx @zhivex-ai/harness@1.2.0-rc.13 doctor
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
For source development, use the [contributor guide](https://github.com/Zhivex/zhivex-harness/blob/main/CONTRIBUTING.md).
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"v1.2.0-rc.9",
|
|
44
44
|
"v1.2.0-rc.10",
|
|
45
45
|
"v1.2.0-rc.11",
|
|
46
|
-
"v1.2.0-rc.12"
|
|
46
|
+
"v1.2.0-rc.12",
|
|
47
|
+
"v1.2.0-rc.13"
|
|
47
48
|
],
|
|
48
49
|
"expectedModels": [
|
|
49
50
|
{
|
|
@@ -381,6 +382,14 @@
|
|
|
381
382
|
"qwen": "qwen3.8-flash",
|
|
382
383
|
"openai": "gpt-6-luna"
|
|
383
384
|
}
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"releaseTag": "v1.2.0-rc.13",
|
|
388
|
+
"models": {
|
|
389
|
+
"meta": "muse-spark-1.3-contributor",
|
|
390
|
+
"qwen": "qwen3.8-flash",
|
|
391
|
+
"openai": "gpt-6-luna"
|
|
392
|
+
}
|
|
384
393
|
}
|
|
385
394
|
],
|
|
386
395
|
"expectedCases": [
|
package/examples/README.md
CHANGED
|
@@ -8,13 +8,13 @@ Requirements: Bun 1.4.0+, Node.js 22.13.0+, and Git on macOS or Linux. This inst
|
|
|
8
8
|
harness_demo_dir=$(mktemp -d)
|
|
9
9
|
cd "$harness_demo_dir"
|
|
10
10
|
printf '{"name":"harness-first-run","private":true}\n' > package.json
|
|
11
|
-
bun add --exact --ignore-scripts @zhivex-ai/harness@1.2.0-rc.
|
|
11
|
+
bun add --exact --ignore-scripts @zhivex-ai/harness@1.2.0-rc.13
|
|
12
12
|
./node_modules/.bin/zhx --version
|
|
13
13
|
./node_modules/.bin/zhx --help
|
|
14
14
|
./node_modules/.bin/zhx doctor --provider openai --json
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Version must print `1.2.0-rc.
|
|
17
|
+
Version must print `1.2.0-rc.13`; help lists commands. Doctor does not contact a provider. Without `OPENAI_API_KEY`, it reports the missing credential and exits with code `3`; inspect the JSON checks before proceeding. A directory without Git initialization can also produce a diagnostic warning. No paid model call is required for these checks. Both `zhx` and `zhivex-harness` are installed aliases; Bun manages installation, and their shebang runs Node.
|
|
18
18
|
|
|
19
19
|
To continue in your actual project, use the [global installation and credential setup](../README.md#installation), then `zhx init --profile daily --provider openai` and `zhx doctor --profile daily`. Real tasks require the selected provider credential in the process environment. Profiles store provider/model choices, never credentials. See [CLI](../docs/CLI.md) and [support](../SUPPORT.md).
|
|
20
20
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhivex-ai/harness",
|
|
3
|
-
"version": "1.2.0-rc.
|
|
3
|
+
"version": "1.2.0-rc.13",
|
|
4
4
|
"description": "Governed, provider-portable runtime for coding agents with durable approvals and isolated repository execution.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -146,8 +146,8 @@
|
|
|
146
146
|
"release:prepare": "bun run scripts/prepare-release.ts"
|
|
147
147
|
},
|
|
148
148
|
"dependencies": {
|
|
149
|
-
"@zhivex-ai/agents": "1.10.
|
|
150
|
-
"@zhivex-ai/core": "1.
|
|
149
|
+
"@zhivex-ai/agents": "1.10.1",
|
|
150
|
+
"@zhivex-ai/core": "1.26.0",
|
|
151
151
|
"@zhivex-ai/gemini": "0.12.3",
|
|
152
152
|
"@zhivex-ai/meta": "0.2.8",
|
|
153
153
|
"@zhivex-ai/openai": "0.13.6",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"zod": "4.6.5"
|
|
156
156
|
},
|
|
157
157
|
"overrides": {
|
|
158
|
-
"@zhivex-ai/core": "1.
|
|
158
|
+
"@zhivex-ai/core": "1.26.0"
|
|
159
159
|
},
|
|
160
160
|
"devDependencies": {
|
|
161
161
|
"@modelcontextprotocol/server": "2.0.0",
|