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
|
@@ -16,9 +16,11 @@ import type { EmitContext } from "../commander.ts";
|
|
|
16
16
|
* harnery repo at the *matching version tag* into a cache dir and install the
|
|
17
17
|
* web app's deps. Subsequent runs reuse the cache.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* `web/` imports readers from `src/`, and `web/next.config.ts` resolves the
|
|
20
|
+
* root `playwright` package, so the install covers the repository root and
|
|
21
|
+
* then `web/`. Installing the `playwright` package downloads no browsers.
|
|
22
|
+
* Installing `web/` alone left `harn web up` failing with
|
|
23
|
+
* `Cannot find module 'playwright/package.json'`.
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
const DEFAULT_REPO_URL = "https://github.com/ryanjkelly/harnery.git";
|
|
@@ -53,8 +55,8 @@ function webCacheRoot(ref: string): string {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/** bun if available (preferred), else npm. Both support `<runner> install` and `<runner> run <script>`. */
|
|
56
|
-
export function webRunner(): "bun" | "npm" {
|
|
57
|
-
const r =
|
|
58
|
+
export function webRunner(spawn: typeof spawnSync = spawnSync): "bun" | "npm" {
|
|
59
|
+
const r = spawn("bun", ["--version"], { stdio: "ignore" });
|
|
58
60
|
return r.status === 0 ? "bun" : "npm";
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -63,14 +65,20 @@ export function webRunner(): "bun" | "npm" {
|
|
|
63
65
|
* its path, fetching (clone + install) on first use. Returns null on failure
|
|
64
66
|
* (after emitting an actionable error).
|
|
65
67
|
*/
|
|
66
|
-
export function lazyFetchWebRoot(
|
|
68
|
+
export function lazyFetchWebRoot(
|
|
69
|
+
emit: EmitContext,
|
|
70
|
+
spawn: typeof spawnSync = spawnSync,
|
|
71
|
+
): string | null {
|
|
67
72
|
const { version, repoUrl } = readPkg();
|
|
68
73
|
const ref = resolveRef(version);
|
|
69
74
|
const cacheRoot = webCacheRoot(ref);
|
|
70
75
|
const webDir = path.join(cacheRoot, "web");
|
|
71
76
|
|
|
72
|
-
// node_modules in the cached web/ is the "fetched + installed" marker.
|
|
73
|
-
if (
|
|
77
|
+
// node_modules in both the cached root and web/ is the "fetched + installed" marker.
|
|
78
|
+
if (
|
|
79
|
+
existsSync(path.join(cacheRoot, "node_modules")) &&
|
|
80
|
+
existsSync(path.join(webDir, "node_modules"))
|
|
81
|
+
) {
|
|
74
82
|
emit.log(`harn web · using cached dashboard (${ref})`, "info");
|
|
75
83
|
return webDir;
|
|
76
84
|
}
|
|
@@ -85,7 +93,7 @@ export function lazyFetchWebRoot(emit: EmitContext): string | null {
|
|
|
85
93
|
if (existsSync(cacheRoot)) rmSync(cacheRoot, { recursive: true, force: true });
|
|
86
94
|
mkdirSync(path.dirname(cacheRoot), { recursive: true });
|
|
87
95
|
|
|
88
|
-
const clone =
|
|
96
|
+
const clone = spawn("git", ["clone", "--depth", "1", "--branch", ref, repoUrl, cacheRoot], {
|
|
89
97
|
stdio: "inherit",
|
|
90
98
|
});
|
|
91
99
|
if (clone.status !== 0) {
|
|
@@ -100,16 +108,19 @@ export function lazyFetchWebRoot(emit: EmitContext): string | null {
|
|
|
100
108
|
return null;
|
|
101
109
|
}
|
|
102
110
|
|
|
103
|
-
const runner = webRunner();
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
const runner = webRunner(spawn);
|
|
112
|
+
for (const dir of [cacheRoot, webDir]) {
|
|
113
|
+
const where = dir === webDir ? "web/" : "the harnery root";
|
|
114
|
+
emit.log(`harn web · installing dashboard deps (${runner} install in ${where})`, "info");
|
|
115
|
+
const install = spawn(runner, ["install"], { cwd: dir, stdio: "inherit" });
|
|
116
|
+
if (install.status !== 0) {
|
|
117
|
+
emit.error({
|
|
118
|
+
code: "web_fetch_failed",
|
|
119
|
+
message: `${runner} install in ${dir} failed (status ${install.status})`,
|
|
120
|
+
hint: `Try manually: cd ${dir} && ${runner} install`,
|
|
121
|
+
});
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
emit.log(`harn web · dashboard ready (cached at ${webDir})`, "info");
|
package/src/commands/web.ts
CHANGED
|
@@ -82,11 +82,17 @@ export function nodeOptionsWithHeapCap(mb: number): string | undefined {
|
|
|
82
82
|
return `${inherited} --max-old-space-size=${mb}`.trim();
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Preload the dashboard's server hooks (keep-alive and request diagnostics)
|
|
87
|
+
* without replacing Next's HTTP server. NODE_OPTIONS reaches the child that
|
|
88
|
+
* `next dev` forks, but Next re-parses every `--import` into one value, so
|
|
89
|
+
* this names the single combined preload; a second, different `--import`
|
|
90
|
+
* would collapse into an unloadable path (web/server-preload.mjs).
|
|
91
|
+
*/
|
|
86
92
|
export function nodeOptionsWithWebDiagnostics(webDir: string, mb: number): string {
|
|
87
93
|
const inherited = nodeOptionsWithHeapCap(mb) ?? "";
|
|
88
|
-
if (inherited.includes("server-
|
|
89
|
-
const preload = pathToFileURL(path.join(webDir, "server-
|
|
94
|
+
if (inherited.includes("server-preload.mjs")) return inherited;
|
|
95
|
+
const preload = pathToFileURL(path.join(webDir, "server-preload.mjs")).href;
|
|
90
96
|
return `${inherited} --import=${preload}`.trim();
|
|
91
97
|
}
|
|
92
98
|
|
package/src/commands/workflow.ts
CHANGED
|
@@ -618,11 +618,11 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
618
618
|
workflow
|
|
619
619
|
.command("reclaim <run-id>")
|
|
620
620
|
.description(
|
|
621
|
-
"
|
|
621
|
+
"Preview reclaim of an inactive owned workspace, or confirm salvage or discard and release.",
|
|
622
622
|
)
|
|
623
623
|
.option("--yes", "Confirm that this commits or discards uncommitted work, then releases")
|
|
624
624
|
.option("--discard", "Throw the uncommitted work away instead of salvaging it")
|
|
625
|
-
.option("--json", "Emit the reclaim
|
|
625
|
+
.option("--json", "Emit the reclaim preview or confirmed result as JSON")
|
|
626
626
|
.action(async (runId: string, opts: WorkflowReclaimOpts) => {
|
|
627
627
|
const coordRoot = resolveCoordRoot();
|
|
628
628
|
if (!coordRoot) {
|
|
@@ -632,53 +632,53 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
632
632
|
});
|
|
633
633
|
process.exit(1);
|
|
634
634
|
}
|
|
635
|
-
if (!opts.yes) {
|
|
636
|
-
emit.error({
|
|
637
|
-
code: "reclaim_confirmation_required",
|
|
638
|
-
message: opts.discard
|
|
639
|
-
? "reclaim --discard permanently destroys uncommitted work; pass --yes to confirm"
|
|
640
|
-
: "workspace reclaim commits uncommitted work to its branch and then releases the worktree; pass --yes to confirm",
|
|
641
|
-
});
|
|
642
|
-
process.exit(1);
|
|
643
|
-
}
|
|
644
635
|
try {
|
|
645
|
-
const {
|
|
636
|
+
const { createLocalGitWorktreeProvider, readWorkflowRunManifest } = await import(
|
|
646
637
|
"../core/workflow/index.ts"
|
|
647
638
|
);
|
|
648
|
-
const
|
|
649
|
-
if (
|
|
650
|
-
emit.error({ code: "workflow_reclaim_failed", message: inspection.error });
|
|
651
|
-
process.exit(1);
|
|
652
|
-
}
|
|
653
|
-
const allocation = inspection.value.allocation;
|
|
654
|
-
if (!allocation) {
|
|
639
|
+
const binding = readWorkflowRunManifest(coordRoot, runId).execution.workspace_binding;
|
|
640
|
+
if (binding?.provider.id !== "local-git-worktree") {
|
|
655
641
|
emit.error({
|
|
656
642
|
code: "workflow_reclaim_unavailable",
|
|
657
643
|
message: `run ${runId} has no provider-owned Git workspace to reclaim`,
|
|
658
644
|
});
|
|
659
645
|
process.exit(1);
|
|
660
646
|
}
|
|
661
|
-
const
|
|
662
|
-
|
|
647
|
+
const provider = createLocalGitWorktreeProvider({ coordRoot });
|
|
648
|
+
const result = await provider.reclaim(binding, {
|
|
663
649
|
mode: opts.discard ? "discard" : "salvage",
|
|
664
|
-
|
|
650
|
+
yes: opts.yes,
|
|
665
651
|
});
|
|
666
|
-
// Cleanup still owns removal. Reclaim only changes whether the tree it
|
|
667
|
-
// finds is dirty, so there is one audited release path, not two.
|
|
668
|
-
const provider = await builtInProviderForRun(coordRoot, runId);
|
|
669
|
-
const cleanup = await cleanupWorkspace({ coordRoot, runId, provider });
|
|
670
652
|
if (opts.json) {
|
|
671
653
|
emit.config({ format: "json" });
|
|
672
|
-
emit.data(
|
|
654
|
+
emit.data(result);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (result.action === "preview") {
|
|
658
|
+
const holders = result.lease_holder.map(
|
|
659
|
+
(holder) =>
|
|
660
|
+
`${holder.scope}: ${holder.owner_id ?? "unknown"}${holder.pid ? ` (pid ${holder.pid})` : ""}${holder.detail ? `; ${holder.detail}` : ""}`,
|
|
661
|
+
);
|
|
662
|
+
emit.text(
|
|
663
|
+
`reclaim preview: ${result.run_id}\n` +
|
|
664
|
+
`mode: ${result.mode}\nliveness: ${result.liveness}\nownership: ${result.ownership}\n` +
|
|
665
|
+
`lease holder: ${holders.length > 0 ? holders.join(", ") : "none observed"}\n` +
|
|
666
|
+
`dirty paths: ${result.dirty_paths === null ? "unknown" : result.dirty_paths.length > 0 ? result.dirty_paths.join(", ") : "none"}\n` +
|
|
667
|
+
(result.ownership_detail ? `ownership detail: ${result.ownership_detail}\n` : "") +
|
|
668
|
+
`Pass --yes to confirm ${result.mode} and release after all checks pass.\n`,
|
|
669
|
+
);
|
|
673
670
|
return;
|
|
674
671
|
}
|
|
675
672
|
emit.text(
|
|
676
|
-
`reclaim ${preparation.action}: ${preparation.detail}\n` +
|
|
677
|
-
`workspace cleanup ${cleanup.status}: ${cleanup.binding_id}\n`,
|
|
673
|
+
`reclaim ${result.preparation.action}: ${result.preparation.detail}\n` +
|
|
674
|
+
`workspace cleanup ${result.cleanup.status}: ${result.cleanup.binding_id}\n`,
|
|
678
675
|
);
|
|
679
676
|
} catch (error) {
|
|
680
677
|
emit.error({
|
|
681
|
-
code:
|
|
678
|
+
code:
|
|
679
|
+
error instanceof Error && "code" in error && typeof error.code === "string"
|
|
680
|
+
? error.code
|
|
681
|
+
: "workflow_reclaim_failed",
|
|
682
682
|
message: error instanceof Error ? error.message : String(error),
|
|
683
683
|
});
|
|
684
684
|
process.exit(1);
|
|
@@ -88,6 +88,7 @@ interface LiveAuthorityBaseV3 {
|
|
|
88
88
|
nativeSessionId: string;
|
|
89
89
|
adapter: Adapter;
|
|
90
90
|
observationId?: string;
|
|
91
|
+
expectedGenerationId?: `gen_${string}`;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
/**
|
|
@@ -648,6 +649,13 @@ function recordLiveAuthority<S extends CoordinationAuthoritySignalV3>(
|
|
|
648
649
|
platform: livePlatformV3(),
|
|
649
650
|
expected_prior_state_digest: coordinationAuthorityStateDigestV3(expected),
|
|
650
651
|
desired_state_digest: coordinationAuthorityStateDigestV3(desired),
|
|
652
|
+
expected_generation_id:
|
|
653
|
+
input.expectedGenerationId ??
|
|
654
|
+
((subject === input.owner
|
|
655
|
+
? expected.v3_generation_id
|
|
656
|
+
: readLiveCoordinationRow(input.coordRoot, input.owner)?.v3_generation_id) as
|
|
657
|
+
| `gen_${string}`
|
|
658
|
+
| undefined),
|
|
651
659
|
reconciler: {
|
|
652
660
|
readStateDigest: () => {
|
|
653
661
|
const heartbeat = readHeartbeat(input.coordRoot, subject);
|
|
@@ -681,6 +689,12 @@ export function liveCoordinationWriteModeV3(coordRoot: string): EventV3WriteMode
|
|
|
681
689
|
}
|
|
682
690
|
|
|
683
691
|
function requireHeartbeat(input: LiveAuthorityBaseV3, owner: string): Heartbeat {
|
|
692
|
+
if (
|
|
693
|
+
input.expectedGenerationId &&
|
|
694
|
+
readLiveCoordinationRow(input.coordRoot, owner)?.v3_generation_id !== input.expectedGenerationId
|
|
695
|
+
) {
|
|
696
|
+
throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
|
|
697
|
+
}
|
|
684
698
|
const heartbeat = ensureLiveCoordinationHeartbeat(
|
|
685
699
|
input.coordRoot,
|
|
686
700
|
owner,
|
|
@@ -688,6 +702,9 @@ function requireHeartbeat(input: LiveAuthorityBaseV3, owner: string): Heartbeat
|
|
|
688
702
|
input.adapter,
|
|
689
703
|
);
|
|
690
704
|
if (!heartbeat) throw new LiveCoordinationAuthorityV3Error(`heartbeat_missing:${owner}`);
|
|
705
|
+
if (input.expectedGenerationId && heartbeat.v3_generation_id !== input.expectedGenerationId) {
|
|
706
|
+
throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
|
|
707
|
+
}
|
|
691
708
|
return heartbeat;
|
|
692
709
|
}
|
|
693
710
|
|
|
@@ -5,6 +5,7 @@ import { normalizeNativeIdV3 } from "../events/v3/canonical.ts";
|
|
|
5
5
|
import type { EventV3 } from "../events/v3/contract.ts";
|
|
6
6
|
import { readEventV3ControlState } from "../events/v3/control.ts";
|
|
7
7
|
import { fingerprintContextV3 } from "../events/v3/fingerprint-keys.ts";
|
|
8
|
+
import { withGenerationCustodyV3 } from "../events/v3/generation-custody.ts";
|
|
8
9
|
import {
|
|
9
10
|
liveInstanceIdV3,
|
|
10
11
|
livePlatformV3,
|
|
@@ -25,17 +26,34 @@ interface LiveLifecycleObservationBaseV3 {
|
|
|
25
26
|
observedAt?: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
interface LiveSweepObservationV3Input extends LiveLifecycleObservationBaseV3 {
|
|
30
|
+
expectedGenerationId: `gen_${string}` | null;
|
|
31
|
+
observation: "stale_heartbeat" | "killed" | "unparseable_heartbeat" | "missing_timestamp";
|
|
32
|
+
ageMs: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
export function recordLiveSweepObservationV3(
|
|
29
|
-
input:
|
|
30
|
-
observation: "stale_heartbeat" | "killed" | "unparseable_heartbeat" | "missing_timestamp";
|
|
31
|
-
ageMs: number;
|
|
32
|
-
},
|
|
36
|
+
input: LiveSweepObservationV3Input,
|
|
33
37
|
): LiveLifecycleObservationV3Result {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
input = { ...input };
|
|
39
|
+
// Reject stale observations before preparing durable custody state. The
|
|
40
|
+
// in-custody checks cover replacement while acquiring custody and reentry.
|
|
41
|
+
requireSweepHookState(input);
|
|
42
|
+
return withGenerationCustodyV3(input.coordRoot, [liveInstanceIdV3(input.owner)], () => {
|
|
43
|
+
requireSweepHookState(input);
|
|
44
|
+
return recordObservation(
|
|
45
|
+
input,
|
|
46
|
+
"lifecycle.sweep_observed",
|
|
47
|
+
{
|
|
48
|
+
subject_instance_id: liveInstanceIdV3(input.owner),
|
|
49
|
+
observation: input.observation,
|
|
50
|
+
provisional: true,
|
|
51
|
+
age_ms: Math.max(0, Math.floor(input.ageMs)),
|
|
52
|
+
},
|
|
53
|
+
() => {
|
|
54
|
+
requireSweepHookState(input);
|
|
55
|
+
},
|
|
56
|
+
);
|
|
39
57
|
});
|
|
40
58
|
}
|
|
41
59
|
|
|
@@ -44,11 +62,16 @@ export function recordLiveResumeObservationV3(
|
|
|
44
62
|
): LiveLifecycleObservationV3Result {
|
|
45
63
|
const normalized = withAttestedAdapter(input);
|
|
46
64
|
const hook = requireHookState(normalized);
|
|
47
|
-
return recordObservation(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
return recordObservation(
|
|
66
|
+
normalized,
|
|
67
|
+
"session.resumed",
|
|
68
|
+
{
|
|
69
|
+
prior_generation_id: hook.generation_id,
|
|
70
|
+
continuity: "native",
|
|
71
|
+
evidence_reference: "heartbeat_recreated",
|
|
72
|
+
},
|
|
73
|
+
() => {},
|
|
74
|
+
);
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
function recordObservation<T extends "lifecycle.sweep_observed" | "session.resumed">(
|
|
@@ -66,6 +89,7 @@ function recordObservation<T extends "lifecycle.sweep_observed" | "session.resum
|
|
|
66
89
|
continuity: "native";
|
|
67
90
|
evidence_reference: string;
|
|
68
91
|
},
|
|
92
|
+
beforeWrite: () => void,
|
|
69
93
|
): LiveLifecycleObservationV3Result {
|
|
70
94
|
const route = resolveLiveEventLedgerRouteV3(input.coordRoot);
|
|
71
95
|
if (route.state === "blocked") throw new LiveCoordinationAuthorityV3Error(route.reason);
|
|
@@ -121,6 +145,7 @@ function recordObservation<T extends "lifecycle.sweep_observed" | "session.resum
|
|
|
121
145
|
monotonic_ns: process.hrtime.bigint().toString(),
|
|
122
146
|
};
|
|
123
147
|
const event = buildEventV3(eventType, { ...common, payload } as never) as EventV3;
|
|
148
|
+
beforeWrite();
|
|
124
149
|
const durability = writeEventV3(input.coordRoot, event, {
|
|
125
150
|
expectedGenesisId: control.genesis.event.payload.genesis_id as `gex_${string}`,
|
|
126
151
|
});
|
|
@@ -130,6 +155,19 @@ function recordObservation<T extends "lifecycle.sweep_observed" | "session.resum
|
|
|
130
155
|
return { state: "recorded", event };
|
|
131
156
|
}
|
|
132
157
|
|
|
158
|
+
function requireSweepHookState(input: LiveSweepObservationV3Input): void {
|
|
159
|
+
if (
|
|
160
|
+
typeof input.expectedGenerationId !== "string" ||
|
|
161
|
+
!/^gen_[a-zA-Z0-9._-]+$/.test(input.expectedGenerationId)
|
|
162
|
+
) {
|
|
163
|
+
throw new LiveCoordinationAuthorityV3Error("hook_generation_not_joinable");
|
|
164
|
+
}
|
|
165
|
+
const hook = requireHookState(withAttestedAdapter(input));
|
|
166
|
+
if (hook.generation_id !== input.expectedGenerationId) {
|
|
167
|
+
throw new LiveCoordinationAuthorityV3Error("expected_generation_changed");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
133
171
|
function withAttestedAdapter<T extends LiveLifecycleObservationBaseV3>(input: T): T {
|
|
134
172
|
const adapter = liveCoordinationAdapterV3(input.coordRoot, input.owner);
|
|
135
173
|
if (!adapter) return input;
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* shown to them, because nothing surfaces another session's journal.
|
|
10
10
|
*
|
|
11
11
|
* This module addresses the durable persona name instead of the instance, so a
|
|
12
|
-
* message can be queued before its recipient exists.
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
12
|
+
* message can be queued before its recipient exists. The recipient's next
|
|
13
|
+
* SessionStart or prompt can drain the queue for injection. A drain does not
|
|
14
|
+
* confirm that the adapter emitted the context or that anyone read it.
|
|
15
15
|
*
|
|
16
16
|
* Storage: `.harnery/mailbox/<slug>.jsonl`, one JSON record per line, appended
|
|
17
|
-
* by senders and removed by an atomic claim on the recipient's side.
|
|
18
|
-
* records are appended to `.harnery/mailbox/delivered/<slug>.jsonl` for audit
|
|
17
|
+
* by senders and removed by an atomic claim on the recipient's side. Drained
|
|
18
|
+
* records are appended to `.harnery/mailbox/delivered/<slug>.jsonl` for audit,
|
|
19
|
+
* with injection explicitly unconfirmed.
|
|
19
20
|
*
|
|
20
21
|
* Related but distinct: `core/inbox` is an instance-keyed transport with a
|
|
21
22
|
* storage-framework service behind it. This mailbox is name-keyed, file-only,
|
|
@@ -48,8 +49,8 @@ export const MAILBOX_MESSAGE_SCHEMA = "harnery.mailbox-message/v1" as const;
|
|
|
48
49
|
export const MAX_BODY_BYTES = 4000;
|
|
49
50
|
/** Pending messages held for one name before senders are refused. */
|
|
50
51
|
export const MAX_PENDING = 25;
|
|
51
|
-
/**
|
|
52
|
-
const
|
|
52
|
+
/** Drain-archive ceiling; the file is truncated from the head past this. */
|
|
53
|
+
const MAX_ARCHIVE_BYTES = 256 * 1024;
|
|
53
54
|
|
|
54
55
|
export interface MailboxMessageV1 {
|
|
55
56
|
schema: typeof MAILBOX_MESSAGE_SCHEMA;
|
|
@@ -104,7 +105,7 @@ export function mailboxPath(coordRoot: string, name: string): string {
|
|
|
104
105
|
return join(mailboxDir(coordRoot), `${slugFor(name)}.jsonl`);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
function
|
|
108
|
+
function archivePath(coordRoot: string, name: string): string {
|
|
108
109
|
return join(mailboxDir(coordRoot), "delivered", `${slugFor(name)}.jsonl`);
|
|
109
110
|
}
|
|
110
111
|
|
|
@@ -243,10 +244,10 @@ export function peekMailbox(coordRoot: string, name: string): MailboxMessageV1[]
|
|
|
243
244
|
/**
|
|
244
245
|
* Claim and return every pending message for a name.
|
|
245
246
|
*
|
|
246
|
-
* The claim is an atomic rename, so two sessions sharing a name cannot
|
|
247
|
+
* The claim is an atomic rename, so two sessions sharing a name cannot claim
|
|
247
248
|
* the same message twice: whichever rename wins owns the batch. Claimed records
|
|
248
|
-
* are
|
|
249
|
-
*
|
|
249
|
+
* are archived before the claim file is removed. The archive records only the
|
|
250
|
+
* drain; output, recipient read, and turn completion remain unconfirmed.
|
|
250
251
|
*/
|
|
251
252
|
export function drainMailbox(coordRoot: string, name: string): MailboxMessageV1[] {
|
|
252
253
|
let path: string;
|
|
@@ -279,12 +280,20 @@ export function drainMailbox(coordRoot: string, name: string): MailboxMessageV1[
|
|
|
279
280
|
|
|
280
281
|
if (messages.length > 0) {
|
|
281
282
|
try {
|
|
282
|
-
const archive =
|
|
283
|
+
const archive = archivePath(coordRoot, name);
|
|
283
284
|
mkdirSync(join(mailboxDir(coordRoot), "delivered"), { recursive: true });
|
|
284
|
-
const
|
|
285
|
+
const drainedAt = new Date().toISOString();
|
|
285
286
|
appendFileSync(
|
|
286
287
|
archive,
|
|
287
|
-
`${messages
|
|
288
|
+
`${messages
|
|
289
|
+
.map((m) =>
|
|
290
|
+
JSON.stringify({
|
|
291
|
+
...m,
|
|
292
|
+
drained_at: drainedAt,
|
|
293
|
+
delivery_status: "injection-unconfirmed",
|
|
294
|
+
}),
|
|
295
|
+
)
|
|
296
|
+
.join("\n")}\n`,
|
|
288
297
|
"utf8",
|
|
289
298
|
);
|
|
290
299
|
trimArchive(archive);
|
|
@@ -336,7 +345,7 @@ function sweepStaleClaims(coordRoot: string): void {
|
|
|
336
345
|
|
|
337
346
|
function trimArchive(path: string): void {
|
|
338
347
|
try {
|
|
339
|
-
if (statSync(path).size <=
|
|
348
|
+
if (statSync(path).size <= MAX_ARCHIVE_BYTES) return;
|
|
340
349
|
const lines = readFileSync(path, "utf8").split("\n").filter(Boolean);
|
|
341
350
|
const keep = lines.slice(Math.floor(lines.length / 2));
|
|
342
351
|
writeFileSync(path, `${keep.join("\n")}\n`, "utf8");
|
|
@@ -354,8 +363,8 @@ export function formatMailboxDelivery(
|
|
|
354
363
|
const bin = resolveBinName(coordRoot);
|
|
355
364
|
const header =
|
|
356
365
|
messages.length === 1
|
|
357
|
-
? `Message from a peer agent (
|
|
358
|
-
: `${messages.length} messages from peer agents (
|
|
366
|
+
? `Message from a peer agent (prepared for injection, recorded in your journal):`
|
|
367
|
+
: `${messages.length} messages from peer agents (prepared for injection, recorded in your journal):`;
|
|
359
368
|
const body = messages
|
|
360
369
|
.map((m) => {
|
|
361
370
|
const when = m.created_at.replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
@@ -48,17 +48,18 @@ import { join } from "node:path";
|
|
|
48
48
|
import { coordEnv } from "../../../lib/env.ts";
|
|
49
49
|
import { endOfTurnStatusCommand, hostPromptReminder, resolveBinName } from "../../config.ts";
|
|
50
50
|
import type { PressureWorkloadClass } from "../../diagnostics/contract.ts";
|
|
51
|
+
import { formatPeerInbox, peerInbox } from "../../inbox/peer-messages.ts";
|
|
51
52
|
import { type RemoteMachine, readRemoteMachines } from "../../presence/index.ts";
|
|
52
53
|
import { readResourceStatus } from "../../resources/status.ts";
|
|
53
|
-
import { drainMailbox, formatMailboxDelivery } from "../mailbox.ts";
|
|
54
54
|
import { sessionNameDisplayBlock, sessionNameDisplayPending } from "../session-name-display.ts";
|
|
55
55
|
import { heartbeatPath } from "../state/heartbeat-reader.ts";
|
|
56
56
|
import {
|
|
57
57
|
readLiveCoordinationRow,
|
|
58
58
|
readLiveCoordinationRows,
|
|
59
59
|
} from "../state/live-coordination-view.ts";
|
|
60
|
+
import { forkRelationLabels } from "../state/names.ts";
|
|
60
61
|
import type { AgentActivity, TaskState } from "../state/session-state.ts";
|
|
61
|
-
import { formatPendingCouncils
|
|
62
|
+
import { formatPendingCouncils } from "./session-context.ts";
|
|
62
63
|
|
|
63
64
|
interface HeartbeatRow {
|
|
64
65
|
instance_id?: string;
|
|
@@ -102,6 +103,8 @@ export interface PromptContextOpts {
|
|
|
102
103
|
* being bounced into a retry. Not hash-deduped, same rationale as the
|
|
103
104
|
* status footer. */
|
|
104
105
|
turnRitualNudge?: string;
|
|
106
|
+
/** The emitter acknowledges exactly these IDs only after output succeeds. */
|
|
107
|
+
onInboxPrepared?: (messageIds: readonly string[]) => void;
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
/**
|
|
@@ -131,16 +134,12 @@ export function renderPromptContext(opts: PromptContextOpts): string {
|
|
|
131
134
|
// skipped without it.
|
|
132
135
|
const resolvedName = agentName ?? readLiveCoordinationRow(coordRoot, instanceId)?.name;
|
|
133
136
|
|
|
134
|
-
// 1.
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (delivered.length > 0) {
|
|
141
|
-
recordDeliveredMessages(instanceId, delivered);
|
|
142
|
-
sections.push(formatMailboxDelivery(coordRoot, delivered));
|
|
143
|
-
}
|
|
137
|
+
// 1. Instance-bound messages. Rendering is read-only; failed output leaves
|
|
138
|
+
// this same batch pending rather than losing it before a prompt sees it.
|
|
139
|
+
const delivered = peerInbox(coordRoot).prepareSurface(instanceId);
|
|
140
|
+
if (delivered.length > 0) {
|
|
141
|
+
opts.onInboxPrepared?.(delivered.map(({ message_id }) => message_id));
|
|
142
|
+
sections.push(formatPeerInbox(coordRoot, delivered));
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
// 2. Peer table with hash dedup.
|
|
@@ -441,7 +440,11 @@ function computePeerTableIfChanged(
|
|
|
441
440
|
if (oldHash && oldHash === newHash) return "";
|
|
442
441
|
|
|
443
442
|
// Render peer table via the same formatter used at SessionStart.
|
|
444
|
-
const local = formatPeerTable(
|
|
443
|
+
const local = formatPeerTable(
|
|
444
|
+
peers,
|
|
445
|
+
mySessionId,
|
|
446
|
+
forkRelationLabels(coordRoot, selfInstanceId, new Set(peers.map((p) => p.instance_id ?? ""))),
|
|
447
|
+
);
|
|
445
448
|
const remoteTable = formatRemoteTable(remote);
|
|
446
449
|
const table = [local, remoteTable].filter(Boolean).join("\n\n");
|
|
447
450
|
if (!table) return "";
|
|
@@ -580,7 +583,11 @@ function writeHashFile(path: string, value: string): void {
|
|
|
580
583
|
* (no cross-file imports of formatting internals). If both renderers diverge,
|
|
581
584
|
* pull the shared bits into a small util module. */
|
|
582
585
|
|
|
583
|
-
function formatPeerTable(
|
|
586
|
+
function formatPeerTable(
|
|
587
|
+
peers: HeartbeatRow[],
|
|
588
|
+
mySessionId: string,
|
|
589
|
+
relations: Map<string, string> = new Map(),
|
|
590
|
+
): string {
|
|
584
591
|
if (peers.length === 0) return "";
|
|
585
592
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
586
593
|
|
|
@@ -592,16 +599,20 @@ function formatPeerTable(peers: HeartbeatRow[], mySessionId: string): string {
|
|
|
592
599
|
}
|
|
593
600
|
}
|
|
594
601
|
|
|
595
|
-
type RowExt = HeartbeatRow & { display_files: string[] };
|
|
602
|
+
type RowExt = HeartbeatRow & { display_files: string[]; relation?: string };
|
|
596
603
|
const rows: RowExt[] = peers
|
|
597
604
|
.filter((p) => (p.kind ?? "unknown") !== "transient")
|
|
598
605
|
.map((p) => {
|
|
599
606
|
const folded = fold[p.instance_id ?? ""] ?? [];
|
|
600
607
|
const display = Array.from(new Set([...(p.files_touched ?? []), ...folded])).sort();
|
|
601
|
-
|
|
608
|
+
const relation = relations.get(p.instance_id ?? "");
|
|
609
|
+
return { ...p, display_files: display, ...(relation ? { relation } : {}) };
|
|
602
610
|
});
|
|
603
611
|
|
|
604
|
-
|
|
612
|
+
// Related sessions sort first so a fork parent never hides in "+N more".
|
|
613
|
+
const blocking = rows
|
|
614
|
+
.filter((p) => p.session_id !== mySessionId)
|
|
615
|
+
.sort((a, b) => Number(!!b.relation) - Number(!!a.relation) || byStartedAt(a, b));
|
|
605
616
|
const group = rows.filter((p) => p.session_id === mySessionId).sort(byStartedAt);
|
|
606
617
|
|
|
607
618
|
const out: string[] = [];
|
|
@@ -625,7 +636,7 @@ function byStartedAt(a: HeartbeatRow, b: HeartbeatRow): number {
|
|
|
625
636
|
}
|
|
626
637
|
|
|
627
638
|
function renderSubtable(
|
|
628
|
-
rows: Array<HeartbeatRow & { display_files: string[] }>,
|
|
639
|
+
rows: Array<HeartbeatRow & { display_files: string[]; relation?: string }>,
|
|
629
640
|
header: string,
|
|
630
641
|
nowSec: number,
|
|
631
642
|
): string {
|
|
@@ -635,7 +646,10 @@ function renderSubtable(
|
|
|
635
646
|
return `${header}\n${first.join("\n")}${overflow}`;
|
|
636
647
|
}
|
|
637
648
|
|
|
638
|
-
function formatRow(
|
|
649
|
+
function formatRow(
|
|
650
|
+
r: HeartbeatRow & { display_files: string[]; relation?: string },
|
|
651
|
+
nowSec: number,
|
|
652
|
+
): string {
|
|
639
653
|
const taskPart = r.task ? ` "${r.task.slice(0, 60)}"` : "";
|
|
640
654
|
const statePart = formatState(r);
|
|
641
655
|
// Fall back started_at → last_heartbeat; if neither is a valid timestamp,
|
|
@@ -646,7 +660,8 @@ function formatRow(r: HeartbeatRow & { display_files: string[] }, nowSec: number
|
|
|
646
660
|
// Prefer a short instance_id over a bare "unknown" so an incomplete row is
|
|
647
661
|
// still identifiable.
|
|
648
662
|
const label = r.name ?? (r.instance_id ? r.instance_id.slice(0, 8) : "unknown");
|
|
649
|
-
|
|
663
|
+
const relation = r.relation ? ` [${r.relation}]` : "";
|
|
664
|
+
return ` - agent-${label}${relation}${taskPart} (${statePart}, ${ageFrom}, ${filesPart})`;
|
|
650
665
|
}
|
|
651
666
|
|
|
652
667
|
function formatState(r: HeartbeatRow): string {
|