harnery 0.42.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/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 +95 -52
- 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/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/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 +19 -17
- 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 +64 -43
- 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/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/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/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/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 +120 -23
- 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/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/semantic/contract.d.ts +5 -5
- 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/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/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/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/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 +8 -1
- package/src/commander.ts +32 -0
- package/src/commands/agents.ts +151 -74
- 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/tunnel.ts +394 -116
- package/src/commands/web-fetch.ts +30 -19
- package/src/commands/web.ts +9 -3
- 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 +35 -20
- package/src/core/agents/render/session-context.ts +92 -48
- 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/names.ts +117 -6
- 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/events/v3/support-storage/pack-reader.ts +45 -19
- 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 +139 -22
- 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/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/storage/process-log.ts +17 -2
- 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/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
- package/src/lib/zip/index.ts +483 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Shared, deliberately small shell lexer for PreToolUse command guards. */
|
|
2
|
+
|
|
3
|
+
export interface Token {
|
|
4
|
+
value: string;
|
|
5
|
+
quoted: boolean;
|
|
6
|
+
operator?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Keep quoted arguments opaque, including examples and heredoc bodies. */
|
|
10
|
+
export function tokenize(source: string): Token[] {
|
|
11
|
+
const out: Token[] = [];
|
|
12
|
+
const heredocs: { end: string; tabs: boolean }[] = [];
|
|
13
|
+
for (let i = 0; i < source.length; ) {
|
|
14
|
+
const ch = source[i]!;
|
|
15
|
+
if (ch === "\\" && source[i + 1] === "\n") {
|
|
16
|
+
i += 2;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (ch === "\n") {
|
|
20
|
+
out.push({ value: ";", quoted: false, operator: true });
|
|
21
|
+
i++;
|
|
22
|
+
for (const doc of heredocs.splice(0)) {
|
|
23
|
+
while (i < source.length) {
|
|
24
|
+
const end = source.indexOf("\n", i);
|
|
25
|
+
const stop = end === -1 ? source.length : end;
|
|
26
|
+
const line = source.slice(i, stop);
|
|
27
|
+
i = end === -1 ? source.length : end + 1;
|
|
28
|
+
if ((doc.tabs ? line.replace(/^\t+/, "") : line) === doc.end) break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (/\s/.test(ch)) {
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (ch === "#") {
|
|
38
|
+
const end = source.indexOf("\n", i);
|
|
39
|
+
i = end === -1 ? source.length : end;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (";|&(){}<>".includes(ch)) {
|
|
43
|
+
const op = source.slice(i).match(/^(?:<<-?|>>|&&|\|\||[;|&(){}<>])/)![0];
|
|
44
|
+
out.push({ value: op, quoted: false, operator: true });
|
|
45
|
+
i += op.length;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
let value = "";
|
|
49
|
+
let quoted = false;
|
|
50
|
+
while (i < source.length && !/[\s;|&(){}<>]/.test(source[i]!)) {
|
|
51
|
+
const next = source[i++]!;
|
|
52
|
+
if (next === "'" || next === '"' || next === "`") {
|
|
53
|
+
quoted = true;
|
|
54
|
+
while (i < source.length && source[i] !== next) {
|
|
55
|
+
if (source[i] === "\\" && next !== "'" && i + 1 < source.length) i++;
|
|
56
|
+
value += source[i++]!;
|
|
57
|
+
}
|
|
58
|
+
if (source[i] === next) i++;
|
|
59
|
+
} else if (next === "\\" && i < source.length) {
|
|
60
|
+
const escaped = source[i++]!;
|
|
61
|
+
if (escaped !== "\n") value += escaped;
|
|
62
|
+
} else {
|
|
63
|
+
value += next;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const previous = out.at(-1)?.value;
|
|
67
|
+
if (previous === "<<" || previous === "<<-") {
|
|
68
|
+
heredocs.push({ end: value, tabs: previous === "<<-" });
|
|
69
|
+
}
|
|
70
|
+
out.push({ value, quoted });
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function basename(value: string): string {
|
|
76
|
+
return value.split(/[\\/]/).at(-1) ?? value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The command string of a Bash-style tool call, or null for any other tool. */
|
|
80
|
+
export function shellCommandFromTool(toolName: unknown, toolInput: unknown): string | null {
|
|
81
|
+
if (typeof toolName !== "string") return null;
|
|
82
|
+
const name = toolName.toLowerCase().replace(/^functions\./, "");
|
|
83
|
+
if (!["bash", "shell", "shell_command", "exec_command"].includes(name)) return null;
|
|
84
|
+
let input = toolInput;
|
|
85
|
+
if (typeof input === "string") {
|
|
86
|
+
try {
|
|
87
|
+
input = JSON.parse(input);
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!input || typeof input !== "object") return null;
|
|
93
|
+
const args = input as Record<string, unknown>;
|
|
94
|
+
const command = args.command ?? args.cmd;
|
|
95
|
+
return typeof command === "string" ? command : null;
|
|
96
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { basename, shellCommandFromTool, tokenize } from "./shell-tokens.ts";
|
|
2
|
+
|
|
1
3
|
/** Recognize inline waiters; this is not a proof of arbitrary shell termination. */
|
|
2
4
|
const REASON =
|
|
3
5
|
"Inline shell waiter blocked: polling loops and delayed log-only checks can outlive the job " +
|
|
@@ -6,82 +8,6 @@ const REASON =
|
|
|
6
8
|
"nonzero timeout exit, and producer-exit/failure detection. Do not create another waiter " +
|
|
7
9
|
"to check progress or infer completion from a broad pgrep -f pattern.";
|
|
8
10
|
|
|
9
|
-
interface Token {
|
|
10
|
-
value: string;
|
|
11
|
-
quoted: boolean;
|
|
12
|
-
operator?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Keep quoted arguments opaque, including examples and heredoc bodies. */
|
|
16
|
-
function tokenize(source: string): Token[] {
|
|
17
|
-
const out: Token[] = [];
|
|
18
|
-
const heredocs: { end: string; tabs: boolean }[] = [];
|
|
19
|
-
for (let i = 0; i < source.length; ) {
|
|
20
|
-
const ch = source[i]!;
|
|
21
|
-
if (ch === "\\" && source[i + 1] === "\n") {
|
|
22
|
-
i += 2;
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
if (ch === "\n") {
|
|
26
|
-
out.push({ value: ";", quoted: false, operator: true });
|
|
27
|
-
i++;
|
|
28
|
-
for (const doc of heredocs.splice(0)) {
|
|
29
|
-
while (i < source.length) {
|
|
30
|
-
const end = source.indexOf("\n", i);
|
|
31
|
-
const stop = end === -1 ? source.length : end;
|
|
32
|
-
const line = source.slice(i, stop);
|
|
33
|
-
i = end === -1 ? source.length : end + 1;
|
|
34
|
-
if ((doc.tabs ? line.replace(/^\t+/, "") : line) === doc.end) break;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (/\s/.test(ch)) {
|
|
40
|
-
i++;
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (ch === "#") {
|
|
44
|
-
const end = source.indexOf("\n", i);
|
|
45
|
-
i = end === -1 ? source.length : end;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (";|&(){}<>".includes(ch)) {
|
|
49
|
-
const op = source.slice(i).match(/^(?:<<-?|>>|&&|\|\||[;|&(){}<>])/)![0];
|
|
50
|
-
out.push({ value: op, quoted: false, operator: true });
|
|
51
|
-
i += op.length;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
let value = "";
|
|
55
|
-
let quoted = false;
|
|
56
|
-
while (i < source.length && !/[\s;|&(){}<>]/.test(source[i]!)) {
|
|
57
|
-
const next = source[i++]!;
|
|
58
|
-
if (next === "'" || next === '"' || next === "`") {
|
|
59
|
-
quoted = true;
|
|
60
|
-
while (i < source.length && source[i] !== next) {
|
|
61
|
-
if (source[i] === "\\" && next !== "'" && i + 1 < source.length) i++;
|
|
62
|
-
value += source[i++]!;
|
|
63
|
-
}
|
|
64
|
-
if (source[i] === next) i++;
|
|
65
|
-
} else if (next === "\\" && i < source.length) {
|
|
66
|
-
const escaped = source[i++]!;
|
|
67
|
-
if (escaped !== "\n") value += escaped;
|
|
68
|
-
} else {
|
|
69
|
-
value += next;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const previous = out.at(-1)?.value;
|
|
73
|
-
if (previous === "<<" || previous === "<<-") {
|
|
74
|
-
heredocs.push({ end: value, tabs: previous === "<<-" });
|
|
75
|
-
}
|
|
76
|
-
out.push({ value, quoted });
|
|
77
|
-
}
|
|
78
|
-
return out;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function basename(value: string): string {
|
|
82
|
-
return value.split(/[\\/]/).at(-1) ?? value;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
11
|
function isWaiter(source: string, depth = 0): boolean {
|
|
86
12
|
if (depth > 8) return false;
|
|
87
13
|
const tokens = tokenize(source);
|
|
@@ -148,19 +74,6 @@ function isWaiter(source: string, depth = 0): boolean {
|
|
|
148
74
|
}
|
|
149
75
|
|
|
150
76
|
export function shellWaiterReason(toolName: unknown, toolInput: unknown): string | null {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (!["bash", "shell", "shell_command", "exec_command"].includes(name)) return null;
|
|
154
|
-
let input = toolInput;
|
|
155
|
-
if (typeof input === "string") {
|
|
156
|
-
try {
|
|
157
|
-
input = JSON.parse(input);
|
|
158
|
-
} catch {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (!input || typeof input !== "object") return null;
|
|
163
|
-
const args = input as Record<string, unknown>;
|
|
164
|
-
const command = args.command ?? args.cmd;
|
|
165
|
-
return typeof command === "string" && isWaiter(command) ? REASON : null;
|
|
77
|
+
const command = shellCommandFromTool(toolName, toolInput);
|
|
78
|
+
return command !== null && isWaiter(command) ? REASON : null;
|
|
166
79
|
}
|
|
@@ -19,7 +19,8 @@ export interface HarneryInboxMessageRecordV1 {
|
|
|
19
19
|
schema: typeof HARNERY_INBOX_RECORD_SCHEMA;
|
|
20
20
|
kind: "message";
|
|
21
21
|
message_id: string;
|
|
22
|
-
|
|
22
|
+
/** A required null identifies an operator; strings identify real agent instances. */
|
|
23
|
+
sender_instance_id: string | null;
|
|
23
24
|
sender_display_name: string;
|
|
24
25
|
recipient_instance_id: string;
|
|
25
26
|
recipient_display_name: string;
|
package/src/core/inbox/index.ts
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { resolveBinName } from "../config.ts";
|
|
2
|
+
import type { HarneryInboxLimits, HarneryInboxMessageRecordV1 } from "./contract.ts";
|
|
3
|
+
import { HarneryInboxService } from "./service.ts";
|
|
4
|
+
import { type MessageTargetInput, resolveMessageTarget } from "./targets.ts";
|
|
5
|
+
|
|
6
|
+
const DAY = 24 * 60 * 60_000;
|
|
7
|
+
|
|
8
|
+
/** Preserve the existing ping body/count bounds. Pending messages have no TTL;
|
|
9
|
+
* the lifecycle planner protects them through the idle finalization window. */
|
|
10
|
+
export const PEER_INBOX_LIMITS: HarneryInboxLimits = {
|
|
11
|
+
max_message_body_bytes: 4_000,
|
|
12
|
+
max_pending_count: 25,
|
|
13
|
+
max_pending_bytes: 100_000,
|
|
14
|
+
max_history_bytes: 1024 * 1024,
|
|
15
|
+
max_history_records: 10_000,
|
|
16
|
+
warning_pressure_ratio: 0.8,
|
|
17
|
+
max_surface_count: 10,
|
|
18
|
+
max_surface_bytes: 16_000,
|
|
19
|
+
max_surface_tokens: 4_000,
|
|
20
|
+
surfaced_grace_ms: 7 * DAY,
|
|
21
|
+
terminal_grace_ms: 7 * DAY,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function peerInbox(coordRoot: string): HarneryInboxService {
|
|
25
|
+
return new HarneryInboxService({ coord_root: coordRoot, limits: PEER_INBOX_LIMITS });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** CLI and web use this one path. A receipt proves a synced append, never that
|
|
29
|
+
* a process is awake or that the recipient has noticed the message. */
|
|
30
|
+
export function sendPeerMessage(input: {
|
|
31
|
+
coordRoot: string;
|
|
32
|
+
target: MessageTargetInput;
|
|
33
|
+
senderInstanceId: string | null;
|
|
34
|
+
senderName: string;
|
|
35
|
+
body: string;
|
|
36
|
+
}) {
|
|
37
|
+
const target = resolveMessageTarget(input.coordRoot, input.target);
|
|
38
|
+
if (!target.ok) return target;
|
|
39
|
+
const receipt = peerInbox(input.coordRoot).send({
|
|
40
|
+
sender_instance_id: input.senderInstanceId,
|
|
41
|
+
sender_display_name: input.senderName,
|
|
42
|
+
recipient_instance_id: target.target.instance_id,
|
|
43
|
+
recipient_display_name: target.target.name,
|
|
44
|
+
body: input.body.trim(),
|
|
45
|
+
});
|
|
46
|
+
return { ok: true as const, target: target.target, ...receipt, state: target.target.delivery };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatPeerInbox(
|
|
50
|
+
coordRoot: string,
|
|
51
|
+
messages: readonly HarneryInboxMessageRecordV1[],
|
|
52
|
+
): string {
|
|
53
|
+
if (messages.length === 0) return "";
|
|
54
|
+
const onlyAgents = messages.every(({ sender_instance_id }) => sender_instance_id !== null);
|
|
55
|
+
const header = onlyAgents
|
|
56
|
+
? messages.length === 1
|
|
57
|
+
? "Message from a peer agent:"
|
|
58
|
+
: `${messages.length} messages from peer agents:`
|
|
59
|
+
: messages.length === 1
|
|
60
|
+
? "Message for this task:"
|
|
61
|
+
: `${messages.length} messages for this task:`;
|
|
62
|
+
const body = messages
|
|
63
|
+
.map(
|
|
64
|
+
(message) =>
|
|
65
|
+
` ${message.message_id} from ${message.sender_instance_id === null ? "Operator" : `agent-${message.sender_display_name}`} (${message.created_at}):\n` +
|
|
66
|
+
message.body
|
|
67
|
+
.split("\n")
|
|
68
|
+
.map((line) => ` ${line}`)
|
|
69
|
+
.join("\n"),
|
|
70
|
+
)
|
|
71
|
+
.join("\n\n");
|
|
72
|
+
const replyTarget = messages.find(({ sender_instance_id }) => sender_instance_id !== null);
|
|
73
|
+
return (
|
|
74
|
+
`${header}\n\n${body}\n\nMessages are bound to this task instance.` +
|
|
75
|
+
(replyTarget
|
|
76
|
+
? ` Reply with \`${resolveBinName(coordRoot)} agents ping ${replyTarget.sender_display_name} ` +
|
|
77
|
+
`--instance-id ${replyTarget.sender_instance_id} "<your reply>"\`.`
|
|
78
|
+
: "")
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Wait for every earlier stdout write to finish before acknowledging context.
|
|
83
|
+
* If output fails, the caller throws and leaves the batch pending. */
|
|
84
|
+
export async function acknowledgeEmittedPeerInbox(
|
|
85
|
+
coordRoot: string,
|
|
86
|
+
instanceId: string,
|
|
87
|
+
messageIds: readonly string[],
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
if (messageIds.length === 0) return;
|
|
90
|
+
await new Promise<void>((resolve, reject) => {
|
|
91
|
+
process.stdout.write("", (error) => (error ? reject(error) : resolve()));
|
|
92
|
+
});
|
|
93
|
+
peerInbox(coordRoot).acknowledgeSurface(instanceId, messageIds);
|
|
94
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, statSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type CrashSafeJsonlUpdate,
|
|
6
|
+
type DurableHistoryReadOptions,
|
|
7
|
+
readCrashSafeJsonlFile,
|
|
8
|
+
updateCrashSafeJsonlFile,
|
|
9
|
+
} from "../storage/durable-history.ts";
|
|
5
10
|
import {
|
|
6
11
|
HARNERY_INBOX_RECORD_SCHEMA,
|
|
7
12
|
HARNERY_INBOX_STATUS_SCHEMA,
|
|
@@ -22,7 +27,7 @@ export interface HarneryInboxServiceOptions {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export interface HarneryInboxSendInput {
|
|
25
|
-
sender_instance_id: string;
|
|
30
|
+
sender_instance_id: string | null;
|
|
26
31
|
sender_display_name: string;
|
|
27
32
|
recipient_instance_id: string;
|
|
28
33
|
recipient_display_name: string;
|
|
@@ -59,7 +64,7 @@ export class HarneryInboxService {
|
|
|
59
64
|
schema: HARNERY_INBOX_RECORD_SCHEMA,
|
|
60
65
|
kind: "message",
|
|
61
66
|
message_id: `msg_${this.#options.id?.() ?? randomUUID()}`,
|
|
62
|
-
sender_instance_id:
|
|
67
|
+
sender_instance_id: senderId(input.sender_instance_id),
|
|
63
68
|
sender_display_name: boundedDisplay(input.sender_display_name),
|
|
64
69
|
recipient_instance_id: recipient,
|
|
65
70
|
recipient_display_name: boundedDisplay(input.recipient_display_name),
|
|
@@ -68,7 +73,7 @@ export class HarneryInboxService {
|
|
|
68
73
|
body_bytes: bodyBytes,
|
|
69
74
|
};
|
|
70
75
|
const path = inboxPath(this.#options.coord_root, recipient);
|
|
71
|
-
return
|
|
76
|
+
return updateInboxHistory<HarneryInboxRecordV1, HarneryInboxSendReceipt>(
|
|
72
77
|
path,
|
|
73
78
|
readOptions(limits),
|
|
74
79
|
(records) => {
|
|
@@ -88,10 +93,12 @@ export class HarneryInboxService {
|
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
records(recipientInstanceId: string): HarneryInboxRecordV1[] {
|
|
91
|
-
|
|
96
|
+
const records = readCrashSafeJsonlFile<HarneryInboxRecordV1>(
|
|
92
97
|
inboxPath(this.#options.coord_root, opaqueId(recipientInstanceId, "recipient instance")),
|
|
93
98
|
readOptions(this.#options.limits),
|
|
94
99
|
);
|
|
100
|
+
for (const record of records) validateRecord(record);
|
|
101
|
+
return records;
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
pending(recipientInstanceId: string): HarneryInboxMessageRecordV1[] {
|
|
@@ -133,13 +140,45 @@ export class HarneryInboxService {
|
|
|
133
140
|
max_tokens: this.#options.limits.max_surface_tokens,
|
|
134
141
|
},
|
|
135
142
|
): Promise<HarneryInboxSurfaceResult> {
|
|
136
|
-
validateSurfaceBudget(budget);
|
|
137
143
|
const recipient = opaqueId(recipientInstanceId, "recipient instance");
|
|
138
|
-
const selected =
|
|
144
|
+
const selected = this.prepareSurface(recipient, budget);
|
|
139
145
|
await emit(selected);
|
|
140
|
-
|
|
146
|
+
this.acknowledgeSurface(
|
|
147
|
+
recipient,
|
|
148
|
+
selected.map(({ message_id }) => message_id),
|
|
149
|
+
);
|
|
150
|
+
const remaining = this.pending(recipient);
|
|
151
|
+
return {
|
|
152
|
+
emitted: selected,
|
|
153
|
+
remaining_pending_count: remaining.length,
|
|
154
|
+
remaining_pending_bytes: remaining.reduce((sum, record) => sum + record.body_bytes, 0),
|
|
155
|
+
repeated_after_crash_possible: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Preparing prompt text is read-only. A failed or abandoned emission must
|
|
160
|
+
* leave the same messages pending for the recipient's next prompt. */
|
|
161
|
+
prepareSurface(
|
|
162
|
+
recipientInstanceId: string,
|
|
163
|
+
budget: HarneryInboxSurfaceBudget = {
|
|
164
|
+
max_count: this.#options.limits.max_surface_count,
|
|
165
|
+
max_bytes: this.#options.limits.max_surface_bytes,
|
|
166
|
+
max_tokens: this.#options.limits.max_surface_tokens,
|
|
167
|
+
},
|
|
168
|
+
): HarneryInboxMessageRecordV1[] {
|
|
169
|
+
validateSurfaceBudget(budget);
|
|
170
|
+
return selectOldestFitting(this.pending(recipientInstanceId), budget);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Call only after the selected IDs have actually been emitted. A message
|
|
174
|
+
* appended while output is in flight remains pending rather than being
|
|
175
|
+
* included in an acknowledgement of an older batch. */
|
|
176
|
+
acknowledgeSurface(recipientInstanceId: string, messageIds: readonly string[]): void {
|
|
177
|
+
const recipient = opaqueId(recipientInstanceId, "recipient instance");
|
|
178
|
+
const selected = new Set(messageIds);
|
|
179
|
+
if (selected.size > 0) {
|
|
141
180
|
const surfacedAt = now(this.#options).toISOString();
|
|
142
|
-
|
|
181
|
+
updateInboxHistory<HarneryInboxRecordV1, void>(
|
|
143
182
|
inboxPath(this.#options.coord_root, recipient),
|
|
144
183
|
readOptions(this.#options.limits),
|
|
145
184
|
(records) => {
|
|
@@ -147,9 +186,9 @@ export class HarneryInboxService {
|
|
|
147
186
|
project(records).pending.map(({ message_id }) => message_id),
|
|
148
187
|
);
|
|
149
188
|
return {
|
|
150
|
-
append: selected
|
|
151
|
-
.filter((
|
|
152
|
-
.map((
|
|
189
|
+
append: [...selected]
|
|
190
|
+
.filter((message_id) => stillPending.has(message_id))
|
|
191
|
+
.map((message_id) => ({
|
|
153
192
|
schema: HARNERY_INBOX_RECORD_SCHEMA,
|
|
154
193
|
kind: "surfaced" as const,
|
|
155
194
|
message_id,
|
|
@@ -160,13 +199,32 @@ export class HarneryInboxService {
|
|
|
160
199
|
},
|
|
161
200
|
);
|
|
162
201
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The shared writer refuses an occupied lease before invoking the update.
|
|
206
|
+
* Briefly retry only that exact refusal so concurrent peer senders both get
|
|
207
|
+
* a chance to append. All validation, authority and I/O failures still fail
|
|
208
|
+
* immediately, and an occupied live lease is never removed or overridden. */
|
|
209
|
+
function updateInboxHistory<T, R>(
|
|
210
|
+
path: string,
|
|
211
|
+
options: DurableHistoryReadOptions,
|
|
212
|
+
update: (records: readonly T[]) => CrashSafeJsonlUpdate<R>,
|
|
213
|
+
): R {
|
|
214
|
+
const deadline = performance.now() + 2_000;
|
|
215
|
+
const pause = new Int32Array(new SharedArrayBuffer(4));
|
|
216
|
+
while (true) {
|
|
217
|
+
try {
|
|
218
|
+
return updateCrashSafeJsonlFile(path, options, update);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (
|
|
221
|
+
!(error instanceof Error) ||
|
|
222
|
+
error.message !== `durable history lease busy: ${path}.lease` ||
|
|
223
|
+
performance.now() >= deadline
|
|
224
|
+
)
|
|
225
|
+
throw error;
|
|
226
|
+
Atomics.wait(pause, 0, 0, Math.min(10, deadline - performance.now()));
|
|
227
|
+
}
|
|
170
228
|
}
|
|
171
229
|
}
|
|
172
230
|
|
|
@@ -270,10 +328,15 @@ function now(options: HarneryInboxServiceOptions): Date {
|
|
|
270
328
|
}
|
|
271
329
|
|
|
272
330
|
function opaqueId(value: string, field: string): string {
|
|
273
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value))
|
|
331
|
+
if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value))
|
|
332
|
+
throw new Error(`invalid ${field} id`);
|
|
274
333
|
return value;
|
|
275
334
|
}
|
|
276
335
|
|
|
336
|
+
function senderId(value: string | null): string | null {
|
|
337
|
+
return value === null ? null : opaqueId(value, "sender instance");
|
|
338
|
+
}
|
|
339
|
+
|
|
277
340
|
function boundedDisplay(value: string): string {
|
|
278
341
|
const result = value.trim();
|
|
279
342
|
if (result.length === 0 || Buffer.byteLength(result) > 256)
|
|
@@ -283,8 +346,11 @@ function boundedDisplay(value: string): string {
|
|
|
283
346
|
|
|
284
347
|
function validateRecord(record: HarneryInboxRecordV1): void {
|
|
285
348
|
if (record.schema !== HARNERY_INBOX_RECORD_SCHEMA) throw new Error("unknown inbox record schema");
|
|
286
|
-
if (record.kind === "message"
|
|
287
|
-
|
|
349
|
+
if (record.kind === "message") {
|
|
350
|
+
senderId(record.sender_instance_id);
|
|
351
|
+
if (Buffer.byteLength(record.body) !== record.body_bytes) {
|
|
352
|
+
throw new Error(`inbox message ${record.message_id} has invalid body_bytes`);
|
|
353
|
+
}
|
|
288
354
|
}
|
|
289
355
|
}
|
|
290
356
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { resolveName } from "../agents/state/names.ts";
|
|
2
|
+
import { coordFreshnessSeconds } from "../config.ts";
|
|
3
|
+
import {
|
|
4
|
+
type CoordinationGenerationViewV3,
|
|
5
|
+
type CoordinationViewV3,
|
|
6
|
+
readCoordinationViewV3,
|
|
7
|
+
requireAuthoritySafeCoordinationViewV3,
|
|
8
|
+
} from "../events/v3/coordination-view.ts";
|
|
9
|
+
import {
|
|
10
|
+
liveInstanceIdV3,
|
|
11
|
+
nativeInstanceIdV3,
|
|
12
|
+
observeLiveEventLedgerRouteV3,
|
|
13
|
+
} from "../events/v3/live-route-observer.ts";
|
|
14
|
+
|
|
15
|
+
export interface MessageTargetInput {
|
|
16
|
+
name?: string;
|
|
17
|
+
instanceId?: string;
|
|
18
|
+
requireLive?: boolean;
|
|
19
|
+
now?: Date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type MessageTargetResult =
|
|
23
|
+
| {
|
|
24
|
+
ok: true;
|
|
25
|
+
target: { instance_id: string; name: string; delivery: "written-live" | "queued" };
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
ok: false;
|
|
29
|
+
code: "no_peer" | "ambiguous_peer" | "not_live" | "authority_unsafe";
|
|
30
|
+
message: string;
|
|
31
|
+
candidates?: string[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Resolve one immutable recipient without extending presence or claiming attention. */
|
|
35
|
+
export function resolveMessageTarget(
|
|
36
|
+
coordRoot: string,
|
|
37
|
+
input: MessageTargetInput,
|
|
38
|
+
): MessageTargetResult {
|
|
39
|
+
const requestedName = normalizeName(input.name);
|
|
40
|
+
const requestedInstance = input.instanceId?.trim();
|
|
41
|
+
if (!requestedName && !requestedInstance) {
|
|
42
|
+
return { ok: false, code: "no_peer", message: "Supply an exact agent name or instance id." };
|
|
43
|
+
}
|
|
44
|
+
const now = input.now?.getTime() ?? Date.now();
|
|
45
|
+
if (!Number.isFinite(now)) return unsafe("The message addressing clock is invalid.");
|
|
46
|
+
|
|
47
|
+
let view: CoordinationViewV3;
|
|
48
|
+
try {
|
|
49
|
+
if (observeLiveEventLedgerRouteV3(coordRoot).state === "blocked") {
|
|
50
|
+
return unsafe("The event ledger cannot establish message recipient authority.");
|
|
51
|
+
}
|
|
52
|
+
view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(coordRoot));
|
|
53
|
+
if (!view.source_complete) return unsafe("The event ledger read is incomplete.");
|
|
54
|
+
} catch {
|
|
55
|
+
return unsafe("The event ledger cannot establish message recipient authority.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Address historical instances without reopening them. A current generation
|
|
59
|
+
// wins over its own terminal history; otherwise use its last ledger position.
|
|
60
|
+
const byInstance = new Map<string, CoordinationGenerationViewV3>();
|
|
61
|
+
for (const generation of Object.values(view.terminal_generations)) {
|
|
62
|
+
const prior = byInstance.get(generation.instance_id);
|
|
63
|
+
if (!prior || laterPosition(generation, prior))
|
|
64
|
+
byInstance.set(generation.instance_id, generation);
|
|
65
|
+
}
|
|
66
|
+
for (const generation of Object.values(view.instances))
|
|
67
|
+
byInstance.set(generation.instance_id, generation);
|
|
68
|
+
const freshnessMs = coordFreshnessSeconds(coordRoot) * 1_000;
|
|
69
|
+
const candidates = [...byInstance.values()].flatMap((generation) => {
|
|
70
|
+
const instanceId = nativeInstanceIdV3(generation.instance_id);
|
|
71
|
+
const name = resolveName(coordRoot, instanceId)?.name;
|
|
72
|
+
if (!name) return [];
|
|
73
|
+
if (requestedInstance && liveInstanceIdV3(requestedInstance) !== generation.instance_id)
|
|
74
|
+
return [];
|
|
75
|
+
if (requestedName && normalizeName(name) !== requestedName) return [];
|
|
76
|
+
const age = now - Date.parse(generation.last_observed_at);
|
|
77
|
+
const diagnostics = view.diagnostics_by_generation[generation.generation_id] ?? [];
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
instanceId,
|
|
81
|
+
name,
|
|
82
|
+
safe: !diagnostics.some((diagnostic) => diagnostic.authority_blocking),
|
|
83
|
+
fresh:
|
|
84
|
+
generation.phase === "live" && Number.isFinite(age) && age >= 0 && age <= freshnessMs,
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (candidates.length === 0) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
code: "no_peer",
|
|
93
|
+
message: "No known agent matches that exact name and instance.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (candidates.some((candidate) => !candidate.safe)) {
|
|
97
|
+
return unsafe("A matching agent generation does not have safe coordination authority.");
|
|
98
|
+
}
|
|
99
|
+
const fresh = candidates.filter((candidate) => candidate.fresh);
|
|
100
|
+
const selected = requestedInstance ? candidates : fresh.length > 0 ? fresh : candidates;
|
|
101
|
+
if (selected.length !== 1) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
code: "ambiguous_peer",
|
|
105
|
+
message: "Several agent instances hold that exact name; specify --instance-id.",
|
|
106
|
+
candidates: selected.map(({ instanceId }) => instanceId).sort(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const target = selected[0]!;
|
|
110
|
+
if (input.requireLive && !target.fresh) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
code: "not_live",
|
|
114
|
+
message:
|
|
115
|
+
"That agent is known but outside live presence; omit --require-live to queue its message.",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
target: {
|
|
121
|
+
instance_id: target.instanceId,
|
|
122
|
+
name: target.name,
|
|
123
|
+
delivery: target.fresh ? "written-live" : "queued",
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function laterPosition(
|
|
129
|
+
left: CoordinationGenerationViewV3,
|
|
130
|
+
right: CoordinationGenerationViewV3,
|
|
131
|
+
): boolean {
|
|
132
|
+
return (
|
|
133
|
+
left.source_position.segment_ordinal > right.source_position.segment_ordinal ||
|
|
134
|
+
(left.source_position.segment_ordinal === right.source_position.segment_ordinal &&
|
|
135
|
+
left.source_position.byte_offset > right.source_position.byte_offset)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeName(name: string | undefined): string {
|
|
140
|
+
return (name ?? "")
|
|
141
|
+
.trim()
|
|
142
|
+
.replace(/^agent-/i, "")
|
|
143
|
+
.toLowerCase();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function unsafe(message: string): MessageTargetResult {
|
|
147
|
+
return { ok: false, code: "authority_unsafe", message };
|
|
148
|
+
}
|
|
@@ -23,14 +23,13 @@ export async function* watchInbox(
|
|
|
23
23
|
|
|
24
24
|
function delay(ms: number, signal: AbortSignal): Promise<void> {
|
|
25
25
|
return new Promise((resolve) => {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
"abort",
|
|
29
|
-
()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
26
|
+
const finish = () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
signal.removeEventListener("abort", finish);
|
|
29
|
+
resolve();
|
|
30
|
+
};
|
|
31
|
+
const timer = setTimeout(finish, ms);
|
|
32
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
33
|
+
if (signal.aborted) finish();
|
|
35
34
|
});
|
|
36
35
|
}
|