harnery 0.43.0 → 0.44.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/README.md +3 -3
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +41 -48
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +38 -11
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +10 -0
- package/dist/commands/doctor.d.ts +12 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +22 -7
- package/dist/commands/governor.js +22 -10
- package/dist/commands/inbox.d.ts +1 -1
- package/dist/commands/inbox.d.ts.map +1 -1
- package/dist/commands/inbox.js +55 -2
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +28 -4
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +16 -1
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +7 -2
- package/dist/commands/toc.d.ts +14 -0
- package/dist/commands/toc.d.ts.map +1 -1
- package/dist/commands/toc.js +7 -4
- package/dist/commands/workflow.js +24 -30
- package/dist/core/agents/live-authority-v3.d.ts +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -0
- package/dist/core/agents/live-lifecycle-v3.d.ts +4 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +28 -7
- package/dist/core/agents/mailbox.d.ts +9 -8
- package/dist/core/agents/mailbox.d.ts.map +1 -1
- package/dist/core/agents/mailbox.js +24 -17
- package/dist/core/agents/render/prompt-context.d.ts +2 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +8 -12
- package/dist/core/agents/render/session-context.d.ts +2 -8
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +7 -30
- package/dist/core/agents/rules/stop-hook.js +3 -3
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +25 -14
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +5 -0
- package/dist/core/agents/state/proc-start.d.ts +19 -0
- package/dist/core/agents/state/proc-start.d.ts.map +1 -1
- package/dist/core/agents/state/proc-start.js +41 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +180 -70
- package/dist/core/agents/state/unchanged-file.d.ts +44 -0
- package/dist/core/agents/state/unchanged-file.d.ts.map +1 -0
- package/dist/core/agents/state/unchanged-file.js +390 -0
- package/dist/core/context/index.d.ts +22 -10
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +381 -119
- package/dist/core/context/observation.d.ts +26 -0
- package/dist/core/context/observation.d.ts.map +1 -0
- package/dist/core/context/observation.js +11 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +25 -14
- package/dist/core/diagnostics/path-identity.d.ts +3 -0
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -0
- package/dist/core/diagnostics/path-identity.js +12 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -1
- package/dist/core/diagnostics/sanitize.js +4 -3
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +29 -0
- package/dist/core/events/v3/generation-custody.d.ts +5 -0
- package/dist/core/events/v3/generation-custody.d.ts.map +1 -0
- package/dist/core/events/v3/generation-custody.js +50 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +1 -0
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +7 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +77 -64
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +14 -1
- package/dist/core/governor/service-read.d.ts +10 -2
- package/dist/core/governor/service-read.d.ts.map +1 -1
- package/dist/core/governor/service-read.js +105 -22
- package/dist/core/governor/service.d.ts +20 -2
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +72 -47
- package/dist/core/hooks/cli.js +33 -5
- package/dist/core/inbox/contract.d.ts +2 -1
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/index.d.ts +2 -0
- package/dist/core/inbox/index.d.ts.map +1 -1
- package/dist/core/inbox/index.js +2 -0
- package/dist/core/inbox/peer-messages.d.ts +37 -0
- package/dist/core/inbox/peer-messages.d.ts.map +1 -0
- package/dist/core/inbox/peer-messages.js +72 -0
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +67 -21
- package/dist/core/inbox/targets.d.ts +22 -0
- package/dist/core/inbox/targets.d.ts.map +1 -0
- package/dist/core/inbox/targets.js +108 -0
- package/dist/core/inbox/watcher.js +7 -3
- package/dist/core/journal/index.d.ts.map +1 -1
- package/dist/core/journal/index.js +17 -2
- package/dist/core/resources/contract.d.ts +3 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/service-status.d.ts +14 -1
- package/dist/core/resources/service-status.d.ts.map +1 -1
- package/dist/core/resources/service-status.js +23 -13
- package/dist/core/resources/service.d.ts +9 -1
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +47 -27
- package/dist/core/resources/status.d.ts +2 -5
- package/dist/core/resources/status.d.ts.map +1 -1
- package/dist/core/resources/status.js +36 -7
- package/dist/core/servers/index.d.ts +24 -13
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +132 -92
- package/dist/core/servers/tunnels.d.ts +3 -5
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +3 -9
- package/dist/core/supervisor/contract.d.ts +12 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.d.ts.map +1 -1
- package/dist/core/supervisor/explanations.js +2 -1
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +54 -9
- package/dist/core/supervisor/service.d.ts +9 -1
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +59 -29
- package/dist/core/supervisor/services.d.ts.map +1 -1
- package/dist/core/supervisor/services.js +22 -1
- package/dist/core/supervisor/status.d.ts +17 -2
- package/dist/core/supervisor/status.d.ts.map +1 -1
- package/dist/core/supervisor/status.js +70 -12
- package/dist/core/supervisor/timeline.d.ts.map +1 -1
- package/dist/core/supervisor/timeline.js +2 -1
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +105 -32
- package/dist/core/workflow/failure-evidence.d.ts +16 -0
- package/dist/core/workflow/failure-evidence.d.ts.map +1 -0
- package/dist/core/workflow/failure-evidence.js +114 -0
- package/dist/core/workflow/index.d.ts +1 -1
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +1 -1
- package/dist/core/workflow/live-session-v3.d.ts +6 -0
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +126 -5
- package/dist/core/workflow/proof.d.ts +5 -5
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +115 -20
- package/dist/core/workflow/run-state.d.ts +3 -1
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +31 -27
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +19 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +13 -0
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +19 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +14 -0
- package/dist/core/workflow/types.d.ts +64 -17
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.d.ts +0 -1
- package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/index.js +0 -1
- package/dist/core/workflow/workspaces/leases.d.ts +3 -1
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +30 -16
- package/dist/core/workflow/workspaces/local-git.d.ts +6 -2
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +270 -6
- package/dist/core/workflow/workspaces/ownership.d.ts +11 -0
- package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -0
- package/dist/core/workflow/workspaces/ownership.js +194 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts +35 -0
- package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/reclaim.js +14 -0
- package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/recovery.js +21 -13
- package/dist/core/workflow/workspaces/state.d.ts +1 -0
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +21 -1
- package/dist/lib/browser/windows-chrome.d.ts +100 -12
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +262 -73
- package/dist/lib/docs-links.d.ts.map +1 -1
- package/dist/lib/docs-links.js +45 -2
- package/dist/lib/guarded-remove.d.ts +7 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -1
- package/dist/lib/guarded-remove.js +106 -12
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -0
- package/dist/lib/zip/index.d.ts +41 -0
- package/dist/lib/zip/index.d.ts.map +1 -0
- package/dist/lib/zip/index.js +398 -0
- package/package.json +7 -1
- package/src/commands/agents.ts +54 -63
- package/src/commands/browse.ts +50 -14
- package/src/commands/diagnostics.ts +10 -0
- package/src/commands/doctor.ts +26 -7
- package/src/commands/governor.ts +23 -10
- package/src/commands/inbox.ts +52 -2
- package/src/commands/rm.ts +43 -4
- package/src/commands/servers.ts +16 -4
- package/src/commands/supervisor.ts +7 -2
- package/src/commands/toc.ts +14 -6
- package/src/commands/workflow.ts +30 -30
- package/src/core/agents/live-authority-v3.ts +17 -0
- package/src/core/agents/live-lifecycle-v3.ts +52 -14
- package/src/core/agents/mailbox.ts +26 -17
- package/src/core/agents/render/prompt-context.ts +10 -12
- package/src/core/agents/render/session-context.ts +9 -32
- package/src/core/agents/rules/stop-hook.ts +3 -3
- package/src/core/agents/state/heartbeat-writer.ts +29 -12
- package/src/core/agents/state/live-coordination-writer.ts +29 -0
- package/src/core/agents/state/pidmap.ts +13 -0
- package/src/core/agents/state/proc-start.ts +57 -0
- package/src/core/agents/state/stale-sweep.ts +247 -82
- package/src/core/agents/state/unchanged-file.ts +456 -0
- package/src/core/context/index.ts +446 -136
- package/src/core/context/observation.ts +29 -0
- package/src/core/diagnostics/bundle.ts +23 -18
- package/src/core/diagnostics/path-identity.ts +15 -0
- package/src/core/diagnostics/sanitize.ts +4 -3
- package/src/core/events/v3/authority-outbox.ts +56 -0
- package/src/core/events/v3/generation-custody.ts +59 -0
- package/src/core/events/v3/producers/coordination-recorder.ts +15 -0
- package/src/core/events/v3/producers/recorder.ts +94 -68
- package/src/core/governor/planning.ts +20 -8
- package/src/core/governor/service-read.ts +128 -20
- package/src/core/governor/service.ts +119 -42
- package/src/core/hooks/cli.ts +33 -5
- package/src/core/inbox/contract.ts +2 -1
- package/src/core/inbox/index.ts +2 -0
- package/src/core/inbox/peer-messages.ts +94 -0
- package/src/core/inbox/service.ts +88 -22
- package/src/core/inbox/targets.ts +148 -0
- package/src/core/inbox/watcher.ts +8 -9
- package/src/core/journal/index.ts +19 -2
- package/src/core/resources/contract.ts +4 -0
- package/src/core/resources/service-status.ts +35 -12
- package/src/core/resources/service.ts +71 -24
- package/src/core/resources/status.ts +57 -8
- package/src/core/servers/index.ts +177 -91
- package/src/core/servers/tunnels.ts +3 -11
- package/src/core/supervisor/contract.ts +13 -0
- package/src/core/supervisor/explanations.ts +4 -1
- package/src/core/supervisor/findings.ts +68 -10
- package/src/core/supervisor/service.ts +88 -26
- package/src/core/supervisor/services.ts +26 -8
- package/src/core/supervisor/status.ts +89 -12
- package/src/core/supervisor/timeline.ts +2 -1
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +131 -49
- package/src/core/workflow/failure-evidence.ts +141 -0
- package/src/core/workflow/index.ts +0 -1
- package/src/core/workflow/live-session-v3.ts +146 -5
- package/src/core/workflow/proof.ts +134 -26
- package/src/core/workflow/run-state.ts +31 -29
- package/src/core/workflow/spawn-claude.ts +18 -0
- package/src/core/workflow/spawn-codex.ts +13 -0
- package/src/core/workflow/spawn-cursor.ts +18 -0
- package/src/core/workflow/spawn-opencode.ts +14 -0
- package/src/core/workflow/types.ts +51 -17
- package/src/core/workflow/workspaces/index.ts +0 -1
- package/src/core/workflow/workspaces/leases.ts +48 -17
- package/src/core/workflow/workspaces/local-git.ts +315 -5
- package/src/core/workflow/workspaces/ownership.ts +232 -0
- package/src/core/workflow/workspaces/reclaim.ts +58 -0
- package/src/core/workflow/workspaces/recovery.ts +25 -13
- package/src/core/workflow/workspaces/state.ts +20 -0
- package/src/lib/browser/windows-chrome.ts +346 -86
- package/src/lib/docs-links.ts +49 -2
- package/src/lib/guarded-remove.ts +132 -14
- package/src/lib/identities/assume.ts +4 -0
- package/src/lib/zip/index.ts +483 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { inflateRawSync } from "node:zlib";
|
|
5
|
+
import { strFromU8, zipSync } from "fflate";
|
|
6
|
+
export const DEFAULT_ZIP_LIMITS = Object.freeze({
|
|
7
|
+
archiveBytes: 64 * 1024 * 1024,
|
|
8
|
+
entryBytes: 64 * 1024 * 1024,
|
|
9
|
+
totalBytes: 256 * 1024 * 1024,
|
|
10
|
+
entries: 10_000,
|
|
11
|
+
});
|
|
12
|
+
function limitsFor(options) {
|
|
13
|
+
const limits = { ...DEFAULT_ZIP_LIMITS, ...options.limits };
|
|
14
|
+
for (const [key, value] of Object.entries(limits))
|
|
15
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
16
|
+
throw new Error(`Invalid ZIP limit: ${key}`);
|
|
17
|
+
return limits;
|
|
18
|
+
}
|
|
19
|
+
function readBoundedFile(path, limit, noFollow = false) {
|
|
20
|
+
const fd = openSync(path, constants.O_RDONLY | (noFollow ? (constants.O_NOFOLLOW ?? 0) : 0));
|
|
21
|
+
try {
|
|
22
|
+
const stat = fstatSync(fd);
|
|
23
|
+
if (!stat.isFile() || stat.size > limit)
|
|
24
|
+
throw new Error("ZIP input exceeds byte limit or is not a file");
|
|
25
|
+
const bytes = Buffer.alloc(stat.size + 1);
|
|
26
|
+
let count = 0;
|
|
27
|
+
while (count < bytes.length) {
|
|
28
|
+
const read = readSync(fd, bytes, count, bytes.length - count, null);
|
|
29
|
+
if (!read)
|
|
30
|
+
break;
|
|
31
|
+
count += read;
|
|
32
|
+
}
|
|
33
|
+
if (count !== stat.size)
|
|
34
|
+
throw new Error("ZIP input changed while reading");
|
|
35
|
+
return bytes.subarray(0, count);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function bytesOf(source, limits) {
|
|
42
|
+
const bytes = typeof source === "string" ? readBoundedFile(source, limits.archiveBytes) : source;
|
|
43
|
+
if (bytes.byteLength > limits.archiveBytes)
|
|
44
|
+
throw new Error("ZIP archive exceeds compressed byte limit");
|
|
45
|
+
return bytes;
|
|
46
|
+
}
|
|
47
|
+
function centralEntries(bytes, limits) {
|
|
48
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
49
|
+
let end = bytes.length - 22;
|
|
50
|
+
for (; end >= Math.max(0, bytes.length - 65_557); end--) {
|
|
51
|
+
if (view.getUint32(end, true) === 0x06054b50 &&
|
|
52
|
+
end + 22 + view.getUint16(end + 20, true) === bytes.length)
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
if (end < Math.max(0, bytes.length - 65_557))
|
|
56
|
+
throw new Error("Missing ZIP end record");
|
|
57
|
+
const count = view.getUint16(end + 10, true);
|
|
58
|
+
if (view.getUint16(end + 4, true) !== 0 ||
|
|
59
|
+
view.getUint16(end + 6, true) !== 0 ||
|
|
60
|
+
view.getUint16(end + 8, true) !== count ||
|
|
61
|
+
count === 0xffff)
|
|
62
|
+
throw new Error("Split or ZIP64 archives are unsupported");
|
|
63
|
+
if (count > limits.entries)
|
|
64
|
+
throw new Error("ZIP archive exceeds entry count limit");
|
|
65
|
+
const central = view.getUint32(end + 16, true);
|
|
66
|
+
let offset = central;
|
|
67
|
+
if (offset + view.getUint32(end + 12, true) !== end)
|
|
68
|
+
throw new Error("Invalid ZIP directory bounds");
|
|
69
|
+
const entries = [];
|
|
70
|
+
const names = new Set();
|
|
71
|
+
let total = 0;
|
|
72
|
+
for (let index = 0; index < count; index++) {
|
|
73
|
+
if (offset + 46 > end || view.getUint32(offset, true) !== 0x02014b50)
|
|
74
|
+
throw new Error("Invalid ZIP directory entry");
|
|
75
|
+
const flags = view.getUint16(offset + 8, true);
|
|
76
|
+
const method = view.getUint16(offset + 10, true);
|
|
77
|
+
if (flags & 1)
|
|
78
|
+
throw new Error("Encrypted ZIP entries are unsupported");
|
|
79
|
+
if (method !== 0 && method !== 8)
|
|
80
|
+
throw new Error("Unsupported ZIP compression method");
|
|
81
|
+
const nameLength = view.getUint16(offset + 28, true);
|
|
82
|
+
const next = offset +
|
|
83
|
+
46 +
|
|
84
|
+
nameLength +
|
|
85
|
+
view.getUint16(offset + 30, true) +
|
|
86
|
+
view.getUint16(offset + 32, true);
|
|
87
|
+
if (next > end || view.getUint16(offset + 34, true) !== 0)
|
|
88
|
+
throw new Error("Invalid ZIP directory entry bounds");
|
|
89
|
+
const rawName = bytes.subarray(offset + 46, offset + 46 + nameLength);
|
|
90
|
+
const name = strFromU8(rawName, !(flags & 0x800));
|
|
91
|
+
if (names.has(name))
|
|
92
|
+
throw new Error(`Duplicate ZIP entry: ${name}`);
|
|
93
|
+
names.add(name);
|
|
94
|
+
const size = view.getUint32(offset + 24, true);
|
|
95
|
+
const compressedSize = view.getUint32(offset + 20, true);
|
|
96
|
+
const checksum = view.getUint32(offset + 16, true);
|
|
97
|
+
const header = view.getUint32(offset + 42, true);
|
|
98
|
+
if (size === 0xffffffff || compressedSize === 0xffffffff || header === 0xffffffff)
|
|
99
|
+
throw new Error("ZIP64 entries are unsupported");
|
|
100
|
+
total += size;
|
|
101
|
+
if (size > limits.entryBytes || total > limits.totalBytes)
|
|
102
|
+
throw new Error("ZIP archive exceeds entry or total byte limit");
|
|
103
|
+
if (header + 30 > central || view.getUint32(header, true) !== 0x04034b50)
|
|
104
|
+
throw new Error("Invalid ZIP local header");
|
|
105
|
+
const localNameLength = view.getUint16(header + 26, true);
|
|
106
|
+
const dataOffset = header + 30 + localNameLength + view.getUint16(header + 28, true);
|
|
107
|
+
if (dataOffset + compressedSize > central ||
|
|
108
|
+
localNameLength !== nameLength ||
|
|
109
|
+
view.getUint16(header + 6, true) !== flags ||
|
|
110
|
+
view.getUint16(header + 8, true) !== method ||
|
|
111
|
+
!rawName.every((byte, i) => bytes[header + 30 + i] === byte))
|
|
112
|
+
throw new Error("ZIP local and central header mismatch");
|
|
113
|
+
if (!(flags & 8) &&
|
|
114
|
+
(view.getUint32(header + 14, true) !== checksum ||
|
|
115
|
+
view.getUint32(header + 18, true) !== compressedSize ||
|
|
116
|
+
view.getUint32(header + 22, true) !== size))
|
|
117
|
+
throw new Error("ZIP local and central size/checksum mismatch");
|
|
118
|
+
if (method === 0 && compressedSize !== size)
|
|
119
|
+
throw new Error("Stored ZIP entry size mismatch");
|
|
120
|
+
if (name.endsWith("/") && size !== 0)
|
|
121
|
+
throw new Error("ZIP directory has a payload");
|
|
122
|
+
entries.push({
|
|
123
|
+
name,
|
|
124
|
+
size,
|
|
125
|
+
compressedSize,
|
|
126
|
+
directory: name.endsWith("/"),
|
|
127
|
+
crc32: checksum,
|
|
128
|
+
method,
|
|
129
|
+
dataOffset,
|
|
130
|
+
headerOffset: header,
|
|
131
|
+
});
|
|
132
|
+
offset = next;
|
|
133
|
+
}
|
|
134
|
+
if (offset !== end)
|
|
135
|
+
throw new Error("Invalid ZIP directory length");
|
|
136
|
+
const ordered = [...entries].sort((a, b) => a.headerOffset - b.headerOffset);
|
|
137
|
+
for (let i = 1; i < ordered.length; i++)
|
|
138
|
+
if (ordered[i].headerOffset < ordered[i - 1].dataOffset + ordered[i - 1].compressedSize)
|
|
139
|
+
throw new Error("Overlapping ZIP entry ranges");
|
|
140
|
+
return entries;
|
|
141
|
+
}
|
|
142
|
+
const CRC_TABLE = Uint32Array.from({ length: 256 }, (_, value) => {
|
|
143
|
+
for (let bit = 0; bit < 8; bit++)
|
|
144
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
145
|
+
return value >>> 0;
|
|
146
|
+
});
|
|
147
|
+
function crc32(bytes) {
|
|
148
|
+
let crc = 0xffffffff;
|
|
149
|
+
for (const byte of bytes)
|
|
150
|
+
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ byte) & 255];
|
|
151
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
152
|
+
}
|
|
153
|
+
function inventory(bytes, limits) {
|
|
154
|
+
try {
|
|
155
|
+
return centralEntries(bytes, limits);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
throw new Error(`Unreadable ZIP archive: ${error instanceof Error ? error.message : String(error)}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export function listZip(source, options = {}) {
|
|
162
|
+
const limits = limitsFor(options);
|
|
163
|
+
return inventory(bytesOf(source, limits), limits).map(({ method, dataOffset, headerOffset, ...entry }) => entry);
|
|
164
|
+
}
|
|
165
|
+
export function readZipEntries(source, keep = () => true, options = {}) {
|
|
166
|
+
const limits = limitsFor(options);
|
|
167
|
+
const bytes = bytesOf(source, limits);
|
|
168
|
+
const selected = inventory(bytes, limits).filter((entry) => !entry.directory && keep(entry.name));
|
|
169
|
+
const result = new Map();
|
|
170
|
+
for (const entry of selected) {
|
|
171
|
+
const compressed = bytes.subarray(entry.dataOffset, entry.dataOffset + entry.compressedSize);
|
|
172
|
+
// zlib enforces actual output length during decoding, independent of forged headers.
|
|
173
|
+
const data = entry.method === 0
|
|
174
|
+
? compressed.slice()
|
|
175
|
+
: inflateRawSync(compressed, {
|
|
176
|
+
maxOutputLength: Math.max(1, Math.min(entry.size, limits.entryBytes)),
|
|
177
|
+
});
|
|
178
|
+
if (!(data instanceof Uint8Array) || data.byteLength !== entry.size)
|
|
179
|
+
throw new Error(`ZIP entry size or decode mismatch: ${entry.name}`);
|
|
180
|
+
if (crc32(data) !== entry.crc32)
|
|
181
|
+
throw new Error(`ZIP entry checksum mismatch: ${entry.name}`);
|
|
182
|
+
result.set(entry.name, data);
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
export function readZipEntry(source, name, options = {}) {
|
|
187
|
+
return readZipEntries(source, (entry) => entry === name, options).get(name);
|
|
188
|
+
}
|
|
189
|
+
export function readZipText(source, name, options = {}) {
|
|
190
|
+
const bytes = readZipEntry(source, name, options);
|
|
191
|
+
return bytes ? new TextDecoder().decode(bytes) : undefined;
|
|
192
|
+
}
|
|
193
|
+
/** Portable names exclude traversal, device names, streams and ambiguous aliases. */
|
|
194
|
+
export function safeEntryPath(root, name) {
|
|
195
|
+
const normalized = name.replaceAll("\\", "/");
|
|
196
|
+
const parts = normalized.replace(/\/$/, "").split("/");
|
|
197
|
+
if (!name ||
|
|
198
|
+
normalized.startsWith("/") ||
|
|
199
|
+
isAbsolute(normalized) ||
|
|
200
|
+
parts.some((part) => !part ||
|
|
201
|
+
part === "." ||
|
|
202
|
+
part === ".." ||
|
|
203
|
+
[...part].some((character) => character.charCodeAt(0) < 32) ||
|
|
204
|
+
/[:<>"|?*]/.test(part) ||
|
|
205
|
+
/[. ]$/.test(part) ||
|
|
206
|
+
/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) {
|
|
207
|
+
throw new Error(`Unsafe ZIP entry path: ${name}`);
|
|
208
|
+
}
|
|
209
|
+
const base = resolve(root);
|
|
210
|
+
const target = resolve(base, ...parts);
|
|
211
|
+
if (!target.startsWith(base.endsWith(sep) ? base : base + sep))
|
|
212
|
+
throw new Error(`ZIP entry escapes destination: ${name}`);
|
|
213
|
+
return target;
|
|
214
|
+
}
|
|
215
|
+
function statIfPresent(path) {
|
|
216
|
+
try {
|
|
217
|
+
return lstatSync(path);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error.code === "ENOENT")
|
|
221
|
+
return undefined;
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const outside = (rel) => rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel);
|
|
226
|
+
/**
|
|
227
|
+
* Canonicalize the directory holding a caller-named path. The caller chose
|
|
228
|
+
* that location, so links among its ancestors (macOS /tmp and /var, a linked
|
|
229
|
+
* home directory) are resolved once here. The named path itself and everything
|
|
230
|
+
* below it stay subject to assertNoLinks, which is where a planted link could
|
|
231
|
+
* redirect a write. Missing directories are appended unresolved and checked
|
|
232
|
+
* again once they exist.
|
|
233
|
+
*/
|
|
234
|
+
function anchored(path) {
|
|
235
|
+
const named = resolve(path);
|
|
236
|
+
const missing = [basename(named)];
|
|
237
|
+
let existing = dirname(named);
|
|
238
|
+
while (!statIfPresent(existing) && dirname(existing) !== existing) {
|
|
239
|
+
missing.unshift(basename(existing));
|
|
240
|
+
existing = dirname(existing);
|
|
241
|
+
}
|
|
242
|
+
const anchor = realpathSync(existing);
|
|
243
|
+
return { anchor, path: join(anchor, ...missing) };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Inspect every existing component from the path up to, not including, its
|
|
247
|
+
* anchor, including a dangling link at the final path.
|
|
248
|
+
*/
|
|
249
|
+
function assertNoLinks(path, leaf, anchor) {
|
|
250
|
+
let current = resolve(path);
|
|
251
|
+
if (outside(relative(anchor, current)))
|
|
252
|
+
throw new Error(`ZIP path is outside its anchored directory: ${current}`);
|
|
253
|
+
let first = true;
|
|
254
|
+
while (current !== anchor) {
|
|
255
|
+
const stat = statIfPresent(current);
|
|
256
|
+
if (stat) {
|
|
257
|
+
if (stat.isSymbolicLink())
|
|
258
|
+
throw new Error(`Refusing ZIP path through a symbolic link: ${current}`);
|
|
259
|
+
if ((!first || leaf === "directory") && !stat.isDirectory())
|
|
260
|
+
throw new Error(`ZIP directory path is not a directory: ${current}`);
|
|
261
|
+
if (first && leaf === "file" && !stat.isFile())
|
|
262
|
+
throw new Error(`ZIP output is not a regular file: ${current}`);
|
|
263
|
+
}
|
|
264
|
+
current = dirname(current);
|
|
265
|
+
first = false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function writeAtomic(path, bytes, overwrite, anchor) {
|
|
269
|
+
assertNoLinks(path, "file", anchor);
|
|
270
|
+
if (!overwrite && statIfPresent(path))
|
|
271
|
+
throw new Error(`ZIP output already exists: ${path}`);
|
|
272
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
273
|
+
assertNoLinks(dirname(path), "directory", anchor);
|
|
274
|
+
const temp = join(dirname(path), `.zip-${randomUUID()}.tmp`);
|
|
275
|
+
let fd;
|
|
276
|
+
try {
|
|
277
|
+
fd = openSync(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
278
|
+
writeFileSync(fd, bytes);
|
|
279
|
+
closeSync(fd);
|
|
280
|
+
fd = undefined;
|
|
281
|
+
assertNoLinks(path, "file", anchor);
|
|
282
|
+
if (overwrite)
|
|
283
|
+
renameSync(temp, path);
|
|
284
|
+
else {
|
|
285
|
+
linkSync(temp, path);
|
|
286
|
+
unlinkSync(temp);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
if (fd !== undefined)
|
|
291
|
+
closeSync(fd);
|
|
292
|
+
if (statIfPresent(temp))
|
|
293
|
+
unlinkSync(temp);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** Preflight all selected names, collisions and existing filesystem paths before writing. */
|
|
297
|
+
export function extractZip(source, dest, options = {}) {
|
|
298
|
+
const limits = limitsFor(options);
|
|
299
|
+
const bytes = bytesOf(source, limits);
|
|
300
|
+
const entries = inventory(bytes, limits).filter((entry) => (options.filter ?? (() => true))(entry.name));
|
|
301
|
+
const { anchor, path: root } = anchored(dest);
|
|
302
|
+
assertNoLinks(root, "directory", anchor);
|
|
303
|
+
const targets = new Map();
|
|
304
|
+
for (const entry of entries) {
|
|
305
|
+
const target = safeEntryPath(root, entry.name);
|
|
306
|
+
const key = target.toLowerCase();
|
|
307
|
+
if (targets.has(key))
|
|
308
|
+
throw new Error(`Colliding ZIP entry: ${entry.name}`);
|
|
309
|
+
targets.set(key, entry.directory);
|
|
310
|
+
assertNoLinks(target, entry.directory ? "directory" : "file", anchor);
|
|
311
|
+
if (!entry.directory && !options.overwrite && statIfPresent(target))
|
|
312
|
+
throw new Error(`ZIP output already exists: ${target}`);
|
|
313
|
+
}
|
|
314
|
+
for (const target of targets.keys()) {
|
|
315
|
+
let parent = dirname(target);
|
|
316
|
+
while (parent !== dirname(parent)) {
|
|
317
|
+
if (targets.get(parent) === false)
|
|
318
|
+
throw new Error(`ZIP file/directory collision: ${parent}`);
|
|
319
|
+
parent = dirname(parent);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const selected = new Set(entries.filter((entry) => !entry.directory).map((entry) => entry.name));
|
|
323
|
+
const files = readZipEntries(bytes, (name) => selected.has(name), options);
|
|
324
|
+
mkdirSync(root, { recursive: true });
|
|
325
|
+
for (const entry of entries) {
|
|
326
|
+
if (entry.directory) {
|
|
327
|
+
const path = safeEntryPath(root, entry.name);
|
|
328
|
+
assertNoLinks(path, "directory", anchor);
|
|
329
|
+
mkdirSync(path, { recursive: true });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const [name, data] of files)
|
|
333
|
+
writeAtomic(safeEntryPath(root, name), data, options.overwrite ?? false, anchor);
|
|
334
|
+
return [...files.keys()];
|
|
335
|
+
}
|
|
336
|
+
export function buildZip(inputs, options = {}) {
|
|
337
|
+
const limits = limitsFor(options);
|
|
338
|
+
// Inputs are matched against the base as the caller spelled it; reads go
|
|
339
|
+
// through its anchored form.
|
|
340
|
+
const named = resolve(options.base ?? ".");
|
|
341
|
+
const { anchor, path: base } = anchored(named);
|
|
342
|
+
assertNoLinks(base, "directory", anchor);
|
|
343
|
+
const tree = Object.create(null);
|
|
344
|
+
const entries = [];
|
|
345
|
+
const names = new Set();
|
|
346
|
+
let total = 0;
|
|
347
|
+
const add = (input) => {
|
|
348
|
+
const rel = relative(named, resolve(named, input));
|
|
349
|
+
if (!rel || outside(rel))
|
|
350
|
+
throw new Error(`ZIP input is outside its base directory: ${input}`);
|
|
351
|
+
const path = join(base, rel);
|
|
352
|
+
const stat = lstatSync(path);
|
|
353
|
+
if (stat.isSymbolicLink())
|
|
354
|
+
throw new Error(`Refusing ZIP input symbolic link: ${input}`);
|
|
355
|
+
assertNoLinks(path, stat.isDirectory() ? "directory" : "file", anchor);
|
|
356
|
+
if (stat.isDirectory()) {
|
|
357
|
+
for (const name of readdirSync(path).sort())
|
|
358
|
+
add(join(named, rel, name));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (!stat.isFile())
|
|
362
|
+
throw new Error(`ZIP input is not a regular file: ${input}`);
|
|
363
|
+
const name = rel.split(sep).join("/");
|
|
364
|
+
safeEntryPath(base, name);
|
|
365
|
+
if (Object.hasOwn(tree, name))
|
|
366
|
+
return;
|
|
367
|
+
if (names.has(name.toLowerCase()))
|
|
368
|
+
throw new Error(`Colliding ZIP input: ${name}`);
|
|
369
|
+
total += stat.size;
|
|
370
|
+
if (stat.size > limits.entryBytes ||
|
|
371
|
+
total > limits.totalBytes ||
|
|
372
|
+
entries.length >= limits.entries)
|
|
373
|
+
throw new Error("ZIP inputs exceed size or entry count limit");
|
|
374
|
+
const bytes = readBoundedFile(path, Math.min(stat.size, limits.entryBytes), true);
|
|
375
|
+
if (bytes.byteLength !== stat.size)
|
|
376
|
+
throw new Error(`ZIP input changed while reading: ${name}`);
|
|
377
|
+
names.add(name.toLowerCase());
|
|
378
|
+
tree[name] = [bytes, { mtime: stat.mtime }];
|
|
379
|
+
entries.push(name);
|
|
380
|
+
};
|
|
381
|
+
for (const input of [...inputs].sort())
|
|
382
|
+
add(input);
|
|
383
|
+
if (!entries.length)
|
|
384
|
+
throw new Error("Nothing to ZIP: inputs contain no files");
|
|
385
|
+
const bytes = zipSync(tree, { level: options.level ?? 6 });
|
|
386
|
+
if (bytes.byteLength > limits.archiveBytes)
|
|
387
|
+
throw new Error("ZIP archive exceeds compressed byte limit");
|
|
388
|
+
return { bytes, entries };
|
|
389
|
+
}
|
|
390
|
+
export function createZip(archive, inputs, options = {}) {
|
|
391
|
+
const { anchor, path } = anchored(archive);
|
|
392
|
+
assertNoLinks(path, "file", anchor);
|
|
393
|
+
if (!options.overwrite && statIfPresent(path))
|
|
394
|
+
throw new Error(`ZIP output already exists: ${archive}`);
|
|
395
|
+
const result = buildZip(inputs, options);
|
|
396
|
+
writeAtomic(path, result.bytes, options.overwrite ?? false, anchor);
|
|
397
|
+
return result.entries;
|
|
398
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harnery",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryan Kelly",
|
|
@@ -182,6 +182,11 @@
|
|
|
182
182
|
"types": "./dist/lib/format.d.ts",
|
|
183
183
|
"import": "./dist/lib/format.js"
|
|
184
184
|
},
|
|
185
|
+
"./lib/zip": {
|
|
186
|
+
"bun": "./src/lib/zip/index.ts",
|
|
187
|
+
"types": "./dist/lib/zip/index.d.ts",
|
|
188
|
+
"import": "./dist/lib/zip/index.js"
|
|
189
|
+
},
|
|
185
190
|
"./lib/headless": {
|
|
186
191
|
"bun": "./src/lib/headless/index.ts",
|
|
187
192
|
"types": "./dist/lib/headless/index.d.ts",
|
|
@@ -284,6 +289,7 @@
|
|
|
284
289
|
"@sinclair/typebox": "^0.34.52",
|
|
285
290
|
"axe-core": "^4.12.1",
|
|
286
291
|
"commander": "^14.0.3",
|
|
292
|
+
"fflate": "^0.8.2",
|
|
287
293
|
"gpt-tokenizer": "^3.4.0",
|
|
288
294
|
"js-yaml": "^4.3.0",
|
|
289
295
|
"linkedom": "^0.18.0",
|
package/src/commands/agents.ts
CHANGED
|
@@ -50,13 +50,6 @@ import {
|
|
|
50
50
|
LiveCoordinationAuthorityV3Error,
|
|
51
51
|
reopenLiveCoordinationGenerationV3,
|
|
52
52
|
} from "../core/agents/live-authority-v3.ts";
|
|
53
|
-
import {
|
|
54
|
-
isAddressableName,
|
|
55
|
-
MailboxCapacityError,
|
|
56
|
-
type QueueResult,
|
|
57
|
-
queueMailboxMessage,
|
|
58
|
-
suggestNames,
|
|
59
|
-
} from "../core/agents/mailbox.ts";
|
|
60
53
|
import { qaSignalStatusRow } from "../core/agents/qa-signal.ts";
|
|
61
54
|
import { reconcileCoordinationV3 } from "../core/agents/reconcile-coordination-v3.ts";
|
|
62
55
|
import {
|
|
@@ -130,9 +123,12 @@ import {
|
|
|
130
123
|
readRuntimeContextTelemetry,
|
|
131
124
|
readRuntimeContextUsage,
|
|
132
125
|
} from "../core/hooks/adapter/runtime-telemetry.ts";
|
|
126
|
+
import { HarneryInboxCapacityError } from "../core/inbox/contract.ts";
|
|
127
|
+
import { peerInbox, sendPeerMessage } from "../core/inbox/peer-messages.ts";
|
|
133
128
|
import { type RemoteMachine, readRemoteMachines } from "../core/presence/index.ts";
|
|
134
129
|
import { formatResourceSummary, readResourceStatus } from "../core/resources/status.ts";
|
|
135
130
|
import { registerContextCommand } from "./context.ts";
|
|
131
|
+
import { registerInboxCommand } from "./inbox.ts";
|
|
136
132
|
|
|
137
133
|
type LiveCoordinationRow = ReturnType<typeof readLiveCoordinationRows>[number];
|
|
138
134
|
|
|
@@ -313,6 +309,11 @@ export function registerAgentsCommand(
|
|
|
313
309
|
);
|
|
314
310
|
|
|
315
311
|
registerContextCommand(cmd, emitParam, programContext);
|
|
312
|
+
registerInboxCommand(cmd, emitParam, peerInbox(monorepoRoot() ?? process.cwd()), () => {
|
|
313
|
+
const owner = resolveOwner();
|
|
314
|
+
if (!owner) throw new Error("recipient instance is required outside an agent session");
|
|
315
|
+
return owner;
|
|
316
|
+
});
|
|
316
317
|
|
|
317
318
|
cmd
|
|
318
319
|
.command("whoami")
|
|
@@ -530,15 +531,22 @@ export function registerAgentsCommand(
|
|
|
530
531
|
cmd
|
|
531
532
|
.command("ping <name> <message...>")
|
|
532
533
|
.description(
|
|
533
|
-
"
|
|
534
|
-
"
|
|
535
|
-
"
|
|
536
|
-
"Only an unknown name is refused.",
|
|
534
|
+
"Queue a durable message for one known task instance by name. Stale or " +
|
|
535
|
+
"done tasks receive it when that same task resumes. Ambiguous names " +
|
|
536
|
+
"require an exact instance; receipts do not confirm attention.",
|
|
537
537
|
)
|
|
538
|
+
.option("--instance-id <id>", "Select the exact known recipient instance")
|
|
539
|
+
.option("--require-live", "Refuse a recipient outside live presence")
|
|
538
540
|
.option("--json", "JSON output")
|
|
539
|
-
.action(
|
|
540
|
-
|
|
541
|
-
|
|
541
|
+
.action(
|
|
542
|
+
(
|
|
543
|
+
name: string,
|
|
544
|
+
message: string[],
|
|
545
|
+
opts: { json?: boolean; instanceId?: string; requireLive?: boolean },
|
|
546
|
+
) => {
|
|
547
|
+
runPing(name, message.join(" "), opts);
|
|
548
|
+
},
|
|
549
|
+
);
|
|
542
550
|
|
|
543
551
|
cmd
|
|
544
552
|
.command("wait <name>")
|
|
@@ -4870,7 +4878,11 @@ function formatDuration(ms: number): string {
|
|
|
4870
4878
|
return h > 0 ? `${d}d${h}h` : `${d}d`;
|
|
4871
4879
|
}
|
|
4872
4880
|
|
|
4873
|
-
function runPing(
|
|
4881
|
+
function runPing(
|
|
4882
|
+
name: string,
|
|
4883
|
+
message: string,
|
|
4884
|
+
opts: { json?: boolean; instanceId?: string; requireLive?: boolean },
|
|
4885
|
+
): void {
|
|
4874
4886
|
if (!message || message.trim().length === 0) {
|
|
4875
4887
|
emit.error({ code: "empty_message", message: "message is required (and non-empty)" });
|
|
4876
4888
|
process.exit(1);
|
|
@@ -4891,52 +4903,33 @@ function runPing(name: string, message: string, opts: { json?: boolean }): void
|
|
|
4891
4903
|
);
|
|
4892
4904
|
process.exit(1);
|
|
4893
4905
|
}
|
|
4894
|
-
const coordRoot = monorepoRoot();
|
|
4895
|
-
const peerOwner = resolveOwnerByName(name);
|
|
4896
|
-
|
|
4897
|
-
// A name with no live session is the common case, not an error: the agent it
|
|
4898
|
-
// refers to may have ended minutes ago and may come back under the same name.
|
|
4899
|
-
// Refuse only a name that no agent has ever held, which is a typo — queueing
|
|
4900
|
-
// that would swallow the message silently.
|
|
4901
|
-
if (!peerOwner && coordRoot && !isAddressableName(coordRoot, name)) {
|
|
4902
|
-
const suggestions = suggestNames(coordRoot, name);
|
|
4903
|
-
emit.error({
|
|
4904
|
-
code: "unknown_agent",
|
|
4905
|
-
message:
|
|
4906
|
-
`no agent has ever been named "${name}". ` +
|
|
4907
|
-
(suggestions.length > 0 ? `Did you mean ${suggestions.join(", ")}? ` : "") +
|
|
4908
|
-
`Run \`${resolveBinName()} agents list\` for live sessions.`,
|
|
4909
|
-
});
|
|
4910
|
-
process.exit(1);
|
|
4911
|
-
}
|
|
4912
|
-
|
|
4906
|
+
const coordRoot = monorepoRoot() ?? process.cwd();
|
|
4913
4907
|
const myHb = readCurrentCoordinationRow(myOwner);
|
|
4914
4908
|
const fromName = myHb?.name ?? "anonymous";
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
// A live recipient gets the journal entry immediately, so a peer reading
|
|
4918
|
-
// their own journal mid-session sees it without waiting for a prompt. A
|
|
4919
|
-
// dormant recipient has no journal to write to yet; the drain writes it into
|
|
4920
|
-
// whichever session picks the message up.
|
|
4921
|
-
const doc = peerOwner ? appendEntry(peerOwner, "handoff", body) : null;
|
|
4922
|
-
|
|
4923
|
-
let queued: QueueResult;
|
|
4909
|
+
let sent: ReturnType<typeof sendPeerMessage>;
|
|
4924
4910
|
try {
|
|
4925
|
-
|
|
4926
|
-
coordRoot
|
|
4927
|
-
|
|
4928
|
-
fromName,
|
|
4929
|
-
|
|
4911
|
+
sent = sendPeerMessage({
|
|
4912
|
+
coordRoot,
|
|
4913
|
+
target: { name, instanceId: opts.instanceId, requireLive: opts.requireLive },
|
|
4914
|
+
senderName: fromName,
|
|
4915
|
+
senderInstanceId: myOwner,
|
|
4930
4916
|
body: message.trim(),
|
|
4931
|
-
journaled: Boolean(peerOwner),
|
|
4932
4917
|
});
|
|
4933
4918
|
} catch (error) {
|
|
4934
|
-
if (error instanceof
|
|
4935
|
-
emit.error({ code: `
|
|
4919
|
+
if (error instanceof HarneryInboxCapacityError) {
|
|
4920
|
+
emit.error({ code: `inbox_${error.reason_code}`, message: error.message });
|
|
4936
4921
|
process.exit(1);
|
|
4937
4922
|
}
|
|
4938
4923
|
throw error;
|
|
4939
4924
|
}
|
|
4925
|
+
if (!sent.ok) {
|
|
4926
|
+
emit.error({
|
|
4927
|
+
code: sent.code,
|
|
4928
|
+
message: sent.message,
|
|
4929
|
+
...(sent.candidates ? { candidates: sent.candidates } : {}),
|
|
4930
|
+
});
|
|
4931
|
+
process.exit(1);
|
|
4932
|
+
}
|
|
4940
4933
|
|
|
4941
4934
|
// Canonical delivery record: sender is the envelope owner, recipient rides
|
|
4942
4935
|
// in data, so read-only observers can render the communication without
|
|
@@ -4948,21 +4941,20 @@ function runPing(name: string, message: string, opts: { json?: boolean }): void
|
|
|
4948
4941
|
observation: {
|
|
4949
4942
|
event_type: "coord.message_observed",
|
|
4950
4943
|
direction: "sent",
|
|
4951
|
-
subject:
|
|
4944
|
+
subject: sent.target.instance_id,
|
|
4945
|
+
message_id: sent.message_id,
|
|
4952
4946
|
body: message.trim(),
|
|
4953
4947
|
},
|
|
4954
4948
|
});
|
|
4955
4949
|
|
|
4956
4950
|
const data = {
|
|
4957
|
-
peer:
|
|
4958
|
-
peer_instance_id:
|
|
4959
|
-
|
|
4960
|
-
message_id:
|
|
4961
|
-
|
|
4951
|
+
peer: sent.target.name,
|
|
4952
|
+
peer_instance_id: sent.target.instance_id,
|
|
4953
|
+
state: sent.state,
|
|
4954
|
+
message_id: sent.message_id,
|
|
4955
|
+
synced: sent.synced,
|
|
4956
|
+
body_bytes: sent.body_bytes,
|
|
4962
4957
|
from: fromName,
|
|
4963
|
-
body,
|
|
4964
|
-
journal_path: doc?.path ?? null,
|
|
4965
|
-
journal_bytes: doc?.bytes ?? null,
|
|
4966
4958
|
};
|
|
4967
4959
|
|
|
4968
4960
|
if (opts.json) {
|
|
@@ -4972,9 +4964,8 @@ function runPing(name: string, message: string, opts: { json?: boolean }): void
|
|
|
4972
4964
|
}
|
|
4973
4965
|
emit.data(data);
|
|
4974
4966
|
emit.text(
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
: `queued for agent-${data.peer} (no live session; delivered when a session of that name next starts): "${truncate(message.trim(), 80)}"\n`,
|
|
4967
|
+
`${data.state} for agent-${data.peer} (${data.peer_instance_id}); ` +
|
|
4968
|
+
`stored as ${data.message_id}, pending until surfaced to that task.\n`,
|
|
4978
4969
|
);
|
|
4979
4970
|
}
|
|
4980
4971
|
|