@nanobpm/nano-workforce 0.188.1 → 0.189.1
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 +12 -0
- package/SPEC.md +16 -0
- package/app/adjudications.test.ts +735 -0
- package/app/adjudications.ts +378 -0
- package/app/agentCompletion.test.ts +282 -10
- package/app/agentCompletion.ts +163 -23
- package/app/agentic/cockpit/mount.test.ts +50 -0
- package/app/agentic/cockpit/supply-render.test.ts +20 -0
- package/app/agentic/cockpit/supply-render.ts +15 -0
- package/app/agentic/cockpit/supply-view.ts +19 -2
- package/app/agentic/permission-bridge.test.ts +2 -2
- package/app/agentic/vocab/demand-report.test.ts +66 -1
- package/app/agentic/vocab/demand-report.ts +54 -6
- package/app/answer-escalation.test.ts +415 -2
- package/app/answerContextMapping.test.ts +83 -0
- package/app/contracts.ts +24 -0
- package/app/convergenceAdjudicationResume.test.ts +274 -0
- package/app/github.ts +10 -0
- package/app/harnessProtocol.test.ts +170 -0
- package/app/harnessProtocol.ts +312 -0
- package/app/mcpToolSurface.ts +7 -1
- package/app/service.test.ts +178 -1
- package/app/service.ts +104 -4
- package/app/terminalReaderBehaviour.test.ts +21 -0
- package/db/migrations/107_worker_harness_protocol.sql +30 -0
- package/db/migrations/109_pr_adjudications.sql +61 -0
- package/db/migrations/110_task_completions_auto_applied.sql +34 -0
- package/openapi.yaml +71 -1
- package/operations/completeUserTask.test.ts +5 -5
- package/operations/enrolAgenticWorker.test.ts +84 -0
- package/operations/enrolAgenticWorker.ts +67 -7
- package/operations/getAgenticRegistry.ts +1 -1
- package/operations/getAgenticSupply.test.ts +80 -0
- package/operations/getAgenticSupply.ts +15 -3
- package/operations/listEscalations.test.ts +1 -1
- package/package.json +1 -1
- package/pages/cockpit/mount.js +18 -0
- package/resources/processes/convergence-loop.bpmn +9 -0
- package/resources/processes/merge-loop.bpmn +1 -0
- package/test/worldDb.ts +6 -0
- package/workers/answer-escalation/worker.ts +191 -11
|
@@ -111,6 +111,56 @@ test("the rendered transcript region sits directly beneath the Workers — suppl
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
// #802 — the DEPLOYED browser twin (mount.js) must surface a STALE harness as a distinct badge, and
|
|
115
|
+
// a healthy one with none. The typed renderer (`supply-render.ts`) has its own test, but the twin is
|
|
116
|
+
// hand-maintained and previously had no non-empty supply-row coverage, so it could silently stop
|
|
117
|
+
// surfacing stale harnesses while the typed test stayed green.
|
|
118
|
+
test("#802: mount.js renders a stale-harness badge for a stale worker and none for a healthy one", async () => {
|
|
119
|
+
const worker = (instance: string, harnessStale: boolean, harnessProtocol?: number) => ({
|
|
120
|
+
instance,
|
|
121
|
+
identity: "senior",
|
|
122
|
+
stream: instance,
|
|
123
|
+
family: "senior",
|
|
124
|
+
host: "h1",
|
|
125
|
+
jobKeys: [],
|
|
126
|
+
live: true,
|
|
127
|
+
staleMs: 0,
|
|
128
|
+
harnessStale,
|
|
129
|
+
...(harnessProtocol !== undefined ? { harnessProtocol } : {}),
|
|
130
|
+
});
|
|
131
|
+
const workers = [worker("wk-stale", true, 0), worker("wk-ok", false, 3)];
|
|
132
|
+
const report = { count: workers.length, workers, leaves: [{ token: "senior", workers }], correlations: [] };
|
|
133
|
+
const restore = installEnv((url) => {
|
|
134
|
+
const ok = (body: unknown) => Promise.resolve({ ok: true, status: 200, json: async () => body });
|
|
135
|
+
if (url.includes("/supply")) return ok(report);
|
|
136
|
+
if (url.includes("/agent-instances")) return ok({ count: 0, instances: [] });
|
|
137
|
+
if (url.includes("/transcripts")) return ok({ sessions: [] });
|
|
138
|
+
return Promise.resolve({ ok: false, status: 404, json: async () => ({}) });
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
const { mountCockpit } = await import("../../../pages/cockpit/mount.js");
|
|
142
|
+
const handle = mountCockpit(document.getElementById("root"), OPTS);
|
|
143
|
+
try {
|
|
144
|
+
await handle.refresh();
|
|
145
|
+
const staleRow = document.querySelector('.cockpit-supply-worker[data-worker="wk-stale"]');
|
|
146
|
+
const okRow = document.querySelector('.cockpit-supply-worker[data-worker="wk-ok"]');
|
|
147
|
+
assert(staleRow != null && okRow != null, "both worker rows rendered");
|
|
148
|
+
const badge = staleRow?.querySelector('.cockpit-supply-harness-stale[data-harness-stale="true"]');
|
|
149
|
+
assert(badge != null, "the stale worker carries the harness-stale badge");
|
|
150
|
+
assertEquals(badge?.textContent, "stale harness (v0)", "the badge shows the advertised protocol");
|
|
151
|
+
assertEquals(
|
|
152
|
+
okRow?.querySelector(".cockpit-supply-harness-stale"),
|
|
153
|
+
null,
|
|
154
|
+
"the healthy worker carries no harness-stale badge",
|
|
155
|
+
);
|
|
156
|
+
} finally {
|
|
157
|
+
handle.dispose();
|
|
158
|
+
}
|
|
159
|
+
} finally {
|
|
160
|
+
restore();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
114
164
|
test("live drill renders the transcript — a nwfTranscriptEvent chunk is never surfaced verbatim", async () => {
|
|
115
165
|
const restore = installEnv(fetchStub());
|
|
116
166
|
try {
|
|
@@ -140,3 +140,23 @@ test("H6: a worker with no correlation renders an em-dash process cell", () => {
|
|
|
140
140
|
assert.equal(cell?.text(), "—");
|
|
141
141
|
assert.equal(cell?.getAttribute("data-correlations"), "0");
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
test("#802: renders a stale-harness badge for a worker below the minimum / with no advertised protocol", () => {
|
|
145
|
+
const host = new FakeElement("body");
|
|
146
|
+
const staleWorker = { instance: "wk-old", identity: "leaf-1", stream: "wk-old", family: "senior", host: "h1", jobKeys: [], live: true, staleMs: 0, harnessStale: true };
|
|
147
|
+
const okWorker = { instance: "wk-ok", identity: "leaf-1", stream: "wk-ok", family: "senior", host: "h2", jobKeys: [], live: true, staleMs: 0, harnessStale: false, harnessProtocol: 3 };
|
|
148
|
+
const report: SupplyReport = {
|
|
149
|
+
count: 2,
|
|
150
|
+
workers: [staleWorker, okWorker],
|
|
151
|
+
leaves: [{ token: "leaf-1", workers: [staleWorker, okWorker] }],
|
|
152
|
+
};
|
|
153
|
+
renderSupply(host, doc, supplyView(report));
|
|
154
|
+
|
|
155
|
+
const rowOld = host.byData("worker", "wk-old")[0];
|
|
156
|
+
assert.equal(rowOld?.getAttribute("data-harness-stale"), "true");
|
|
157
|
+
assert.equal(rowOld?.byClass("cockpit-supply-harness-stale").length, 1, "stale harness badge rendered");
|
|
158
|
+
|
|
159
|
+
const rowOk = host.byData("worker", "wk-ok")[0];
|
|
160
|
+
assert.equal(rowOk?.getAttribute("data-harness-stale"), "false");
|
|
161
|
+
assert.equal(rowOk?.byClass("cockpit-supply-harness-stale").length, 0, "no badge for a healthy harness");
|
|
162
|
+
});
|
|
@@ -51,6 +51,7 @@ function workerRow(doc: DocumentLike, worker: SupplyWorkerView, options: RenderS
|
|
|
51
51
|
row.setAttribute("data-worker", worker.instance);
|
|
52
52
|
row.setAttribute("data-liveness", worker.liveness);
|
|
53
53
|
row.setAttribute("data-stream", worker.stream);
|
|
54
|
+
row.setAttribute("data-harness-stale", String(worker.harnessStale));
|
|
54
55
|
|
|
55
56
|
const nameCell = el(doc, "td", "cockpit-td cockpit-supply-name");
|
|
56
57
|
nameCell.appendChild(dot(doc, worker.liveness));
|
|
@@ -82,6 +83,20 @@ function workerRow(doc: DocumentLike, worker: SupplyWorkerView, options: RenderS
|
|
|
82
83
|
}
|
|
83
84
|
nameCell.appendChild(drill);
|
|
84
85
|
}
|
|
86
|
+
// A stale harness silently swallows machine-readable artifacts (issue #802) — surface it as a
|
|
87
|
+
// distinct badge so the operator can drain/upgrade the worker. Separate from the liveness dot,
|
|
88
|
+
// which grades heartbeat recency, not harness capability.
|
|
89
|
+
if (worker.harnessStale) {
|
|
90
|
+
const badge = el(
|
|
91
|
+
doc,
|
|
92
|
+
"span",
|
|
93
|
+
"cockpit-supply-harness-stale",
|
|
94
|
+
worker.harnessProtocol === undefined ? "stale harness" : `stale harness (v${worker.harnessProtocol})`,
|
|
95
|
+
);
|
|
96
|
+
badge.setAttribute("data-harness-stale", "true");
|
|
97
|
+
badge.setAttribute("title", "Harness protocol below the configured minimum (or none advertised); jobs may dead-end.");
|
|
98
|
+
nameCell.appendChild(badge);
|
|
99
|
+
}
|
|
85
100
|
row.appendChild(nameCell);
|
|
86
101
|
|
|
87
102
|
row.appendChild(el(doc, "td", "cockpit-td cockpit-supply-family", worker.family));
|
|
@@ -38,9 +38,16 @@ export interface SupplyWorkerReport {
|
|
|
38
38
|
readonly live: boolean;
|
|
39
39
|
/** How long since the worker's last liveness refresh, in ms. */
|
|
40
40
|
readonly staleMs: number;
|
|
41
|
+
/**
|
|
42
|
+
* Whether the worker's HARNESS is stale — its advertised harness protocol is below the configured
|
|
43
|
+
* minimum, or it advertised none at all (issue #802). Distinct from {@link Liveness} `"stale"`,
|
|
44
|
+
* which grades heartbeat recency. A stale harness silently swallows machine-readable artifacts
|
|
45
|
+
* (AgentInstance / transcript / result envelope), so its jobs dead-end at human escalations.
|
|
46
|
+
*/
|
|
47
|
+
readonly harnessStale?: boolean;
|
|
48
|
+
/** The harness protocol version the worker advertised at enrolment, if any (issue #802). */
|
|
49
|
+
readonly harnessProtocol?: number;
|
|
41
50
|
}
|
|
42
|
-
|
|
43
|
-
/** The supply registered under one leaf token. */
|
|
44
51
|
export interface SupplyLeafReport {
|
|
45
52
|
readonly token: string;
|
|
46
53
|
readonly workers: readonly SupplyWorkerReport[];
|
|
@@ -128,6 +135,10 @@ export interface SupplyWorkerView {
|
|
|
128
135
|
readonly liveness: Liveness;
|
|
129
136
|
/** How long since the last liveness refresh, in ms. */
|
|
130
137
|
readonly staleMs: number;
|
|
138
|
+
/** Whether the worker's harness protocol is stale (below minimum / absent) — issue #802. */
|
|
139
|
+
readonly harnessStale: boolean;
|
|
140
|
+
/** The advertised harness protocol version, if any (issue #802). */
|
|
141
|
+
readonly harnessProtocol?: number;
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
/** One leaf-token section in the renderable supply view. */
|
|
@@ -212,6 +223,12 @@ function workerView(
|
|
|
212
223
|
correlations,
|
|
213
224
|
liveness: liveness(worker, staleAfterMs),
|
|
214
225
|
staleMs: worker.staleMs,
|
|
226
|
+
// Fail loud: a report without a harness verdict (an older/cached response) has no trustworthy
|
|
227
|
+
// protocol assessment, so keep the worker visible as STALE until the server supplies a healthy one
|
|
228
|
+
// — mirrors the server's `harness?.stale ?? true` (issue #802). Defaulting to `false` here would
|
|
229
|
+
// let a cached response hide exactly the workers this surface is meant to expose.
|
|
230
|
+
harnessStale: worker.harnessStale ?? true,
|
|
231
|
+
...(worker.harnessProtocol !== undefined ? { harnessProtocol: worker.harnessProtocol } : {}),
|
|
215
232
|
};
|
|
216
233
|
}
|
|
217
234
|
|
|
@@ -141,7 +141,7 @@ test("escalate REQUEST → Tasks-inbox row → operator ALLOW via the completion
|
|
|
141
141
|
assertEquals(result.completion.elementId, "acp-permission");
|
|
142
142
|
assertEquals(completed.length, 1);
|
|
143
143
|
assertEquals(completed[0].userTaskKey, "ut-perm-1");
|
|
144
|
-
assertEquals(completed[0].variables, { optionId: "allow", allowed: true });
|
|
144
|
+
assertEquals(completed[0].variables, { optionId: "allow", allowed: true, completedUserTaskKey: "ut-perm-1", completedCompletionId: 1 });
|
|
145
145
|
assertEquals(stores.task_completions.rows.length, 1);
|
|
146
146
|
assertEquals(stores.task_completions.rows[0].actor_kind, "human");
|
|
147
147
|
|
|
@@ -173,7 +173,7 @@ test("escalate REQUEST → operator DENY via the completion door → RESOLUTION
|
|
|
173
173
|
});
|
|
174
174
|
|
|
175
175
|
assertEquals(result.completion.ok, true);
|
|
176
|
-
assertEquals(completed[0].variables, { optionId: "deny", allowed: false });
|
|
176
|
+
assertEquals(completed[0].variables, { optionId: "deny", allowed: false, completedUserTaskKey: "ut-perm-2", completedCompletionId: 1 });
|
|
177
177
|
assertEquals(frames.length, 1);
|
|
178
178
|
const resolution = decodeResolution(frames[0]);
|
|
179
179
|
assertEquals(resolution.callId, "job-deny");
|
|
@@ -5,8 +5,11 @@ import { test } from "node:test";
|
|
|
5
5
|
import { assert, assertEquals } from "#test-assert";
|
|
6
6
|
import type { TaskDefinitionLeaf } from "@nanobpm/agentic/demand";
|
|
7
7
|
import type { RegisteredWorker } from "@nanobpm/agentic/vocab";
|
|
8
|
+
import { HarnessProtocolRegistry } from "../../harnessProtocol.ts";
|
|
9
|
+
import { noopLog } from "../../../test/log.ts";
|
|
10
|
+
import { memDataFor } from "../../../test/worldDb.ts";
|
|
8
11
|
import { CREW_VOCAB_VERSION } from "./crew-vocab.ts";
|
|
9
|
-
import { buildRegistryReport, engineRestAddress, toWireReport } from "./demand-report.ts";
|
|
12
|
+
import { buildRegistryReport, computeRegistryReport, engineRestAddress, toWireReport } from "./demand-report.ts";
|
|
10
13
|
|
|
11
14
|
const NOW = new Date(0);
|
|
12
15
|
// A demanded taskDefinition leaf. `agentic` is the structural signal the engine reads from a task's
|
|
@@ -142,3 +145,65 @@ test("engineRestAddress strips trailing slashes from the derived NANOBPMN_BASE_U
|
|
|
142
145
|
else process.env.NANOBPMN_BASE_URL = prevBase;
|
|
143
146
|
}
|
|
144
147
|
});
|
|
148
|
+
|
|
149
|
+
test("#802: toWireReport carries staleWorkers through, omitting harnessProtocol when absent", () => {
|
|
150
|
+
const base = buildRegistryReport({ taskDefinitions: [], workers: [seniorImpl] });
|
|
151
|
+
const report = {
|
|
152
|
+
...base,
|
|
153
|
+
staleWorkers: [
|
|
154
|
+
{ instance: "wk-old", stale: true },
|
|
155
|
+
{ instance: "wk-low", stale: true, harnessProtocol: 0 },
|
|
156
|
+
],
|
|
157
|
+
};
|
|
158
|
+
const wire = toWireReport(report);
|
|
159
|
+
assertEquals(wire.staleWorkers, [
|
|
160
|
+
{ instance: "wk-old", stale: true },
|
|
161
|
+
{ instance: "wk-low", stale: true, harnessProtocol: 0 },
|
|
162
|
+
]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("#802: toWireReport omits staleWorkers entirely when the report has none (supply-only build)", () => {
|
|
166
|
+
const report = buildRegistryReport({ taskDefinitions: [], workers: [seniorImpl] });
|
|
167
|
+
assertEquals("staleWorkers" in toWireReport(report), false);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const HARNESS_MIGRATIONS = ["107_worker_harness_protocol.sql"];
|
|
171
|
+
|
|
172
|
+
test("#802: computeRegistryReport threads live workers through the registry into staleWorkers", async () => {
|
|
173
|
+
// Exercises the real assessment→staleWorkers wiring end-to-end against a mounted registry (migration
|
|
174
|
+
// 107) rather than hand-building `staleWorkers` before `toWireReport`: a regression in passing the
|
|
175
|
+
// live workers through `assessWorkersWithAvailability` would otherwise stay green.
|
|
176
|
+
const { data } = memDataFor(HARNESS_MIGRATIONS);
|
|
177
|
+
const reg = new HarnessProtocolRegistry(data);
|
|
178
|
+
await reg.recordEnrolment("w-front", 5); // healthy (>= default min 1)
|
|
179
|
+
await reg.recordEnrolment("w-kimi", 0); // below minimum → stale
|
|
180
|
+
// seniorImpl ("w-senior") never enrolled a protocol → absent → stale.
|
|
181
|
+
const report = await computeRegistryReport(noopLog(), data, [plannerFrontier, plannerKimi, seniorImpl]);
|
|
182
|
+
assertEquals(report.staleWorkers, [
|
|
183
|
+
{ instance: "w-kimi", stale: true, harnessProtocol: 0 },
|
|
184
|
+
{ instance: "w-senior", stale: true },
|
|
185
|
+
]);
|
|
186
|
+
// The stale-harness condition is folded into the OVERALL status so the board's status pill (which
|
|
187
|
+
// renders only `report.status`, not `staleWorkers`) turns RED rather than staying green (issue #802).
|
|
188
|
+
assertEquals(report.status, "red");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("#802: computeRegistryReport keeps status green when every enrolled harness is healthy", async () => {
|
|
192
|
+
// Guards the fold's other edge: a mounted registry with NO stale workers must NOT force `red` — the
|
|
193
|
+
// overall status stays whatever demand×supply produced (here green, empty demand / no missing).
|
|
194
|
+
const { data } = memDataFor(HARNESS_MIGRATIONS);
|
|
195
|
+
const reg = new HarnessProtocolRegistry(data);
|
|
196
|
+
await reg.recordEnrolment("w-front", 5);
|
|
197
|
+
await reg.recordEnrolment("w-senior", 5);
|
|
198
|
+
const report = await computeRegistryReport(noopLog(), data, [plannerFrontier, seniorImpl]);
|
|
199
|
+
assertEquals(report.staleWorkers, []);
|
|
200
|
+
assertEquals(report.status, "green");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("#802: computeRegistryReport OMITS staleWorkers when the registry cannot be consulted", async () => {
|
|
204
|
+
// A legacy DB predating migration 107: the bounded read throws → registryAvailable=false, so the
|
|
205
|
+
// report must omit staleWorkers rather than mislabel an outage as a fleet-wide drain signal.
|
|
206
|
+
const { data } = memDataFor([]);
|
|
207
|
+
const report = await computeRegistryReport(noopLog(), data, [plannerFrontier, seniorImpl]);
|
|
208
|
+
assertEquals("staleWorkers" in report, false, "an outage omits staleWorkers, not report every worker stale");
|
|
209
|
+
});
|
|
@@ -18,9 +18,10 @@ import {
|
|
|
18
18
|
type TaskDefinitionLeaf,
|
|
19
19
|
} from "@nanobpm/agentic/demand";
|
|
20
20
|
import type { RegisteredWorker } from "@nanobpm/agentic/vocab";
|
|
21
|
-
import type { Logger } from "@nanobpm/urban";
|
|
22
|
-
import type { RegistryReport as WireRegistryReport } from "../../../nano-generated/api-io.d.ts";
|
|
21
|
+
import type { DataLayer, Logger } from "@nanobpm/urban";
|
|
22
|
+
import type { StaleWorker, RegistryReport as WireRegistryReport } from "../../../nano-generated/api-io.d.ts";
|
|
23
23
|
import { resolveEngineAddress } from "../../enginePreflight.ts";
|
|
24
|
+
import { assessWorkersWithAvailability, type HarnessAssessment } from "../../harnessProtocol.ts";
|
|
24
25
|
import { envVar } from "../../version.ts";
|
|
25
26
|
import { currentPresenceRegistry } from "../families/presence.family.ts";
|
|
26
27
|
import { CREW_VOCAB_VERSION, crewResolver } from "./crew-vocab.ts";
|
|
@@ -38,6 +39,15 @@ export interface RegistryReport extends DemandSupplyReport {
|
|
|
38
39
|
* demand is unavailable rather than silently showing "no demand".
|
|
39
40
|
*/
|
|
40
41
|
readonly demandUnavailable: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The enrolled workers whose harness is STALE (issue #802) — below the configured minimum protocol,
|
|
44
|
+
* or advertising no version at all — so they may silently swallow AgentInstance / transcript /
|
|
45
|
+
* result-envelope artifacts and should be drained. Empty when every supplied worker is healthy;
|
|
46
|
+
* omitted only when the harness-protocol registry could not be consulted. When non-empty, the
|
|
47
|
+
* overall {@link DemandSupplyReport.status} is folded to `red` (a drain signal), because the board
|
|
48
|
+
* renders only `status` and does not surface this list on its own.
|
|
49
|
+
*/
|
|
50
|
+
readonly staleWorkers?: readonly HarnessAssessment[];
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
/**
|
|
@@ -159,15 +169,53 @@ export function toWireReport(report: RegistryReport): WireRegistryReport {
|
|
|
159
169
|
})),
|
|
160
170
|
},
|
|
161
171
|
status: report.status,
|
|
172
|
+
...(report.staleWorkers !== undefined
|
|
173
|
+
? {
|
|
174
|
+
staleWorkers: report.staleWorkers.map((w): StaleWorker => {
|
|
175
|
+
const out: StaleWorker = { instance: w.instance, stale: w.stale };
|
|
176
|
+
if (w.harnessProtocol !== undefined) out.harnessProtocol = w.harnessProtocol;
|
|
177
|
+
return out;
|
|
178
|
+
}),
|
|
179
|
+
}
|
|
180
|
+
: {}),
|
|
162
181
|
};
|
|
163
182
|
}
|
|
164
183
|
|
|
165
184
|
/**
|
|
166
185
|
* The composition path the `getAgenticRegistry` operation calls: read demand from the engine, read
|
|
167
|
-
* supply from the presence registry, and build the report.
|
|
168
|
-
* degrades to a supply-only report
|
|
186
|
+
* supply from the presence registry, assess harness staleness (issue #802), and build the report.
|
|
187
|
+
* Never throws for an engine outage — it degrades to a supply-only report; the staleness assessment
|
|
188
|
+
* is best-effort and omitted when no data layer is mounted.
|
|
189
|
+
*
|
|
190
|
+
* `workers` defaults to the live presence feed ({@link supplyWorkers}); it is injectable so a test can
|
|
191
|
+
* drive the full assessment→`staleWorkers` wiring against a real registry without mounting the global
|
|
192
|
+
* presence family.
|
|
169
193
|
*/
|
|
170
|
-
export async function computeRegistryReport(
|
|
194
|
+
export async function computeRegistryReport(
|
|
195
|
+
log?: Logger,
|
|
196
|
+
data?: DataLayer,
|
|
197
|
+
workers: readonly RegisteredWorker[] = supplyWorkers(),
|
|
198
|
+
): Promise<RegistryReport> {
|
|
171
199
|
const taskDefinitions = await readDemand(log);
|
|
172
|
-
|
|
200
|
+
const report = buildRegistryReport({ taskDefinitions, workers });
|
|
201
|
+
if (!data) return report;
|
|
202
|
+
const { registryAvailable, assessments } = await assessWorkersWithAvailability(
|
|
203
|
+
data,
|
|
204
|
+
workers.map((w) => w.instance),
|
|
205
|
+
);
|
|
206
|
+
// The registry could not be consulted (read outage / legacy DB): OMIT `staleWorkers` per the report
|
|
207
|
+
// contract, so an operator cannot mistake "the registry is unavailable" for "every harness is stale"
|
|
208
|
+
// and treat an outage as a fleet-wide drain signal (issue #802).
|
|
209
|
+
if (!registryAvailable) return report;
|
|
210
|
+
const staleWorkers = [...assessments.values()]
|
|
211
|
+
.filter((a) => a.stale)
|
|
212
|
+
.sort((a, b) => a.instance.localeCompare(b.instance));
|
|
213
|
+
// Fold the stale-harness condition into the OVERALL status (issue #802). The board renders only
|
|
214
|
+
// `report.status` as its overall pill and does not surface `staleWorkers`, so without this a
|
|
215
|
+
// healthy-demand report (status `green`/`amber`) would still show green while enrolled harnesses are
|
|
216
|
+
// stale — silently swallowing AgentInstance / transcript / result-envelope artifacts. A stale
|
|
217
|
+
// harness is a drain signal, so any stale worker forces the overall status to `red` (`red` is
|
|
218
|
+
// already the worst grade, so this never downgrades an existing `red`).
|
|
219
|
+
const status = staleWorkers.length > 0 ? "red" : report.status;
|
|
220
|
+
return { ...report, status, staleWorkers };
|
|
173
221
|
}
|