harnery 0.42.0 → 0.43.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/dist/commander.d.ts +16 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +18 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +54 -4
- package/dist/commands/tunnel.d.ts +65 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +290 -101
- package/dist/commands/web-fetch.d.ts +3 -2
- package/dist/commands/web-fetch.d.ts.map +1 -1
- package/dist/commands/web-fetch.js +25 -19
- package/dist/commands/web.d.ts +7 -1
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +9 -3
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +11 -5
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +57 -13
- package/dist/core/agents/state/names.d.ts +43 -3
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +87 -3
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
- package/dist/core/hooks/cli.js +87 -18
- package/dist/core/hooks/fork-lineage.d.ts +40 -0
- package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
- package/dist/core/hooks/fork-lineage.js +62 -0
- package/dist/core/hooks/resolve/transcript.d.ts +16 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +84 -0
- package/dist/core/hooks/shell-rm.d.ts +2 -0
- package/dist/core/hooks/shell-rm.d.ts.map +1 -0
- package/dist/core/hooks/shell-rm.js +105 -0
- package/dist/core/hooks/shell-tokens.d.ts +12 -0
- package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
- package/dist/core/hooks/shell-tokens.js +97 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
- package/dist/core/hooks/shell-waiter.js +3 -93
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/storage/process-log.d.ts +12 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +10 -2
- package/dist/lib/tunnel/allowlist.d.ts +44 -0
- package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
- package/dist/lib/tunnel/allowlist.js +204 -0
- package/dist/lib/tunnel/current-address.d.ts +41 -0
- package/dist/lib/tunnel/current-address.d.ts.map +1 -0
- package/dist/lib/tunnel/current-address.js +123 -0
- package/dist/lib/tunnel/gate.js +13 -6
- package/dist/lib/tunnel/processes.d.ts +39 -0
- package/dist/lib/tunnel/processes.d.ts.map +1 -0
- package/dist/lib/tunnel/processes.js +121 -0
- package/dist/lib/tunnel/state.d.ts +28 -3
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +98 -27
- package/package.json +2 -1
- package/src/commander.ts +32 -0
- package/src/commands/agents.ts +97 -11
- package/src/commands/tunnel.ts +394 -116
- package/src/commands/web-fetch.ts +30 -19
- package/src/commands/web.ts +9 -3
- package/src/core/agents/render/prompt-context.ts +25 -8
- package/src/core/agents/render/session-context.ts +83 -16
- package/src/core/agents/state/names.ts +117 -6
- package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
- package/src/core/hooks/cli.ts +106 -17
- package/src/core/hooks/fork-lineage.ts +82 -0
- package/src/core/hooks/resolve/transcript.ts +88 -0
- package/src/core/hooks/shell-rm.ts +103 -0
- package/src/core/hooks/shell-tokens.ts +96 -0
- package/src/core/hooks/shell-waiter.ts +4 -91
- package/src/core/storage/process-log.ts +17 -2
- package/src/lib/tunnel/allowlist.ts +208 -0
- package/src/lib/tunnel/current-address.ts +158 -0
- package/src/lib/tunnel/gate.ts +16 -8
- package/src/lib/tunnel/processes.ts +149 -0
- package/src/lib/tunnel/state.ts +123 -27
|
@@ -14,9 +14,11 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
* harnery repo at the *matching version tag* into a cache dir and install the
|
|
15
15
|
* web app's deps. Subsequent runs reuse the cache.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* `web/` imports readers from `src/`, and `web/next.config.ts` resolves the
|
|
18
|
+
* root `playwright` package, so the install covers the repository root and
|
|
19
|
+
* then `web/`. Installing the `playwright` package downloads no browsers.
|
|
20
|
+
* Installing `web/` alone left `harn web up` failing with
|
|
21
|
+
* `Cannot find module 'playwright/package.json'`.
|
|
20
22
|
*/
|
|
21
23
|
const DEFAULT_REPO_URL = "https://github.com/ryanjkelly/harnery.git";
|
|
22
24
|
/** harnery's own package root: src/commands/ -> ../.. ; valid in both src/ and dist/. */
|
|
@@ -46,8 +48,8 @@ function webCacheRoot(ref) {
|
|
|
46
48
|
return path.join(base, "harnery", "web", safeRef);
|
|
47
49
|
}
|
|
48
50
|
/** bun if available (preferred), else npm. Both support `<runner> install` and `<runner> run <script>`. */
|
|
49
|
-
export function webRunner() {
|
|
50
|
-
const r =
|
|
51
|
+
export function webRunner(spawn = spawnSync) {
|
|
52
|
+
const r = spawn("bun", ["--version"], { stdio: "ignore" });
|
|
51
53
|
return r.status === 0 ? "bun" : "npm";
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
@@ -55,13 +57,14 @@ export function webRunner() {
|
|
|
55
57
|
* its path, fetching (clone + install) on first use. Returns null on failure
|
|
56
58
|
* (after emitting an actionable error).
|
|
57
59
|
*/
|
|
58
|
-
export function lazyFetchWebRoot(emit) {
|
|
60
|
+
export function lazyFetchWebRoot(emit, spawn = spawnSync) {
|
|
59
61
|
const { version, repoUrl } = readPkg();
|
|
60
62
|
const ref = resolveRef(version);
|
|
61
63
|
const cacheRoot = webCacheRoot(ref);
|
|
62
64
|
const webDir = path.join(cacheRoot, "web");
|
|
63
|
-
// node_modules in the cached web/ is the "fetched + installed" marker.
|
|
64
|
-
if (existsSync(path.join(
|
|
65
|
+
// node_modules in both the cached root and web/ is the "fetched + installed" marker.
|
|
66
|
+
if (existsSync(path.join(cacheRoot, "node_modules")) &&
|
|
67
|
+
existsSync(path.join(webDir, "node_modules"))) {
|
|
65
68
|
emit.log(`harn web · using cached dashboard (${ref})`, "info");
|
|
66
69
|
return webDir;
|
|
67
70
|
}
|
|
@@ -71,7 +74,7 @@ export function lazyFetchWebRoot(emit) {
|
|
|
71
74
|
if (existsSync(cacheRoot))
|
|
72
75
|
rmSync(cacheRoot, { recursive: true, force: true });
|
|
73
76
|
mkdirSync(path.dirname(cacheRoot), { recursive: true });
|
|
74
|
-
const clone =
|
|
77
|
+
const clone = spawn("git", ["clone", "--depth", "1", "--branch", ref, repoUrl, cacheRoot], {
|
|
75
78
|
stdio: "inherit",
|
|
76
79
|
});
|
|
77
80
|
if (clone.status !== 0) {
|
|
@@ -84,16 +87,19 @@ export function lazyFetchWebRoot(emit) {
|
|
|
84
87
|
});
|
|
85
88
|
return null;
|
|
86
89
|
}
|
|
87
|
-
const runner = webRunner();
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
const runner = webRunner(spawn);
|
|
91
|
+
for (const dir of [cacheRoot, webDir]) {
|
|
92
|
+
const where = dir === webDir ? "web/" : "the harnery root";
|
|
93
|
+
emit.log(`harn web · installing dashboard deps (${runner} install in ${where})`, "info");
|
|
94
|
+
const install = spawn(runner, ["install"], { cwd: dir, stdio: "inherit" });
|
|
95
|
+
if (install.status !== 0) {
|
|
96
|
+
emit.error({
|
|
97
|
+
code: "web_fetch_failed",
|
|
98
|
+
message: `${runner} install in ${dir} failed (status ${install.status})`,
|
|
99
|
+
hint: `Try manually: cd ${dir} && ${runner} install`,
|
|
100
|
+
});
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
97
103
|
}
|
|
98
104
|
emit.log(`harn web · dashboard ready (cached at ${webDir})`, "info");
|
|
99
105
|
return webDir;
|
package/dist/commands/web.d.ts
CHANGED
|
@@ -19,7 +19,13 @@ export declare function resolveMaxOldSpaceMb(flag?: string): number;
|
|
|
19
19
|
* wins without clobbering flags the caller set (source maps, inspector, …).
|
|
20
20
|
*/
|
|
21
21
|
export declare function nodeOptionsWithHeapCap(mb: number): string | undefined;
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Preload the dashboard's server hooks (keep-alive and request diagnostics)
|
|
24
|
+
* without replacing Next's HTTP server. NODE_OPTIONS reaches the child that
|
|
25
|
+
* `next dev` forks, but Next re-parses every `--import` into one value, so
|
|
26
|
+
* this names the single combined preload; a second, different `--import`
|
|
27
|
+
* would collapse into an unloadable path (web/server-preload.mjs).
|
|
28
|
+
*/
|
|
23
29
|
export declare function nodeOptionsWithWebDiagnostics(webDir: string, mb: number): string;
|
|
24
30
|
export declare function webPerformanceLogDestination(coordRoot: string, env?: Readonly<Record<string, string | undefined>>): string;
|
|
25
31
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/commands/web.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA8CnD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKrE;AAED
|
|
1
|
+
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/commands/web.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AA8CnD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKrE;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAKhF;AAED,wBAAgB,4BAA4B,CAC1C,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GAC9D,MAAM,CAQR;AAmDD;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAevE;AAsBD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA2N5E"}
|
package/dist/commands/web.js
CHANGED
|
@@ -71,12 +71,18 @@ export function nodeOptionsWithHeapCap(mb) {
|
|
|
71
71
|
return inherited;
|
|
72
72
|
return `${inherited} --max-old-space-size=${mb}`.trim();
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Preload the dashboard's server hooks (keep-alive and request diagnostics)
|
|
76
|
+
* without replacing Next's HTTP server. NODE_OPTIONS reaches the child that
|
|
77
|
+
* `next dev` forks, but Next re-parses every `--import` into one value, so
|
|
78
|
+
* this names the single combined preload; a second, different `--import`
|
|
79
|
+
* would collapse into an unloadable path (web/server-preload.mjs).
|
|
80
|
+
*/
|
|
75
81
|
export function nodeOptionsWithWebDiagnostics(webDir, mb) {
|
|
76
82
|
const inherited = nodeOptionsWithHeapCap(mb) ?? "";
|
|
77
|
-
if (inherited.includes("server-
|
|
83
|
+
if (inherited.includes("server-preload.mjs"))
|
|
78
84
|
return inherited;
|
|
79
|
-
const preload = pathToFileURL(path.join(webDir, "server-
|
|
85
|
+
const preload = pathToFileURL(path.join(webDir, "server-preload.mjs")).href;
|
|
80
86
|
return `${inherited} --import=${preload}`.trim();
|
|
81
87
|
}
|
|
82
88
|
export function webPerformanceLogDestination(coordRoot, env = process.env) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;
|
|
1
|
+
{"version":3,"file":"prompt-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/prompt-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAiDH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;8EAC0E;IAC1E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;kFAC8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;kFAC8E;IAC9E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;uEACmE;IACnE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;wBAIoB;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAkFnE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CA6CtF"}
|
|
@@ -43,6 +43,7 @@ import { drainMailbox, formatMailboxDelivery } from "../mailbox.js";
|
|
|
43
43
|
import { sessionNameDisplayBlock, sessionNameDisplayPending } from "../session-name-display.js";
|
|
44
44
|
import { heartbeatPath } from "../state/heartbeat-reader.js";
|
|
45
45
|
import { readLiveCoordinationRow, readLiveCoordinationRows, } from "../state/live-coordination-view.js";
|
|
46
|
+
import { forkRelationLabels } from "../state/names.js";
|
|
46
47
|
import { formatPendingCouncils, recordDeliveredMessages } from "./session-context.js";
|
|
47
48
|
/**
|
|
48
49
|
* Build the combined UserPromptSubmit additionalContext string. Returns "" when
|
|
@@ -337,7 +338,7 @@ function computePeerTableIfChanged(coordRoot, selfInstanceId, selfSessionIdFallb
|
|
|
337
338
|
if (oldHash && oldHash === newHash)
|
|
338
339
|
return "";
|
|
339
340
|
// Render peer table via the same formatter used at SessionStart.
|
|
340
|
-
const local = formatPeerTable(peers, mySessionId);
|
|
341
|
+
const local = formatPeerTable(peers, mySessionId, forkRelationLabels(coordRoot, selfInstanceId, new Set(peers.map((p) => p.instance_id ?? ""))));
|
|
341
342
|
const remoteTable = formatRemoteTable(remote);
|
|
342
343
|
const table = [local, remoteTable].filter(Boolean).join("\n\n");
|
|
343
344
|
if (!table)
|
|
@@ -458,7 +459,7 @@ function writeHashFile(path, value) {
|
|
|
458
459
|
* intentionally co-located here so the prompt-context renderer is self-contained
|
|
459
460
|
* (no cross-file imports of formatting internals). If both renderers diverge,
|
|
460
461
|
* pull the shared bits into a small util module. */
|
|
461
|
-
function formatPeerTable(peers, mySessionId) {
|
|
462
|
+
function formatPeerTable(peers, mySessionId, relations = new Map()) {
|
|
462
463
|
if (peers.length === 0)
|
|
463
464
|
return "";
|
|
464
465
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
@@ -474,9 +475,13 @@ function formatPeerTable(peers, mySessionId) {
|
|
|
474
475
|
.map((p) => {
|
|
475
476
|
const folded = fold[p.instance_id ?? ""] ?? [];
|
|
476
477
|
const display = Array.from(new Set([...(p.files_touched ?? []), ...folded])).sort();
|
|
477
|
-
|
|
478
|
+
const relation = relations.get(p.instance_id ?? "");
|
|
479
|
+
return { ...p, display_files: display, ...(relation ? { relation } : {}) };
|
|
478
480
|
});
|
|
479
|
-
|
|
481
|
+
// Related sessions sort first so a fork parent never hides in "+N more".
|
|
482
|
+
const blocking = rows
|
|
483
|
+
.filter((p) => p.session_id !== mySessionId)
|
|
484
|
+
.sort((a, b) => Number(!!b.relation) - Number(!!a.relation) || byStartedAt(a, b));
|
|
480
485
|
const group = rows.filter((p) => p.session_id === mySessionId).sort(byStartedAt);
|
|
481
486
|
const out = [];
|
|
482
487
|
const blk = renderSubtable(blocking, "Other agent groups active (their files block you):", nowSec);
|
|
@@ -508,7 +513,8 @@ function formatRow(r, nowSec) {
|
|
|
508
513
|
// Prefer a short instance_id over a bare "unknown" so an incomplete row is
|
|
509
514
|
// still identifiable.
|
|
510
515
|
const label = r.name ?? (r.instance_id ? r.instance_id.slice(0, 8) : "unknown");
|
|
511
|
-
|
|
516
|
+
const relation = r.relation ? ` [${r.relation}]` : "";
|
|
517
|
+
return ` - agent-${label}${relation}${taskPart} (${statePart}, ${ageFrom}, ${filesPart})`;
|
|
512
518
|
}
|
|
513
519
|
function formatState(r) {
|
|
514
520
|
const activity = r.activity ?? "unknown";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM1E,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAoB3F,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+BAC2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"session-context.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/render/session-context.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAyC,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM1E,OAAO,EAAuC,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAoB3F,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+BAC2B;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAiG7D;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,SAAS,gBAAgB,EAAE,GACpC,IAAI,CAUN;AA6LD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBvD;AA0DD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAiDlF;AAED;;;GAGG;AACH,eAAO,MAAM,uBAAuB,iBAAW,CAAC"}
|
|
@@ -15,7 +15,7 @@ import { appendEntry } from "../../journal/index.js";
|
|
|
15
15
|
import { readRemoteMachines } from "../../presence/index.js";
|
|
16
16
|
import { drainMailbox, formatMailboxDelivery } from "../mailbox.js";
|
|
17
17
|
import { readLiveCoordinationRows } from "../state/live-coordination-view.js";
|
|
18
|
-
import {
|
|
18
|
+
import { forkRelationLabels, readForkState } from "../state/names.js";
|
|
19
19
|
/**
|
|
20
20
|
* Build the combined SessionStart systemMessage. Returns the additionalContext
|
|
21
21
|
* string (or "" if there's nothing to say).
|
|
@@ -25,7 +25,13 @@ export function renderSessionContext(opts) {
|
|
|
25
25
|
const messages = [];
|
|
26
26
|
// 1. Self-name line + peer table (folded if peers present)
|
|
27
27
|
const peers = readActivePeers(coordRoot, instanceId);
|
|
28
|
-
const
|
|
28
|
+
const forkState = readForkState(coordRoot, instanceId);
|
|
29
|
+
const forkParent = forkState?.parent ?? null;
|
|
30
|
+
const parentRow = forkParent
|
|
31
|
+
? peers.find((peer) => peer.instance_id === forkParent.instance_id)
|
|
32
|
+
: undefined;
|
|
33
|
+
const relations = forkRelationLabels(coordRoot, instanceId, new Set(peers.map((peer) => peer.instance_id ?? "")));
|
|
34
|
+
const localTable = formatPeerTable(peers, sessionId, relations);
|
|
29
35
|
// Cross-machine presence (ADR 0016): sessions on other machines (advisory).
|
|
30
36
|
const peerTable = [localTable, formatRemoteMachines(coordRoot)].filter(Boolean).join("\n\n");
|
|
31
37
|
if (agentName) {
|
|
@@ -35,13 +41,11 @@ export function renderSessionContext(opts) {
|
|
|
35
41
|
// asserts the parent's name, claims, and task. When recorded fork lineage
|
|
36
42
|
// (.name-history forked_from) names the parent, say so specifically — the
|
|
37
43
|
// fork is about to read a transcript full of that exact name. Otherwise
|
|
38
|
-
// fall back to the generic clause, which needs no fork detection.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
? `This conversation was branched from another session (${forkParent.instance_id}); any agent name in earlier context belongs to that session, not to you.`
|
|
44
|
-
: "Any different agent name in earlier context was inherited from another session; this one is authoritative.";
|
|
44
|
+
// fall back to the generic clause, which needs no fork detection. A live
|
|
45
|
+
// parent is the dangerous case (2026-10-07: a fork read its running
|
|
46
|
+
// parent as its own earlier self and offered to close it), so the notice
|
|
47
|
+
// names what the parent is doing and forbids ending it.
|
|
48
|
+
const authority = forkAuthorityClause(forkParent, forkState?.fork === true, parentRow);
|
|
45
49
|
const selfLine = `You are agent-${agentName}${suffix}. ${authority}`;
|
|
46
50
|
messages.push(peerTable ? `${selfLine}\n\n${peerTable}` : selfLine);
|
|
47
51
|
}
|
|
@@ -151,13 +155,48 @@ function formatRemoteMachines(coordRoot) {
|
|
|
151
155
|
function readActivePeers(coordRoot, selfInstanceId) {
|
|
152
156
|
return readLiveCoordinationRows(coordRoot).filter((heartbeat) => heartbeat.instance_id && heartbeat.instance_id !== selfInstanceId);
|
|
153
157
|
}
|
|
158
|
+
const DO_NOT_END = "Do not end, release, or finalize that session unless the operator explicitly tells you to.";
|
|
159
|
+
function forkAuthorityClause(parent, fork, parentRow) {
|
|
160
|
+
if (parent?.name && parentRow) {
|
|
161
|
+
const files = parentRow.files_touched ?? [];
|
|
162
|
+
const task = parentRow.task ? ` on "${parentRow.task.slice(0, 80)}"` : "";
|
|
163
|
+
const holds = files.length > 0
|
|
164
|
+
? ` and holds ${files.length} file${files.length === 1 ? "" : "s"} (${fmtFileList(files)})`
|
|
165
|
+
: "";
|
|
166
|
+
const owned = files.length > 0
|
|
167
|
+
? parentRow.task
|
|
168
|
+
? "That name, task, and those files"
|
|
169
|
+
: "That name and those files"
|
|
170
|
+
: parentRow.task
|
|
171
|
+
? "That name and task"
|
|
172
|
+
: "That name";
|
|
173
|
+
return (`This conversation was forked from agent-${parent.name}, which is still running as a separate session${task}${holds}. ` +
|
|
174
|
+
`${owned} belong${owned === "That name" ? "s" : ""} to agent-${parent.name}, not to you. ${DO_NOT_END}`);
|
|
175
|
+
}
|
|
176
|
+
if (parent?.name) {
|
|
177
|
+
return `This conversation was branched from agent-${parent.name}'s session: earlier context showing agent-${parent.name}'s name, task, or file claims belongs to the pre-fork session, not to you.`;
|
|
178
|
+
}
|
|
179
|
+
if (parent) {
|
|
180
|
+
return `This conversation was branched from another session (${parent.instance_id}); any agent name in earlier context belongs to that session, not to you.`;
|
|
181
|
+
}
|
|
182
|
+
if (fork) {
|
|
183
|
+
return `This conversation was forked from another session, which may still be running. Any agent name, task, or file claim in earlier context belongs to that session, not to you. ${DO_NOT_END}`;
|
|
184
|
+
}
|
|
185
|
+
return "Any different agent name in earlier context was inherited from another session; this one is authoritative.";
|
|
186
|
+
}
|
|
187
|
+
function fmtFileList(files) {
|
|
188
|
+
const sorted = [...files].sort();
|
|
189
|
+
return sorted.length <= 3
|
|
190
|
+
? sorted.join(", ")
|
|
191
|
+
: `${sorted.slice(0, 3).join(", ")}, +${sorted.length - 3} more`;
|
|
192
|
+
}
|
|
154
193
|
/**
|
|
155
194
|
* Renders the peer table as
|
|
156
195
|
* two subsections: "Other agent groups active" (blocking) and "Your group"
|
|
157
196
|
* (subagents/siblings, no mutual block). Folds transient subagents' files
|
|
158
197
|
* into their parent session.
|
|
159
198
|
*/
|
|
160
|
-
function formatPeerTable(peers, mySessionId) {
|
|
199
|
+
function formatPeerTable(peers, mySessionId, relations = new Map()) {
|
|
161
200
|
if (peers.length === 0)
|
|
162
201
|
return "";
|
|
163
202
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
@@ -174,9 +213,13 @@ function formatPeerTable(peers, mySessionId) {
|
|
|
174
213
|
.map((p) => {
|
|
175
214
|
const folded = fold[p.instance_id ?? ""] ?? [];
|
|
176
215
|
const display = Array.from(new Set([...(p.files_touched ?? []), ...folded])).sort();
|
|
177
|
-
|
|
216
|
+
const relation = relations.get(p.instance_id ?? "");
|
|
217
|
+
return { ...p, display_files: display, ...(relation ? { relation } : {}) };
|
|
178
218
|
});
|
|
179
|
-
|
|
219
|
+
// Related sessions sort first so the parent never hides in "+N more".
|
|
220
|
+
const blocking = rows
|
|
221
|
+
.filter((p) => p.session_id !== mySessionId)
|
|
222
|
+
.sort((a, b) => Number(!!b.relation) - Number(!!a.relation) || byStartedAt(a, b));
|
|
180
223
|
const group = rows.filter((p) => p.session_id === mySessionId).sort(byStartedAt);
|
|
181
224
|
const sections = [];
|
|
182
225
|
const blockingSection = renderSubtable(blocking, "Other agent groups active (their files block you):", nowSec);
|
|
@@ -204,7 +247,8 @@ function formatRow(r, nowSec) {
|
|
|
204
247
|
const reason = lifecycle === "blocked" && r.task_state_reason ? `: ${r.task_state_reason.slice(0, 80)}` : "";
|
|
205
248
|
const ageFrom = fmtAge(nowSec - parseIsoSec(r.started_at));
|
|
206
249
|
const filesPart = fmtFiles(r.display_files);
|
|
207
|
-
|
|
250
|
+
const relation = r.relation ? ` [${r.relation}]` : "";
|
|
251
|
+
return ` - agent-${r.name ?? "unknown"}${relation}${taskPart} (activity=${activity}, lifecycle=${lifecycle}${reason}, ${ageFrom}, ${filesPart})`;
|
|
208
252
|
}
|
|
209
253
|
function fmtFiles(files) {
|
|
210
254
|
if (files.length === 0)
|
|
@@ -43,13 +43,20 @@ export interface NameHistoryRow {
|
|
|
43
43
|
ts: string;
|
|
44
44
|
/** Durable persona UUID. Present after `agents identity assume`. */
|
|
45
45
|
agent_id?: string;
|
|
46
|
-
/** Audit marker distinguishing an explicit role adoption from pool assignment
|
|
47
|
-
|
|
46
|
+
/** Audit marker distinguishing an explicit role adoption from pool assignment,
|
|
47
|
+
* and a lineage row appended after the name was already assigned. */
|
|
48
|
+
source?: "pool" | "identity.assume" | "fork.detected";
|
|
48
49
|
previous_name?: string;
|
|
49
50
|
/** Instance this session was forked/branched from (recorded fork lineage).
|
|
50
51
|
* Stamped only on the row that first assigns this instance, when the adapter
|
|
51
52
|
* layer detected or supplied a parent conversation. */
|
|
52
53
|
forked_from?: string;
|
|
54
|
+
/** The adapter declared or inherited-history proved this instance is a fork,
|
|
55
|
+
* even when its parent could not be resolved yet. */
|
|
56
|
+
fork?: true;
|
|
57
|
+
/** Fork detection has run against this instance's transcript; the tool-path
|
|
58
|
+
* heal skips instances carrying it so the scan runs once per instance. */
|
|
59
|
+
fork_checked?: true;
|
|
53
60
|
}
|
|
54
61
|
/**
|
|
55
62
|
* Resolve (name, kind) for an existing owner without consuming a counter
|
|
@@ -112,10 +119,43 @@ export declare function readLiveNames(coordRoot: string, opts: {
|
|
|
112
119
|
export declare function assignName(coordRoot: string, instanceId: string, kind: NameKind, opts: {
|
|
113
120
|
freshnessSecs: number;
|
|
114
121
|
forkedFrom?: string;
|
|
122
|
+
fork?: boolean;
|
|
123
|
+
forkChecked?: boolean;
|
|
115
124
|
nowMs?: number;
|
|
116
125
|
}): string;
|
|
126
|
+
export interface ForkState {
|
|
127
|
+
/** True when the instance is a recorded fork, parent known or not. */
|
|
128
|
+
fork: boolean;
|
|
129
|
+
/** Detection already ran for this instance. */
|
|
130
|
+
checked: boolean;
|
|
131
|
+
parent: {
|
|
132
|
+
instance_id: string;
|
|
133
|
+
name: string | null;
|
|
134
|
+
} | null;
|
|
135
|
+
}
|
|
136
|
+
/** Recorded fork state for <instanceId>, or null when it has no history row. */
|
|
137
|
+
export declare function readForkState(coordRoot: string, instanceId: string): ForkState | null;
|
|
138
|
+
/**
|
|
139
|
+
* Record fork detection that ran after the instance was already named (the
|
|
140
|
+
* tool-path heal). Appends a copy of the latest identity row, so the name,
|
|
141
|
+
* kind, and persona are unchanged, plus `fork_checked` and any lineage found.
|
|
142
|
+
* A no-op for an instance with no history row or one already checked.
|
|
143
|
+
*/
|
|
144
|
+
export declare function recordForkDetection(coordRoot: string, instanceId: string, detection: {
|
|
145
|
+
fork: boolean;
|
|
146
|
+
forkedFrom?: string;
|
|
147
|
+
}): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Peer labels for recorded fork relations, keyed by instance id: this
|
|
150
|
+
* session's fork parent and its direct forks. `liveIds` marks which of them
|
|
151
|
+
* are running, so a fork can tell its live parent from its own earlier self.
|
|
152
|
+
*/
|
|
153
|
+
export declare function forkRelationLabels(coordRoot: string, instanceId: string, liveIds: Set<string>): Map<string, string>;
|
|
154
|
+
/** Instance ids recorded as direct forks of <instanceId>. */
|
|
155
|
+
export declare function readForkChildren(coordRoot: string, instanceId: string): Set<string>;
|
|
117
156
|
/** One step of recorded fork lineage: the latest row for <instanceId> that
|
|
118
|
-
* carries `forked_from
|
|
157
|
+
* carries `forked_from`. Later rows without lineage (an identity assumption)
|
|
158
|
+
* do not erase it: a session never stops being a fork. */
|
|
119
159
|
export declare function readForkParent(coordRoot: string, instanceId: string): {
|
|
120
160
|
instance_id: string;
|
|
121
161
|
name: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/names.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAYH,qBAAqB;AACrB,eAAO,MAAM,WAAW,85EA+Qd,CAAC;AAMX,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,gBAAgB,CAAC;AAE/E,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;IACf,EAAE,EAAE,MAAM,CAAC;IACX,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB
|
|
1
|
+
{"version":3,"file":"names.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/names.ts"],"names":[],"mappings":"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"}
|
|
@@ -496,19 +496,103 @@ export function assignName(coordRoot, instanceId, kind, opts) {
|
|
|
496
496
|
kind,
|
|
497
497
|
source: "pool",
|
|
498
498
|
...(forkedFrom && forkedFrom !== instanceId ? { forked_from: forkedFrom } : {}),
|
|
499
|
+
...(opts.fork || (forkedFrom && forkedFrom !== instanceId) ? { fork: true } : {}),
|
|
500
|
+
...(opts.forkChecked ? { fork_checked: true } : {}),
|
|
499
501
|
ts: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
500
502
|
});
|
|
501
503
|
return name;
|
|
502
504
|
}
|
|
505
|
+
/** Recorded fork state for <instanceId>, or null when it has no history row. */
|
|
506
|
+
export function readForkState(coordRoot, instanceId) {
|
|
507
|
+
const history = readHistory(coordRoot);
|
|
508
|
+
let seen = false;
|
|
509
|
+
let fork = false;
|
|
510
|
+
let checked = false;
|
|
511
|
+
let parentId;
|
|
512
|
+
for (const row of history) {
|
|
513
|
+
if (row.instance_id !== instanceId)
|
|
514
|
+
continue;
|
|
515
|
+
seen = true;
|
|
516
|
+
if (row.fork || row.forked_from)
|
|
517
|
+
fork = true;
|
|
518
|
+
if (row.fork_checked)
|
|
519
|
+
checked = true;
|
|
520
|
+
if (row.forked_from && row.forked_from !== instanceId)
|
|
521
|
+
parentId = row.forked_from;
|
|
522
|
+
}
|
|
523
|
+
if (!seen)
|
|
524
|
+
return null;
|
|
525
|
+
const parent = parentId
|
|
526
|
+
? { instance_id: parentId, name: resolveName(coordRoot, parentId)?.name ?? null }
|
|
527
|
+
: null;
|
|
528
|
+
return { fork, checked, parent };
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Record fork detection that ran after the instance was already named (the
|
|
532
|
+
* tool-path heal). Appends a copy of the latest identity row, so the name,
|
|
533
|
+
* kind, and persona are unchanged, plus `fork_checked` and any lineage found.
|
|
534
|
+
* A no-op for an instance with no history row or one already checked.
|
|
535
|
+
*/
|
|
536
|
+
export function recordForkDetection(coordRoot, instanceId, detection) {
|
|
537
|
+
const history = readHistory(coordRoot);
|
|
538
|
+
let latest;
|
|
539
|
+
for (const row of history) {
|
|
540
|
+
if (row.instance_id !== instanceId)
|
|
541
|
+
continue;
|
|
542
|
+
if (row.fork_checked)
|
|
543
|
+
return false;
|
|
544
|
+
latest = row;
|
|
545
|
+
}
|
|
546
|
+
if (!latest)
|
|
547
|
+
return false;
|
|
548
|
+
const forkedFrom = detection.forkedFrom && detection.forkedFrom !== instanceId ? detection.forkedFrom : undefined;
|
|
549
|
+
appendHistory(coordRoot, {
|
|
550
|
+
instance_id: instanceId,
|
|
551
|
+
name: latest.name,
|
|
552
|
+
kind: latest.kind,
|
|
553
|
+
...(latest.agent_id ? { agent_id: latest.agent_id } : {}),
|
|
554
|
+
source: "fork.detected",
|
|
555
|
+
...(forkedFrom ? { forked_from: forkedFrom } : {}),
|
|
556
|
+
...(detection.fork || forkedFrom || latest.fork ? { fork: true } : {}),
|
|
557
|
+
fork_checked: true,
|
|
558
|
+
ts: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
559
|
+
});
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Peer labels for recorded fork relations, keyed by instance id: this
|
|
564
|
+
* session's fork parent and its direct forks. `liveIds` marks which of them
|
|
565
|
+
* are running, so a fork can tell its live parent from its own earlier self.
|
|
566
|
+
*/
|
|
567
|
+
export function forkRelationLabels(coordRoot, instanceId, liveIds) {
|
|
568
|
+
const out = new Map();
|
|
569
|
+
const parent = readForkParent(coordRoot, instanceId);
|
|
570
|
+
if (parent) {
|
|
571
|
+
out.set(parent.instance_id, liveIds.has(parent.instance_id) ? "your fork parent, live" : "your fork parent");
|
|
572
|
+
}
|
|
573
|
+
for (const child of readForkChildren(coordRoot, instanceId))
|
|
574
|
+
out.set(child, "your fork");
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
/** Instance ids recorded as direct forks of <instanceId>. */
|
|
578
|
+
export function readForkChildren(coordRoot, instanceId) {
|
|
579
|
+
const out = new Set();
|
|
580
|
+
for (const row of readHistory(coordRoot)) {
|
|
581
|
+
if (row.forked_from === instanceId && row.instance_id !== instanceId)
|
|
582
|
+
out.add(row.instance_id);
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
503
586
|
/** One step of recorded fork lineage: the latest row for <instanceId> that
|
|
504
|
-
* carries `forked_from
|
|
587
|
+
* carries `forked_from`. Later rows without lineage (an identity assumption)
|
|
588
|
+
* do not erase it: a session never stops being a fork. */
|
|
505
589
|
export function readForkParent(coordRoot, instanceId) {
|
|
506
590
|
const history = readHistory(coordRoot);
|
|
507
591
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
508
592
|
const row = history[i];
|
|
509
|
-
if (row.instance_id !== instanceId)
|
|
593
|
+
if (row.instance_id !== instanceId || !row.forked_from)
|
|
510
594
|
continue;
|
|
511
|
-
if (
|
|
595
|
+
if (row.forked_from === instanceId)
|
|
512
596
|
return null;
|
|
513
597
|
const parent = resolveName(coordRoot, row.forked_from);
|
|
514
598
|
return { instance_id: row.forked_from, name: parent?.name ?? null };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pack-reader.d.ts","sourceRoot":"","sources":["../../../../../src/core/events/v3/support-storage/pack-reader.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pack-reader.d.ts","sourceRoot":"","sources":["../../../../../src/core/events/v3/support-storage/pack-reader.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,0BAA0B,EAC/B,KAAK,wBAAwB,EAI9B,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,wBAAwB;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAClE;AAED,MAAM,WAAW,+BACf,SAAQ,IAAI,CAAC,wBAAwB,EAAE,gBAAgB,CAAC;IACxD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,0BAA0B,CAAC;IACrC,OAAO,EAAE,+BAA+B,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,0BAA0B,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAUD,wBAAsB,8BAA8B,CAClD,YAAY,EAAE,MAAM,EACpB,MAAM,GAAE,wBAA6B,GACpC,OAAO,CAAC,0BAA0B,CAAC,CAarC;AAED,4FAA4F;AAC5F,wBAAuB,+BAA+B,CACpD,YAAY,EAAE,MAAM,EACpB,MAAM,GAAE,wBAA6B,GACpC,cAAc,CAAC,+BAA+B,CAAC,CAGjD;AAoGD,wBAAsB,0BAA0B,CAC9C,YAAY,EAAE,MAAM,EACpB,MAAM,GAAE,wBAA6B,GACpC,OAAO,CAAC,2BAA2B,CAAC,CAWtC;AAED,yFAAyF;AACzF,wBAAsB,wBAAwB,CAC5C,YAAY,EAAE,MAAM,EACpB,MAAM,GAAE,wBAA6B,GACpC,OAAO,CAAC,0BAA0B,CAAC,CAarC;AAED,0FAA0F;AAC1F,wBAAsB,wBAAwB,CAC5C,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,MAAM,GAAE,wBAA6B,GACpC,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CA0BhE"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { constants
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
3
|
import { lstat, mkdir, open, realpath, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { finished } from "node:stream/promises";
|
|
5
7
|
import { StringDecoder } from "node:string_decoder";
|
|
6
8
|
import { createGunzip, createInflateRaw } from "node:zlib";
|
|
7
9
|
import { stateDirMode, stateFileMode } from "../../../storage/modes.js";
|
|
@@ -44,18 +46,12 @@ async function* streamEventV3SupportPackRecordsForManifest(manifestPath, manifes
|
|
|
44
46
|
throw new Error("event_v3_support_payload_length_mismatch");
|
|
45
47
|
if (payloadStat.size > bounded.max_payload_bytes)
|
|
46
48
|
throw new Error("event_v3_support_payload_too_large");
|
|
47
|
-
if ((await hashHandle(
|
|
49
|
+
if ((await hashHandle(payloadHandle)) !== manifest.payload.digest) {
|
|
48
50
|
throw new Error("event_v3_support_payload_digest_mismatch");
|
|
49
51
|
}
|
|
50
52
|
limits.fault?.("after_payload_digest", payloadPath);
|
|
51
|
-
await assertCanonicalSingleGzipMember(
|
|
53
|
+
await assertCanonicalSingleGzipMember(payloadHandle, payloadStat.size, bounded.max_decompressed_bytes);
|
|
52
54
|
const gunzip = createGunzip();
|
|
53
|
-
const source = createReadStream(payloadPath, {
|
|
54
|
-
fd: payloadHandle.fd,
|
|
55
|
-
autoClose: false,
|
|
56
|
-
start: 0,
|
|
57
|
-
});
|
|
58
|
-
source.pipe(gunzip);
|
|
59
55
|
const decoder = new StringDecoder("utf8");
|
|
60
56
|
let pending = "";
|
|
61
57
|
let decompressedBytes = 0;
|
|
@@ -64,7 +60,7 @@ async function* streamEventV3SupportPackRecordsForManifest(manifestPath, manifes
|
|
|
64
60
|
let priorPath;
|
|
65
61
|
const entries = [];
|
|
66
62
|
const seen = new Set();
|
|
67
|
-
for await (const current of gunzip) {
|
|
63
|
+
for await (const current of decompressHandle(payloadHandle, 0, gunzip)) {
|
|
68
64
|
const chunk = current;
|
|
69
65
|
decompressedBytes += chunk.length;
|
|
70
66
|
if (decompressedBytes > bounded.max_decompressed_bytes) {
|
|
@@ -176,20 +172,51 @@ export async function unpackEventV3SupportPack(manifestPath, destination, limits
|
|
|
176
172
|
throw error;
|
|
177
173
|
}
|
|
178
174
|
}
|
|
179
|
-
async function hashHandle(
|
|
175
|
+
async function hashHandle(handle) {
|
|
180
176
|
const hash = createHash("sha256");
|
|
181
|
-
|
|
182
|
-
for await (const current of stream)
|
|
177
|
+
for await (const current of handleChunks(handle, 0))
|
|
183
178
|
hash.update(current);
|
|
184
179
|
return `sha256:${hash.digest("hex")}`;
|
|
185
180
|
}
|
|
181
|
+
async function* handleChunks(handle, position) {
|
|
182
|
+
while (true) {
|
|
183
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
184
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
185
|
+
if (bytesRead === 0)
|
|
186
|
+
return;
|
|
187
|
+
position += bytesRead;
|
|
188
|
+
yield buffer.subarray(0, bytesRead);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function* decompressHandle(handle, start, decoder) {
|
|
192
|
+
const source = Readable.from(handleChunks(handle, start));
|
|
193
|
+
// Observe failures immediately, then settle source reads before the caller
|
|
194
|
+
// can close and reuse the verified handle, including on early rejection.
|
|
195
|
+
const completed = finished(source, { cleanup: true }).then(() => undefined, (error) => error);
|
|
196
|
+
source.once("error", (error) => decoder.destroy(error));
|
|
197
|
+
source.pipe(decoder);
|
|
198
|
+
try {
|
|
199
|
+
for await (const chunk of decoder)
|
|
200
|
+
yield chunk;
|
|
201
|
+
source.unpipe(decoder);
|
|
202
|
+
source.resume();
|
|
203
|
+
const error = await completed;
|
|
204
|
+
if (error)
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
source.destroy();
|
|
209
|
+
decoder.destroy();
|
|
210
|
+
await completed;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
186
213
|
async function realDirectory(path) {
|
|
187
214
|
const stat = await lstat(path);
|
|
188
215
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
189
216
|
throw new Error("event_v3_support_pack_root_invalid");
|
|
190
217
|
return realpath(path);
|
|
191
218
|
}
|
|
192
|
-
async function assertCanonicalSingleGzipMember(
|
|
219
|
+
async function assertCanonicalSingleGzipMember(handle, payloadBytes, maximumDecompressedBytes) {
|
|
193
220
|
if (payloadBytes < 18)
|
|
194
221
|
throw new Error("event_v3_support_gzip_truncated");
|
|
195
222
|
const header = Buffer.alloc(10);
|
|
@@ -203,9 +230,8 @@ async function assertCanonicalSingleGzipMember(path, handle, payloadBytes, maxim
|
|
|
203
230
|
throw new Error("event_v3_support_gzip_header_noncanonical");
|
|
204
231
|
}
|
|
205
232
|
const inflate = createInflateRaw();
|
|
206
|
-
createReadStream(path, { fd: handle.fd, autoClose: false, start: 10 }).pipe(inflate);
|
|
207
233
|
let decompressed = 0;
|
|
208
|
-
for await (const current of inflate) {
|
|
234
|
+
for await (const current of decompressHandle(handle, 10, inflate)) {
|
|
209
235
|
decompressed += current.length;
|
|
210
236
|
if (decompressed > maximumDecompressedBytes) {
|
|
211
237
|
inflate.destroy();
|