@lyupro/codex-bridge 0.6.9 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -0
- package/README.md +15 -4
- package/cli/dispatcher-model-check.mjs +59 -0
- package/cli/doctor-host-side.mjs +2 -1
- package/cli/doctor-installation.mjs +111 -111
- package/cli/doctor.mjs +13 -5
- package/cli/handback-witness-check.mjs +24 -18
- package/cli/hosts.mjs +2 -10
- package/cli/install-owners.mjs +1 -4
- package/cli/install-record.mjs +2 -1
- package/cli/inventory-transition.mjs +1 -1
- package/cli/lifecycle-lock-inspect.mjs +3 -2
- package/cli/lifecycle-lock.mjs +19 -132
- package/cli/manifest.mjs +6 -6
- package/cli/read.mjs +20 -0
- package/cli/settings-merge.mjs +3 -29
- package/package.json +2 -2
- package/scripts/home-write-inventory.mjs +12 -2
- package/src/agents/codex-advisor.md +37 -121
- package/src/agents/codex-build.md +27 -135
- package/src/agents/codex-review.md +19 -111
- package/src/agents/codex-scout.md +21 -130
- package/src/home/hooks/dispatcher-gate.mjs +14 -9
- package/src/home/hooks/live-runs.mjs +0 -41
- package/src/home/hooks/order-gate.mjs +80 -47
- package/src/home/hooks/reply-guard.mjs +52 -65
- package/src/home/hooks/reply-identity.mjs +64 -0
- package/src/home/hooks/reply-parser.mjs +1 -1
- package/src/home/hooks/reply-verdicts.mjs +31 -27
- package/src/home/hooks/reply-witness.mjs +48 -0
- package/src/home/lib/claude-layout.mjs +14 -0
- package/src/home/lib/command-path.mjs +32 -0
- package/src/home/lib/dispatcher-call.mjs +171 -0
- package/src/home/lib/dispatcher-command.mjs +18 -54
- package/src/home/lib/dispatcher-gate.mjs +4 -0
- package/src/home/lib/dispatcher-model.mjs +112 -0
- package/src/home/lib/dispatcher-order.mjs +21 -22
- package/src/home/lib/dispatcher-pin.mjs +60 -0
- package/src/home/lib/dispatcher-protocol.mjs +91 -0
- package/src/home/lib/dispatcher-stop.mjs +11 -2
- package/src/home/lib/handback-witness.mjs +137 -48
- package/src/home/lib/home-registry.mjs +7 -0
- package/src/home/lib/install-owner-roots.mjs +53 -0
- package/src/home/lib/kernel-lock.mjs +171 -0
- package/src/home/lib/meta/chain.mjs +19 -8
- package/src/home/lib/meta/context-only.mjs +43 -0
- package/src/home/lib/meta/events.mjs +14 -2
- package/src/home/lib/meta/launch-rows.mjs +23 -0
- package/src/home/lib/meta/paths.mjs +1 -1
- package/src/home/lib/meta/reply.mjs +39 -4
- package/src/home/lib/meta/run-state.mjs +3 -0
- package/src/home/lib/meta/verdict.mjs +16 -8
- package/src/home/lib/model-family.mjs +38 -0
- package/src/home/lib/observation-ledger.mjs +145 -0
- package/src/home/lib/order-schema.mjs +204 -0
- package/src/home/lib/order-values.mjs +46 -0
- package/src/home/lib/run-codex.mjs +6 -3
- package/src/home/lib/runner/advise-carry.mjs +2 -1
- package/src/home/lib/runner/args.mjs +140 -220
- package/src/home/lib/runner/attach.mjs +24 -17
- package/src/home/lib/runner/build-evidence.mjs +29 -0
- package/src/home/lib/runner/codex-availability.mjs +50 -0
- package/src/home/lib/runner/codex-cmd.mjs +2 -19
- package/src/home/lib/runner/continuation.mjs +174 -140
- package/src/home/lib/runner/exit-codes.mjs +13 -0
- package/src/home/lib/runner/flag-baseline.mjs +105 -0
- package/src/home/lib/runner/flag-scan.mjs +165 -0
- package/src/home/lib/runner/git-state.mjs +0 -33
- package/src/home/lib/runner/launcher.mjs +237 -277
- package/src/home/lib/runner/order-claim.mjs +84 -0
- package/src/home/lib/runner/order-options.mjs +30 -0
- package/src/home/lib/runner/order-owner.mjs +19 -0
- package/src/home/lib/runner/pass-gate.mjs +111 -0
- package/src/home/lib/runner/preflight.mjs +26 -28
- package/src/home/lib/runner/prompts.mjs +7 -2
- package/src/home/lib/runner/question-kind.mjs +41 -0
- package/src/home/lib/runner/run-env.mjs +2 -1
- package/src/home/lib/runner/sandbox-probe.mjs +1 -1
- package/src/home/lib/runner/scope-check.mjs +31 -12
- package/src/home/lib/runner/task-input.mjs +34 -12
- package/src/home/lib/runner/worker-admission.mjs +43 -0
- package/src/home/lib/runner/worker.mjs +19 -18
- package/src/home/lib/subagent-model-env.mjs +44 -0
- package/src/home/lib/task-header.mjs +158 -0
- package/src/home/lib/untyped-stop.mjs +30 -0
- package/src/home/lib/write-meta.mjs +17 -5
- package/src/home/lib/required-inputs.mjs +0 -261
- /package/{cli → src/home/lib}/frontmatter.mjs +0 -0
|
@@ -18,8 +18,6 @@ import { readJsonFileSync } from '../lib/json-file.mjs';
|
|
|
18
18
|
import { runLiveness } from '../lib/meta/run-liveness.mjs';
|
|
19
19
|
import { IDENTITY_ALIVE } from '../lib/process-identity.mjs';
|
|
20
20
|
|
|
21
|
-
export const RECENT_RUN_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
22
|
-
|
|
23
21
|
/**
|
|
24
22
|
* Match the package's repository comparison without resolving symlinks: Windows realpath adds
|
|
25
23
|
* `\\?\\` and UNC spellings, turning equal paths into unequal ones. A null result is uncertainty,
|
|
@@ -98,45 +96,6 @@ export function liveRuns(runsDir, options = {}) {
|
|
|
98
96
|
return result;
|
|
99
97
|
}
|
|
100
98
|
|
|
101
|
-
/**
|
|
102
|
-
* Find runs recent enough to explain the current dispatcher reply, including completed runs.
|
|
103
|
-
* The 24-hour window covers long and overnight dispatches without letting old project history
|
|
104
|
-
* explain a new reply. This closes the August 13 incident where a fabricated FAIL hid a
|
|
105
|
-
* finished/OK run simply by omitting its folder. Null means the disk could not be trusted.
|
|
106
|
-
*/
|
|
107
|
-
export function recentRuns(runsDir, options = {}) {
|
|
108
|
-
if (typeof runsDir !== 'string' || !runsDir.trim()) return null;
|
|
109
|
-
const now = options.now ?? Date.now();
|
|
110
|
-
const maxAgeMs = options.maxAgeMs ?? RECENT_RUN_MAX_AGE_MS;
|
|
111
|
-
let entries;
|
|
112
|
-
try {
|
|
113
|
-
entries = fs.readdirSync(runsDir, { withFileTypes: true });
|
|
114
|
-
} catch (err) {
|
|
115
|
-
return err.code === 'ENOENT' ? [] : null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const result = [];
|
|
119
|
-
for (const entry of entries) {
|
|
120
|
-
if (!entry.isDirectory()) continue;
|
|
121
|
-
const dir = path.join(runsDir, entry.name);
|
|
122
|
-
const statusPath = path.join(dir, 'status.json');
|
|
123
|
-
if (!fs.existsSync(statusPath)) continue;
|
|
124
|
-
let status;
|
|
125
|
-
try {
|
|
126
|
-
status = readJsonFileSync(statusPath);
|
|
127
|
-
} catch {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
if (!status || typeof status !== 'object' || Array.isArray(status)) return null;
|
|
131
|
-
if (options.agent && status.agent !== options.agent) continue;
|
|
132
|
-
if (options.orderId && String(status.order_id ?? '').trim() !== options.orderId) continue;
|
|
133
|
-
const timestamp = Date.parse(status.finished_at || status.started_at || '');
|
|
134
|
-
if (!Number.isFinite(timestamp) || timestamp > now || now - timestamp > maxAgeMs) continue;
|
|
135
|
-
result.push({ dir, status, timestamp });
|
|
136
|
-
}
|
|
137
|
-
return result.sort((a, b) => b.timestamp - a.timestamp);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
99
|
/** Scan every project under the configured runs root for worktree ownership. */
|
|
141
100
|
export function allLiveRuns(runsRoot, options = {}) {
|
|
142
101
|
if (typeof runsRoot !== 'string' || !runsRoot.trim()) return [];
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* PreToolUse gate for the Codex dispatchers (codex-scout / codex-build / codex-review).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Plan_63 D9: the call carries only the task file; order id and grant come from its header.
|
|
6
|
+
* OW-054: validate the header and order before a grant can skip the order-owner diagnostic,
|
|
7
|
+
* so a `continue: none` line cannot disarm the check. The producer can still correct the order
|
|
8
|
+
* before the runner creates a run folder or spends quota.
|
|
9
9
|
*
|
|
10
10
|
* Input is Claude Code's hook JSON on stdin. The last payload is retained for diagnostics so a
|
|
11
11
|
* future host schema change can be inspected instead of guessed at. Any uncertain shape passes
|
|
@@ -14,21 +14,22 @@
|
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import { AGENTS } from '../lib/agents.mjs';
|
|
17
|
+
import { BRAND_HOME, BRAND_STATE_DIR } from '../lib/brand-home.mjs';
|
|
18
|
+
import { modelLatch, readDispatcherModel } from '../lib/dispatcher-model.mjs';
|
|
19
|
+
import { readDispatcherPin } from '../lib/dispatcher-pin.mjs';
|
|
17
20
|
import { recordHookDiagnostic } from '../lib/hook-diagnostics.mjs';
|
|
21
|
+
import { transcriptHostVersion } from '../lib/host-version.mjs';
|
|
18
22
|
import { readJsonFileSync } from '../lib/json-file.mjs';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
extractValue,
|
|
22
|
-
missingInputs,
|
|
23
|
-
parseContinuationGrant,
|
|
24
|
-
shellUnsafeInputs,
|
|
25
|
-
} from '../lib/required-inputs.mjs';
|
|
23
|
+
import { parseDispatcherCall, renderCallRefusal } from '../lib/dispatcher-call.mjs';
|
|
24
|
+
import { orderFromHeader, renderOrderProblems } from '../lib/order-schema.mjs';
|
|
26
25
|
import { SUBAGENT_TOOLS } from '../lib/hook-definitions.mjs';
|
|
27
26
|
import { taskFingerprint } from '../lib/meta/chain.mjs';
|
|
28
|
-
import { conflictingOrderOwner } from '../lib/runner/order-owner.mjs';
|
|
27
|
+
import { conflictingOrderOwner, orderOwnerConflictText } from '../lib/runner/order-owner.mjs';
|
|
29
28
|
import { resolveProjectRunsDir } from '../lib/runner/project-dir.mjs';
|
|
30
29
|
import { runsRoot } from '../lib/runner/runs-root.mjs';
|
|
31
30
|
import { parseTaskDocument } from '../lib/runner/task-file.mjs';
|
|
31
|
+
import { parseTaskHeader, taskHeaderRefusal } from '../lib/task-header.mjs';
|
|
32
|
+
import { envModelProblem } from '../lib/subagent-model-env.mjs';
|
|
32
33
|
|
|
33
34
|
const GUARDED = new Set(Object.keys(AGENTS));
|
|
34
35
|
/**
|
|
@@ -38,9 +39,14 @@ const GUARDED = new Set(Object.keys(AGENTS));
|
|
|
38
39
|
*/
|
|
39
40
|
const SUBAGENT_TOOL_NAMES = new Set(SUBAGENT_TOOLS);
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
let systemMessage;
|
|
43
|
+
const pass = () => {
|
|
44
|
+
if (systemMessage) process.stdout.write(JSON.stringify({ systemMessage }));
|
|
45
|
+
process.exit(0);
|
|
46
|
+
};
|
|
42
47
|
const deny = (reason) => {
|
|
43
48
|
process.stdout.write(JSON.stringify({
|
|
49
|
+
...(systemMessage ? { systemMessage } : {}),
|
|
44
50
|
hookSpecificOutput: {
|
|
45
51
|
hookEventName: 'PreToolUse',
|
|
46
52
|
permissionDecision: 'deny',
|
|
@@ -64,7 +70,37 @@ if (!SUBAGENT_TOOL_NAMES.has(input.tool_name)) pass();
|
|
|
64
70
|
|
|
65
71
|
const toolInput = input.tool_input;
|
|
66
72
|
if (!toolInput || typeof toolInput !== 'object' || Array.isArray(toolInput)) pass();
|
|
67
|
-
if (!GUARDED.has(toolInput.subagent_type)
|
|
73
|
+
if (!GUARDED.has(toolInput.subagent_type)) pass();
|
|
74
|
+
|
|
75
|
+
// Plan_67 D6: one installed model contract; refuse even matching aliases before prompt exits.
|
|
76
|
+
const agentType = toolInput.subagent_type;
|
|
77
|
+
if (Object.hasOwn(toolInput, 'model')) {
|
|
78
|
+
deny(
|
|
79
|
+
"Order gate denied the Agent call because it passes `model`: a dispatcher's model is set in one place, "
|
|
80
|
+
+ `its installed agent file. Call ${agentType} without model. `
|
|
81
|
+
+ 'The run folder was not created; quota was not spent.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const hostVersion = await transcriptHostVersion(input.transcript_path).catch(() => null);
|
|
86
|
+
const pin = readDispatcherPin({ brandRoot: BRAND_HOME.root, agentType });
|
|
87
|
+
const problem = envModelProblem({ env: process.env, hostVersion, pinFamily: pin.family });
|
|
88
|
+
if (problem) deny(problem);
|
|
89
|
+
} catch {
|
|
90
|
+
// D6 (b): uncertain host/env evidence is diagnostic only; the stop observer still checks it.
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const latch = modelLatch({ ledger: readDispatcherModel({ stateDir: BRAND_STATE_DIR }), agentType });
|
|
94
|
+
if (latch.active) {
|
|
95
|
+
const { data, at } = latch.violation;
|
|
96
|
+
systemMessage = `codex-bridge: the last ${agentType} ran on ${data.parsed.join(', ')} `
|
|
97
|
+
+ `while the installed contract pins ${data.pinFamily} (${data.hostVersion ?? 'unknown'}, ${at}); `
|
|
98
|
+
+ 'the cause is outside this call — another hook or the host; run codex-bridge doctor.';
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// D4 (a): a corrupt ledger must not turn a diagnostic warning into a refusal.
|
|
102
|
+
}
|
|
103
|
+
if (typeof toolInput.prompt !== 'string') pass();
|
|
68
104
|
|
|
69
105
|
/**
|
|
70
106
|
* Plan_62 D7: with agent teams enabled, a registered dispatcher launched with `name` or `team_name`
|
|
@@ -82,28 +118,9 @@ if (teammateField) {
|
|
|
82
118
|
);
|
|
83
119
|
}
|
|
84
120
|
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
const reason = missing.length
|
|
89
|
-
? [
|
|
90
|
-
'Order gate denied the Agent call because required dispatcher input(s) are missing or still placeholders.',
|
|
91
|
-
'Write each value in tool_input.prompt using `label: value` before launching the dispatcher:',
|
|
92
|
-
...missing.flatMap((entry) => {
|
|
93
|
-
const lines = [`- ${entry.label}: ${entry.explanation} Example: \`${entry.example}\`.`];
|
|
94
|
-
const diagnosis = diagnoseInput(toolInput.prompt, entry.label);
|
|
95
|
-
if (diagnosis) lines.push(` found \`${diagnosis.line}\`, ${diagnosis.reason}`);
|
|
96
|
-
return lines;
|
|
97
|
-
}),
|
|
98
|
-
].join('\n')
|
|
99
|
-
: [
|
|
100
|
-
'Order gate denied the Agent call because labelled dispatcher input(s) contain forbidden shell sequences.',
|
|
101
|
-
'Correct each field before launching the dispatcher:',
|
|
102
|
-
...unsafe.map(({ entry, sequence }) =>
|
|
103
|
-
`- ${entry.label}: found ${JSON.stringify(sequence)}; put free text in the task file and pass only a short command-line value.`),
|
|
104
|
-
].join('\n');
|
|
105
|
-
deny(reason);
|
|
106
|
-
}
|
|
121
|
+
// Plan_76 D1: exact registry labels prevent the scope-new and repository-prose incident of 2026-10-04.
|
|
122
|
+
const call = parseDispatcherCall(toolInput.subagent_type, toolInput.prompt);
|
|
123
|
+
if (call.problems.length) deny(renderCallRefusal(toolInput.subagent_type, call.problems));
|
|
107
124
|
|
|
108
125
|
const readStatus = (file) => {
|
|
109
126
|
try {
|
|
@@ -113,13 +130,34 @@ const readStatus = (file) => {
|
|
|
113
130
|
}
|
|
114
131
|
};
|
|
115
132
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
const taskFile = call.inputs.get('task file');
|
|
134
|
+
|
|
135
|
+
let rawTask;
|
|
136
|
+
try {
|
|
137
|
+
rawTask = fs.readFileSync(taskFile, 'utf8');
|
|
138
|
+
} catch {
|
|
139
|
+
pass();
|
|
140
|
+
}
|
|
141
|
+
// Plan_75 D5, 2026-10-03 20:42: malformed metadata must not disappear into the disk-state guard.
|
|
142
|
+
const parsed = parseTaskHeader(rawTask);
|
|
143
|
+
const headerRefusal = taskHeaderRefusal(parsed);
|
|
144
|
+
if (headerRefusal) deny(headerRefusal);
|
|
145
|
+
const { order, problems } = orderFromHeader(toolInput.subagent_type, parsed);
|
|
146
|
+
if (problems.length) {
|
|
147
|
+
deny(`${renderOrderProblems(toolInput.subagent_type, problems)}\nThe run folder was not created; quota was not spent.`);
|
|
148
|
+
}
|
|
149
|
+
const orderId = order.get('order id');
|
|
150
|
+
// Plan_63 D9 / OW-054: only a validated header grant reaches the runner's grant rules.
|
|
151
|
+
if (parsed.grant) pass();
|
|
152
|
+
let taskHash;
|
|
153
|
+
try {
|
|
154
|
+
taskHash = taskFingerprint(parseTaskDocument(parsed.body).task);
|
|
155
|
+
} catch {
|
|
156
|
+
// A malformed Questions or Verify section is the runner's refusal to issue, with its own text.
|
|
157
|
+
pass();
|
|
158
|
+
}
|
|
119
159
|
|
|
120
160
|
try {
|
|
121
|
-
const taskDocument = parseTaskDocument(fs.readFileSync(taskFile, 'utf8'));
|
|
122
|
-
const taskHash = taskFingerprint(taskDocument.task);
|
|
123
161
|
const projectRunsDir = resolveProjectRunsDir(runsRoot(), input.cwd, { create: false }).dir;
|
|
124
162
|
// Per folder, not per directory: a run folder created a moment before its status.json, or any
|
|
125
163
|
// leftover beside the runs, would otherwise throw out of the whole check and silently disarm the
|
|
@@ -131,12 +169,7 @@ try {
|
|
|
131
169
|
const owner = conflictingOrderOwner(runs, orderId, taskHash);
|
|
132
170
|
if (owner) {
|
|
133
171
|
const ownerDir = path.join(projectRunsDir, owner.run);
|
|
134
|
-
deny(
|
|
135
|
-
`Order gate denied the Agent call because order id ${JSON.stringify(orderId)} already belongs ` +
|
|
136
|
-
`to run folder ${ownerDir} (slug ${owner.status.slug}, started_at ${owner.status.started_at}) ` +
|
|
137
|
-
'with a different task. Use a new order id, or provide an explicit continuation grant if ' +
|
|
138
|
-
'this is another pass of that order.',
|
|
139
|
-
);
|
|
172
|
+
deny(orderOwnerConflictText(owner, ownerDir, orderId));
|
|
140
173
|
}
|
|
141
174
|
} catch {
|
|
142
175
|
// The 2026-08-15 collision guard is diagnostic: uncertain disk state must not block real work.
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* In the handback era, SubagentHandback delivers the caller-visible answer before SubagentStop.
|
|
25
25
|
* Alarms name the host by its transcript version: on 2026-09-25 the SDK variable proved inherited.
|
|
26
26
|
* Plan_62 makes this hook yield after delivery and audit every transcript tool call against gate receipts.
|
|
27
|
+
* Plan_63 D10 identifies this run only by the host-keyed gate's immutable runner receipt.
|
|
27
28
|
*
|
|
28
29
|
* Input is JSON on stdin. The fields used here (`agent_type`, `last_assistant_message`)
|
|
29
30
|
* come from Claude Code; the last payload is kept in state/diagnostics/reply-guard.last.json
|
|
@@ -32,20 +33,21 @@
|
|
|
32
33
|
import fs from 'node:fs';
|
|
33
34
|
import path from 'node:path';
|
|
34
35
|
import { AGENTS } from '../lib/agents.mjs';
|
|
35
|
-
import { BRAND_STATE_DIR } from '../lib/brand-home.mjs';
|
|
36
|
+
import { BRAND_HOME, BRAND_STATE_DIR } from '../lib/brand-home.mjs';
|
|
36
37
|
import { recordHookDiagnostic } from '../lib/hook-diagnostics.mjs';
|
|
37
|
-
import { runOrderMismatch, transcriptOrderId } from '../lib/dispatcher-order.mjs';
|
|
38
38
|
import { decideDispatcherStop, transcriptToolUses } from '../lib/dispatcher-stop.mjs';
|
|
39
|
+
import { observeDispatcherModelStop } from '../lib/dispatcher-model.mjs';
|
|
39
40
|
import { recognizeHostRefusal } from '../lib/host-refusal.mjs';
|
|
40
41
|
import { readJsonFileSync } from '../lib/json-file.mjs';
|
|
41
42
|
import { readDispatcherState, updateDispatcherState } from '../lib/dispatcher-state.mjs';
|
|
42
|
-
import {
|
|
43
|
+
import { witnessHostVersion } from '../lib/handback-witness.mjs';
|
|
44
|
+
import { untypedStopEvidence } from '../lib/untyped-stop.mjs';
|
|
43
45
|
import { runLiveness } from '../lib/meta/run-liveness.mjs';
|
|
44
|
-
import {
|
|
45
|
-
import { runsRoot } from '../lib/runner/runs-root.mjs';
|
|
46
|
-
import { liveRuns, normalizePath, recentRuns } from './live-runs.mjs';
|
|
46
|
+
import { liveRuns, normalizePath } from './live-runs.mjs';
|
|
47
47
|
import { FORM, MAX_STATE_BLOCKS, STATE, takeTry } from './guard-tries.mjs';
|
|
48
48
|
import { parseReply } from './reply-parser.mjs';
|
|
49
|
+
import { recordStopWitness, typedStopObservations, untypedStopObservation } from './reply-witness.mjs';
|
|
50
|
+
import { decideReplyIdentity, missingIdsAlarm, readReceiptEvidence } from './reply-identity.mjs';
|
|
49
51
|
import {
|
|
50
52
|
abandonedRunReason,
|
|
51
53
|
abandonedRunStop,
|
|
@@ -58,14 +60,11 @@ import {
|
|
|
58
60
|
missingHostOrderIdReason,
|
|
59
61
|
missingMetaReason,
|
|
60
62
|
missingRunReason,
|
|
61
|
-
noRecentRunReason,
|
|
62
|
-
noRecentRunStop,
|
|
63
63
|
nonexistentRunReason,
|
|
64
64
|
omittedDiscoveredRunReason,
|
|
65
65
|
omittedDiscoveredRunStop,
|
|
66
66
|
omittedSiblingReason,
|
|
67
67
|
omittedSiblingStop,
|
|
68
|
-
orderMismatchStop,
|
|
69
68
|
statusMismatchReason,
|
|
70
69
|
statusMismatchStop,
|
|
71
70
|
} from './reply-verdicts.mjs';
|
|
@@ -114,20 +113,24 @@ try {
|
|
|
114
113
|
|
|
115
114
|
recordHookDiagnostic('reply-guard', input);
|
|
116
115
|
|
|
117
|
-
//
|
|
118
|
-
// audit without a word (Plan_66 D2 (c)); say so instead of passing silently.
|
|
116
|
+
// Plan_67 D9: only evidence bound to this stop can identify an untyped dispatcher.
|
|
119
117
|
if (typeof input.agent_id === 'string' && input.agent_id.length > 0
|
|
120
118
|
&& !(typeof input.agent_type === 'string' && input.agent_type.length > 0)) {
|
|
119
|
+
let evidence;
|
|
121
120
|
try {
|
|
122
|
-
|
|
123
|
-
stateDir: BRAND_STATE_DIR,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
detail: `host omitted agent_type for agent ${input.agent_id}`,
|
|
127
|
-
});
|
|
121
|
+
const state = typeof input.session_id === 'string' && input.session_id.length > 0
|
|
122
|
+
? readDispatcherState({ stateDir: BRAND_STATE_DIR, sessionId: input.session_id, agentId: input.agent_id }) : null;
|
|
123
|
+
const transcript = transcriptToolUses(input.agent_transcript_path);
|
|
124
|
+
evidence = untypedStopEvidence({ state, toolUses: transcript?.toolUses ?? null });
|
|
128
125
|
} catch {
|
|
129
|
-
//
|
|
126
|
+
// An unreadable state is no evidence either way; a guard failure must never break the stop.
|
|
127
|
+
evidence = { dispatcher: false, reason: 'state-unreadable' };
|
|
130
128
|
}
|
|
129
|
+
const { observation, alarm } = untypedStopObservation({
|
|
130
|
+
evidence, hostVersion: await witnessHostVersion(input), agentId: input.agent_id,
|
|
131
|
+
});
|
|
132
|
+
await recordStopWitness({ stateDir: BRAND_STATE_DIR, observations: [observation] });
|
|
133
|
+
if (!alarm) pass();
|
|
131
134
|
process.stdout.write(JSON.stringify({
|
|
132
135
|
systemMessage: 'codex-bridge: the host did not report agent_type for a subagent — the dispatcher gate cannot recognise dispatchers, so their answers are unchecked; run codex-bridge doctor.',
|
|
133
136
|
}));
|
|
@@ -137,32 +140,40 @@ if (typeof input.agent_id === 'string' && input.agent_id.length > 0
|
|
|
137
140
|
// business — an earlier version inferred the agent from transcript text and blocked an
|
|
138
141
|
// unrelated one, which cost a re-answer for nothing.
|
|
139
142
|
if (!GUARDED.has(input.agent_type)) pass();
|
|
143
|
+
try {
|
|
144
|
+
dispatcherSystemMessage = await observeDispatcherModelStop({
|
|
145
|
+
brandRoot: BRAND_HOME.root, stateDir: BRAND_STATE_DIR, input, hostVersion: await witnessHostVersion(input),
|
|
146
|
+
});
|
|
147
|
+
} catch { /* Plan_67 D4: model diagnostics must never block a dispatcher stop. */ }
|
|
140
148
|
|
|
141
149
|
const emitStop = (payload) => {
|
|
142
150
|
process.stdout.write(JSON.stringify(payload));
|
|
143
151
|
process.exit(0);
|
|
144
152
|
};
|
|
153
|
+
const hasIds = typeof input.session_id === 'string' && input.session_id.length > 0
|
|
154
|
+
&& typeof input.agent_id === 'string' && input.agent_id.length > 0;
|
|
155
|
+
let receipt = null;
|
|
156
|
+
let receiptConflict = null;
|
|
145
157
|
try {
|
|
146
|
-
if (
|
|
147
|
-
&& typeof input.agent_id === 'string' && input.agent_id.length > 0) {
|
|
158
|
+
if (hasIds) {
|
|
148
159
|
const ids = {
|
|
149
160
|
stateDir: BRAND_STATE_DIR,
|
|
150
161
|
sessionId: input.session_id,
|
|
151
162
|
agentId: input.agent_id,
|
|
152
163
|
};
|
|
153
164
|
const state = readDispatcherState(ids);
|
|
154
|
-
|
|
165
|
+
({ receipt, receiptConflict } = readReceiptEvidence(state));
|
|
166
|
+
const transcript = transcriptToolUses(input.agent_transcript_path);
|
|
167
|
+
const toolUses = transcript?.toolUses ?? null;
|
|
155
168
|
const stop = decideDispatcherStop({ state, toolUses });
|
|
156
169
|
if (stop.unseen.length) {
|
|
157
170
|
const names = stop.unseen.map((toolUse) => toolUse.name).join(', ');
|
|
158
|
-
dispatcherSystemMessage = `codex-bridge: dispatcher ${input.agent_type} used ${names} outside the dispatcher gate — do not trust its answer; run codex-bridge doctor
|
|
159
|
-
await recordHandbackWitness({
|
|
160
|
-
stateDir: BRAND_STATE_DIR,
|
|
161
|
-
kind: 'alarm',
|
|
162
|
-
hostVersion: await witnessHostVersion(input),
|
|
163
|
-
detail: `${input.agent_type} ${input.agent_id}: ${names} outside the dispatcher gate`,
|
|
164
|
-
});
|
|
171
|
+
dispatcherSystemMessage = [dispatcherSystemMessage, `codex-bridge: dispatcher ${input.agent_type} used ${names} outside the dispatcher gate — do not trust its answer; run codex-bridge doctor.`].filter(Boolean).join(' ');
|
|
165
172
|
}
|
|
173
|
+
await recordStopWitness({ stateDir: BRAND_STATE_DIR, observations: typedStopObservations({
|
|
174
|
+
agentType: input.agent_type, agentId: input.agent_id, hostVersion: await witnessHostVersion(input),
|
|
175
|
+
hasIds, state, toolUses, unseen: stop.unseen, transcriptComplete: transcript?.complete === true,
|
|
176
|
+
}) });
|
|
166
177
|
if (stop.stateUpdate) {
|
|
167
178
|
await updateDispatcherState(ids, (current) => ({
|
|
168
179
|
...(current.corrupt ? {} : current),
|
|
@@ -185,6 +196,14 @@ try {
|
|
|
185
196
|
// The handback path is fail-open; the established reply checks remain the fallback.
|
|
186
197
|
}
|
|
187
198
|
|
|
199
|
+
if (!hasIds) {
|
|
200
|
+
await recordStopWitness({ stateDir: BRAND_STATE_DIR, observations: typedStopObservations({
|
|
201
|
+
agentType: input.agent_type, hostVersion: await witnessHostVersion(input),
|
|
202
|
+
hasIds, state: null, toolUses: null, unseen: [], transcriptComplete: false,
|
|
203
|
+
}) });
|
|
204
|
+
dispatcherSystemMessage ||= missingIdsAlarm(input.agent_type);
|
|
205
|
+
}
|
|
206
|
+
|
|
188
207
|
const reply = String(input.last_assistant_message || '').trim();
|
|
189
208
|
if (!reply) pass();
|
|
190
209
|
const emit = (payload) => {
|
|
@@ -220,48 +239,22 @@ const blockState = (reason, stopReason) => {
|
|
|
220
239
|
|
|
221
240
|
const { runDirs, claimed } = parseReply(reply);
|
|
222
241
|
let runDir = runDirs[0] || null;
|
|
223
|
-
let discoveredRun = false;
|
|
224
|
-
let discoveredStatus = null;
|
|
225
|
-
let searchedRunsDir = null;
|
|
226
242
|
|
|
227
243
|
if (runDir && !fs.existsSync(runDir)) {
|
|
228
244
|
blockForm(nonexistentRunReason);
|
|
229
245
|
}
|
|
230
246
|
|
|
231
|
-
const orderedOrderId = transcriptOrderId(input.agent_transcript_path);
|
|
232
|
-
|
|
233
247
|
if (!runDir && !claimed) {
|
|
234
248
|
// A reply that pronounces nothing cannot contradict the disk. It stays on the old, softer path:
|
|
235
249
|
// three tries about the SHAPE of the answer and then through, which is what a quoted refusal
|
|
236
|
-
// needs. Only a reply that hands down a verdict earns the
|
|
250
|
+
// needs. Only a reply that hands down a verdict earns the receipt checks below.
|
|
237
251
|
blockForm(missingRunReason);
|
|
238
252
|
}
|
|
239
253
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// Plan_62 r2: on 2026-09-24 the newest run of this agent type belonged to an earlier order, and the
|
|
245
|
-
// guard judged the reply against that stranger's folder. With the order id known, only its runs count.
|
|
246
|
-
candidates = recentRuns(searchedRunsDir, {
|
|
247
|
-
agent: input.agent_type,
|
|
248
|
-
...(orderedOrderId ? { orderId: orderedOrderId } : {}),
|
|
249
|
-
});
|
|
250
|
-
} catch {
|
|
251
|
-
pass();
|
|
252
|
-
}
|
|
253
|
-
if (candidates === null) pass();
|
|
254
|
-
|
|
255
|
-
if (!candidates.length) {
|
|
256
|
-
blockState(
|
|
257
|
-
noRecentRunReason,
|
|
258
|
-
noRecentRunStop(MAX_STATE_BLOCKS, input.agent_type, searchedRunsDir),
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
({ dir: runDir, status: discoveredStatus } = candidates[0]);
|
|
263
|
-
discoveredRun = true;
|
|
264
|
-
}
|
|
254
|
+
const identity = decideReplyIdentity({ runDir, hasIds, receipt, receiptConflict, agentType: input.agent_type });
|
|
255
|
+
if (identity.block) blockState(identity.block.reason, identity.block.stop);
|
|
256
|
+
const { discoveredRun, discoveredStatus } = identity;
|
|
257
|
+
runDir = identity.runDir;
|
|
265
258
|
|
|
266
259
|
/**
|
|
267
260
|
* The dispatcher must name every WRITING run the project has live, not just the one it chose to
|
|
@@ -307,12 +300,6 @@ if (fs.existsSync(statusPath)) {
|
|
|
307
300
|
} catch {
|
|
308
301
|
runStatus = null;
|
|
309
302
|
}
|
|
310
|
-
const mismatch = runOrderMismatch(orderedOrderId, runStatus, runDir);
|
|
311
|
-
|
|
312
|
-
if (mismatch) {
|
|
313
|
-
blockState(mismatch.reason, orderMismatchStop(MAX_STATE_BLOCKS, mismatch, runStatus, runDir));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
303
|
if (runStatus?.state === 'running') {
|
|
317
304
|
// Plan_57 D5/D27: a bare pid mistook a reused foreign process for this run's worker.
|
|
318
305
|
// The shared judge uses run identity and keeps unverified processes live (fail open).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Decides which run a dispatcher reply answers for, from the gate's receipt (Plan_63 D10; 2026-09-24 stranger-run incident). */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readJsonFileSync } from '../lib/json-file.mjs';
|
|
4
|
+
import { normalizePath } from './live-runs.mjs';
|
|
5
|
+
import { MAX_STATE_BLOCKS } from './guard-tries.mjs';
|
|
6
|
+
import {
|
|
7
|
+
noReceiptReason,
|
|
8
|
+
noReceiptStop,
|
|
9
|
+
receiptConflictReason,
|
|
10
|
+
receiptConflictStop,
|
|
11
|
+
receiptMismatchReason,
|
|
12
|
+
receiptMismatchStop,
|
|
13
|
+
} from './reply-verdicts.mjs';
|
|
14
|
+
|
|
15
|
+
export function readReceiptEvidence(state) {
|
|
16
|
+
return {
|
|
17
|
+
receipt: typeof state?.runReceipt === 'string' && state.runReceipt.length > 0 ? state.runReceipt : null,
|
|
18
|
+
receiptConflict: typeof state?.runReceiptConflict === 'string' && state.runReceiptConflict.length > 0
|
|
19
|
+
? state.runReceiptConflict : null,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function missingIdsAlarm(agentType) {
|
|
24
|
+
return `codex-bridge: the host did not report session_id and agent_id for dispatcher ${agentType} — whose run this answer quotes cannot be checked; run codex-bridge doctor.`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Plan_63 D10: a recent stranger is never evidence for this reply; only the gate receipt is.
|
|
28
|
+
// Form checks stay in the caller before this decision, preserving their softer try budget.
|
|
29
|
+
export function decideReplyIdentity({ runDir, hasIds, receipt, receiptConflict, agentType }) {
|
|
30
|
+
if (hasIds && receiptConflict) {
|
|
31
|
+
return { block: {
|
|
32
|
+
reason: receiptConflictReason(receipt, receiptConflict),
|
|
33
|
+
stop: receiptConflictStop(MAX_STATE_BLOCKS, agentType, receipt, receiptConflict),
|
|
34
|
+
} };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!runDir) {
|
|
38
|
+
if (!(hasIds && receipt)) {
|
|
39
|
+
return { block: { reason: noReceiptReason, stop: noReceiptStop(MAX_STATE_BLOCKS, agentType) } };
|
|
40
|
+
}
|
|
41
|
+
let discoveredStatus = null;
|
|
42
|
+
try {
|
|
43
|
+
discoveredStatus = readJsonFileSync(path.join(receipt, 'status.json'));
|
|
44
|
+
} catch {
|
|
45
|
+
discoveredStatus = null;
|
|
46
|
+
}
|
|
47
|
+
return { runDir: receipt, discoveredRun: true, discoveredStatus };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (hasIds) {
|
|
51
|
+
if (!receipt) {
|
|
52
|
+
return { block: { reason: noReceiptReason, stop: noReceiptStop(MAX_STATE_BLOCKS, agentType) } };
|
|
53
|
+
}
|
|
54
|
+
if (normalizePath(runDir) !== normalizePath(receipt)) {
|
|
55
|
+
return { block: {
|
|
56
|
+
reason: receiptMismatchReason(runDir, receipt),
|
|
57
|
+
stop: receiptMismatchStop(MAX_STATE_BLOCKS, agentType, runDir, receipt),
|
|
58
|
+
} };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Review 2026-10-05_201513_plan63-r-review-20261005 and Plan_66 H2: missing ids alarm
|
|
62
|
+
// in the caller; named folders retain disk checks because host contract loss must never block work.
|
|
63
|
+
return { runDir, discoveredRun: false, discoveredStatus: null };
|
|
64
|
+
}
|
|
@@ -39,7 +39,7 @@ export const stopText = (headline, observed, runStatus, folder) => {
|
|
|
39
39
|
'The run will close itself: the worker outlives every caller and completes meta.json and ' +
|
|
40
40
|
'status.json without the dispatcher. Wait for state to change and reread status.json — the ' +
|
|
41
41
|
'verdict will be there; there is no need to ask the dispatcher again. Repeating the same ' +
|
|
42
|
-
'command with the same
|
|
42
|
+
'command with the same task file attaches to this run rather than starting another.',
|
|
43
43
|
].join(' ');
|
|
44
44
|
};
|
|
45
45
|
|
|
@@ -62,18 +62,29 @@ export const handbackDemandReason =
|
|
|
62
62
|
'exactly the command its refusal quotes and hand back again; the gate replaces the message with the ' +
|
|
63
63
|
"runner's output.";
|
|
64
64
|
|
|
65
|
-
export const
|
|
66
|
-
'Contract violated:
|
|
67
|
-
|
|
68
|
-
'analysis instead of Codex is prohibited in all outcomes.';
|
|
65
|
+
export const noReceiptReason =
|
|
66
|
+
'Contract violated: no run receipt is recorded for this dispatcher. Run the canonical ' +
|
|
67
|
+
"codex-bridge run command and return that run's stdout verbatim; another run on disk cannot identify this run.";
|
|
69
68
|
|
|
70
|
-
export const
|
|
69
|
+
export const noReceiptStop = (maxBlocks, agentType) =>
|
|
71
70
|
`The reply guard stopped the session: the dispatcher responded ${maxBlocks} times ` +
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
`without a run receipt for agent ${agentType}. The answer was not checked.`;
|
|
72
|
+
|
|
73
|
+
export const receiptMismatchReason = (runDir, receipt) =>
|
|
74
|
+
`Contract violated: the response names run folder ${runDir}, but this dispatcher's run receipt ` +
|
|
75
|
+
`names ${receipt}. Run the canonical codex-bridge run command and return that run's stdout verbatim.`;
|
|
76
|
+
|
|
77
|
+
export const receiptMismatchStop = (maxBlocks, agentType, runDir, receipt) =>
|
|
78
|
+
`The reply guard stopped the session: dispatcher ${agentType} returned run folder ${runDir} ` +
|
|
79
|
+
`${maxBlocks} times while its run receipt names ${receipt}. The answer was not checked.`;
|
|
80
|
+
|
|
81
|
+
export const receiptConflictReason = (receipt, conflict) =>
|
|
82
|
+
`Contract violated: this dispatcher's run receipt names ${fact(receipt)}, but a contradictory ` +
|
|
83
|
+
`receipt names ${conflict}. Run the canonical codex-bridge run command and return that run's stdout verbatim.`;
|
|
84
|
+
|
|
85
|
+
export const receiptConflictStop = (maxBlocks, agentType, receipt, conflict) =>
|
|
86
|
+
`The reply guard stopped the session: dispatcher ${agentType} responded ${maxBlocks} times ` +
|
|
87
|
+
`with conflicting run receipts ${fact(receipt)} and ${conflict}. The answer was not checked.`;
|
|
77
88
|
|
|
78
89
|
export const omittedSiblingReason = (runDir, dir, status) =>
|
|
79
90
|
`Contract violated: the response names ${runDir} but omits live run folder ${dir}; report every ` +
|
|
@@ -87,17 +98,10 @@ export const omittedSiblingStop = (maxBlocks, dir, status) => stopText(
|
|
|
87
98
|
dir,
|
|
88
99
|
);
|
|
89
100
|
|
|
90
|
-
export const orderMismatchStop = (maxBlocks, mismatch, runStatus, runDir) => stopText(
|
|
91
|
-
`The reply guard stopped the session: the dispatcher returned another order's run ${maxBlocks} times.`,
|
|
92
|
-
mismatch.observed,
|
|
93
|
-
runStatus,
|
|
94
|
-
runDir,
|
|
95
|
-
);
|
|
96
|
-
|
|
97
101
|
export const liveRunReason =
|
|
98
102
|
'Contract violated: status.json says state=running and the process is alive — the run is ' +
|
|
99
103
|
'not finished, but you are already responding. The STARTED output of the starting call ' +
|
|
100
|
-
'is not a result. Repeat the identical codex-bridge run command, same
|
|
104
|
+
'is not a result. Repeat the identical codex-bridge run command, same task file, with ' +
|
|
101
105
|
'timeout 1800000: it attaches to this same run, costs no quota, and prints the verdict.';
|
|
102
106
|
|
|
103
107
|
export const liveRunStop = (maxBlocks, runStatus, runDir) => stopText(
|
|
@@ -111,7 +115,7 @@ export const liveRunStop = (maxBlocks, runStatus, runDir) => stopText(
|
|
|
111
115
|
export const deadRunReason =
|
|
112
116
|
'Contract violated: status.json says state=running, but the process with this pid is dead ' +
|
|
113
117
|
'and meta.json is missing — the run is abandoned. Repeat the identical codex-bridge run ' +
|
|
114
|
-
'command, same
|
|
118
|
+
'command, same task file, with timeout of at least 1800000 and return its stdout verbatim.';
|
|
115
119
|
|
|
116
120
|
export const deadRunStop = (maxBlocks, runStatus, runDir) => stopText(
|
|
117
121
|
`The reply guard stopped the session: the dispatcher responded ${maxBlocks} times for a run it did not complete.`,
|
|
@@ -125,7 +129,7 @@ export const deadRunStop = (maxBlocks, runStatus, runDir) => stopText(
|
|
|
125
129
|
|
|
126
130
|
export const abandonedRunReason =
|
|
127
131
|
'Contract violated: status.json says state=abandoned — the runner died without a verdict. ' +
|
|
128
|
-
'Repeat the identical codex-bridge run command, same
|
|
132
|
+
'Repeat the identical codex-bridge run command, same task file, with timeout of at least ' +
|
|
129
133
|
'1800000 and return its stdout verbatim.';
|
|
130
134
|
|
|
131
135
|
export const abandonedRunStop = (maxBlocks, runStatus, runDir) => stopText(
|
|
@@ -144,7 +148,7 @@ export const missingMetaReason = (runDir) =>
|
|
|
144
148
|
|
|
145
149
|
export const missingDiscoveredMetaStop = (maxBlocks, discoveredStatus, runDir) => stopText(
|
|
146
150
|
`The reply guard stopped the session: the dispatcher responded ${maxBlocks} times ` +
|
|
147
|
-
'without naming the
|
|
151
|
+
'without naming the run its receipt names.',
|
|
148
152
|
`status.json says state=${fact(discoveredStatus?.state)}, status=${fact(discoveredStatus?.status)}, ` +
|
|
149
153
|
'but the adjacent meta.json is missing.',
|
|
150
154
|
discoveredStatus,
|
|
@@ -158,8 +162,8 @@ export const statusMismatchReason = (claimed, meta, discoveredStatus, runDir) =>
|
|
|
158
162
|
'its output verbatim without substituting your own judgment.';
|
|
159
163
|
|
|
160
164
|
export const statusMismatchStop = (maxBlocks, claimed, meta, discoveredStatus, runDir) => stopText(
|
|
161
|
-
`The reply guard stopped the session: the dispatcher contradicted the
|
|
162
|
-
`status.json says state=${fact(discoveredStatus
|
|
165
|
+
`The reply guard stopped the session: the dispatcher contradicted the run its receipt names ${maxBlocks} times.`,
|
|
166
|
+
`status.json says state=${fact(discoveredStatus?.state)}, status=${fact(discoveredStatus?.status)}; ` +
|
|
163
167
|
`meta.json says status=${meta.status}, but the reply says ${claimed}.`,
|
|
164
168
|
discoveredStatus,
|
|
165
169
|
runDir,
|
|
@@ -167,12 +171,12 @@ export const statusMismatchStop = (maxBlocks, claimed, meta, discoveredStatus, r
|
|
|
167
171
|
|
|
168
172
|
export const omittedDiscoveredRunReason = (meta, discoveredStatus, runDir) =>
|
|
169
173
|
`Contract violated: the response omitted RUN=${runDir}; status.json says ` +
|
|
170
|
-
`state=${fact(discoveredStatus
|
|
174
|
+
`state=${fact(discoveredStatus?.state)}, status=${fact(discoveredStatus?.status)}, and meta.json ` +
|
|
171
175
|
`says status=${fact(meta.status)}. Return the runner stdout verbatim.`;
|
|
172
176
|
|
|
173
177
|
export const omittedDiscoveredRunStop = (maxBlocks, meta, discoveredStatus, runDir) => stopText(
|
|
174
|
-
`The reply guard stopped the session: the dispatcher omitted the
|
|
175
|
-
`status.json says state=${fact(discoveredStatus
|
|
178
|
+
`The reply guard stopped the session: the dispatcher omitted the run its receipt names ${maxBlocks} times.`,
|
|
179
|
+
`status.json says state=${fact(discoveredStatus?.state)}, status=${fact(discoveredStatus?.status)}; ` +
|
|
176
180
|
`meta.json says status=${fact(meta.status)}.`,
|
|
177
181
|
discoveredStatus,
|
|
178
182
|
runDir,
|