@staix/agent-hub 0.20.0 → 0.22.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/CHANGELOG.md +66 -3
- package/README.md +2 -2
- package/docs/agent-notes/README.md +11 -0
- package/docs/agent-notes/adapters.md +7 -3
- package/docs/agent-notes/benchmarks.md +27 -0
- package/docs/agent-notes/budget.md +2 -1
- package/docs/agent-notes/bus.md +3 -3
- package/docs/agent-notes/daemon.md +17 -8
- package/docs/agent-notes/local-worker.md +4 -0
- package/docs/agent-notes/models.md +2 -0
- package/docs/agent-notes/tasks.md +17 -9
- package/docs/agent-notes/tests.md +9 -2
- package/docs/bench.md +1 -1
- package/docs/data-policy.md +2 -0
- package/docs/dogfood.md +39 -4
- package/docs/events.md +2 -1
- package/docs/memory.md +9 -3
- package/docs/operations.md +610 -62
- package/docs/profiles.md +13 -1
- package/docs/quickstart.md +18 -9
- package/docs/research.md +7 -4
- package/docs/sdlc.md +54 -10
- package/docs/security.md +396 -34
- package/docs/smoke.md +133 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +1101 -22
- package/docs/verification/2026-10-27-0.19.0.md +5 -5
- package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-bash-permission.json +635 -0
- package/docs/verification/2026-10-28-claude-2.1.311-2.1.312-permission-transport.json +403 -0
- package/docs/verification/2026-10-28-claude-implementer-permission-triage.md +123 -0
- package/docs/verification/2026-10-28-jcode-0.99.1-usage.md +41 -0
- package/docs/verification/2026-10-28-realpath-name-audit.md +222 -0
- package/docs/verification/2026-10-28-routing-f0bb8ad0.md +1000 -0
- package/docs/verification/2026-10-29-0.21.0.md +61 -0
- package/docs/verification/2026-10-29-model-effort-capabilities.md +123 -0
- package/docs/verification/2026-10-29-modelrouting-b152ddb9.md +953 -0
- package/docs/verification/2026-10-30-0.22.0.md +49 -0
- package/docs/verification/2026-10-30-445-capability-probes.md +172 -0
- package/docs/verification/2026-10-30-bus-delivery-operational-settings.md +77 -0
- package/docs/verification/2026-10-30-jcode-ac4.md +24 -0
- package/docs/verification/2026-10-30-jcode-ac6.md +52 -0
- package/docs/verification/2026-10-30-recovery-wait-settings.md +141 -0
- package/docs/verification/claude-implementer-permission-triage.ts +160 -0
- package/docs/verification/claude-permission-transport-native.py +71 -14
- package/docs/verification/profiles/YYYY-MM-DD-research.md +1 -1
- package/docs/verified.json +15 -15
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1016 -280
- package/src/adapters/acp.ts +79 -3
- package/src/adapters/channel-peer.ts +4 -0
- package/src/adapters/claude-channel.ts +13 -6
- package/src/adapters/claude-implementer.ts +60 -7
- package/src/adapters/codex-appserver.ts +77 -4
- package/src/adapters/conformance/fakes/jcode-agent.ts +6 -2
- package/src/adapters/conformance/kit.ts +3 -9
- package/src/adapters/definitions/builtin-metadata.ts +3 -114
- package/src/adapters/definitions/native-config-defaults.ts +1 -1
- package/src/adapters/definitions/native-config-policy.ts +3 -2
- package/src/adapters/definitions/native-context.ts +12 -0
- package/src/adapters/external-metadata.ts +48 -0
- package/src/adapters/external-modules.ts +63 -3
- package/src/adapters/jcode.ts +6 -2
- package/src/adapters/local-worker.ts +40 -25
- package/src/adapters/native-proof-ports.ts +10 -0
- package/src/adapters/registry-types.ts +4 -0
- package/src/broker/protocol.ts +203 -0
- package/src/broker/server.ts +314 -0
- package/src/broker/store.ts +548 -0
- package/src/cli/bench.ts +3 -2
- package/src/cli/broker.ts +58 -0
- package/src/cli/conductor-recovery.ts +142 -0
- package/src/cli/configured-acp-entry.ts +26 -6
- package/src/cli/console-state.ts +59 -17
- package/src/cli/console.ts +17 -2
- package/src/cli/facts-hook.ts +3 -1
- package/src/cli/help.ts +29 -16
- package/src/cli/identity.ts +25 -10
- package/src/cli/init.ts +34 -7
- package/src/cli/intake-output.ts +33 -0
- package/src/cli/learn-output.ts +38 -0
- package/src/cli/main.ts +334 -73
- package/src/cli/output.ts +41 -1
- package/src/cli/recovery-package.ts +18 -1
- package/src/cli/recovery-waits.ts +26 -0
- package/src/cli/role-holders.ts +35 -0
- package/src/cli/room.ts +304 -0
- package/src/cli/say-input.ts +16 -0
- package/src/cli/schedule-output.ts +49 -0
- package/src/cli/sdlc-output.ts +44 -6
- package/src/cli/smoke-pty-bridge.ts +82 -0
- package/src/cli/smoke-pty.ts +153 -0
- package/src/cli/smoke-run.ts +139 -0
- package/src/cli/smoke-target.ts +130 -0
- package/src/cli/source-output.ts +29 -0
- package/src/cli/task-output.ts +3 -2
- package/src/cli/upgrade-runtime.ts +112 -28
- package/src/cli/upgrade.ts +68 -8
- package/src/hub/ask.ts +76 -12
- package/src/hub/board.ts +132 -22
- package/src/hub/bus.ts +416 -65
- package/src/hub/child-process.ts +2 -0
- package/src/hub/ci-watch.ts +42 -20
- package/src/hub/claude-failure-stage.ts +85 -0
- package/src/hub/claude-shell-wrapper.ts +11 -5
- package/src/hub/conductor.ts +17 -4
- package/src/hub/config-apply.ts +34 -6
- package/src/hub/config-reload.ts +7 -5
- package/src/hub/config-trust.ts +7 -1
- package/src/hub/context-window.ts +33 -3
- package/src/hub/control-client.ts +14 -2
- package/src/hub/corpus-config.ts +34 -0
- package/src/hub/corpus-evidence.ts +184 -0
- package/src/hub/corpus.ts +373 -0
- package/src/hub/cost-governance.ts +26 -0
- package/src/hub/daemon.ts +1209 -183
- package/src/hub/deliverable-files.ts +8 -2
- package/src/hub/delivery-journal.ts +200 -73
- package/src/hub/dogfood-profile.ts +32 -5
- package/src/hub/dogfood-read.ts +6 -2
- package/src/hub/envelope.ts +11 -0
- package/src/hub/eval-gate.ts +131 -0
- package/src/hub/events.ts +1 -1
- package/src/hub/external-instance.ts +69 -0
- package/src/hub/facts.ts +15 -5
- package/src/hub/folder-trigger.ts +223 -0
- package/src/hub/hub-tools.ts +2 -1
- package/src/hub/inference.ts +20 -5
- package/src/hub/instance-config.ts +69 -3
- package/src/hub/instance-execution.ts +6 -2
- package/src/hub/instances.ts +32 -4
- package/src/hub/intake.ts +125 -0
- package/src/hub/learning.ts +117 -0
- package/src/hub/lifecycle.ts +7 -3
- package/src/hub/limits.ts +12 -5
- package/src/hub/native-permission-request.ts +77 -0
- package/src/hub/operational-authority.ts +63 -5
- package/src/hub/operational-settings.ts +154 -0
- package/src/hub/operational-view.ts +1 -1
- package/src/hub/packs.ts +132 -0
- package/src/hub/path-policy.ts +7 -0
- package/src/hub/peer-doing.ts +130 -0
- package/src/hub/peers.ts +12 -0
- package/src/hub/queue-backpressure.ts +58 -0
- package/src/hub/read-tools.ts +2 -0
- package/src/hub/recovery-authority.ts +151 -0
- package/src/hub/recovery-policy.ts +220 -0
- package/src/hub/research.ts +24 -14
- package/src/hub/restart.ts +7 -0
- package/src/hub/routing.ts +54 -11
- package/src/hub/schedules.ts +266 -0
- package/src/hub/sdlc-merge.ts +38 -14
- package/src/hub/sdlc-metrics.ts +32 -10
- package/src/hub/sdlc-queue.ts +7 -1
- package/src/hub/sdlc-release.ts +1 -1
- package/src/hub/sdlc.ts +37 -7
- package/src/hub/security-baseline.ts +38 -0
- package/src/hub/settings.ts +27 -2
- package/src/hub/shipped-packs.ts +8 -0
- package/src/hub/snapshots.ts +72 -6
- package/src/hub/source-read.ts +160 -0
- package/src/hub/sources.ts +311 -0
- package/src/hub/tasks.ts +987 -110
- package/src/hub/workflow.ts +1 -1
- package/src/local/doc-read.ts +69 -0
- package/src/local/proxy.ts +4 -13
- package/src/local/tools.ts +37 -3
- package/src/local/web-fetch-config.ts +43 -0
- package/src/local/web-fetch.ts +183 -0
- package/src/memory/brief.ts +85 -13
- package/src/memory/capture.ts +1 -1
- package/src/rooms/application.ts +114 -0
- package/src/rooms/boundary.ts +103 -0
- package/src/rooms/commands.ts +294 -0
- package/src/rooms/crypto.ts +371 -0
- package/src/rooms/link.ts +584 -0
- package/src/rooms/policy.ts +82 -0
- package/src/rooms/remote-peer.ts +80 -0
- package/src/rooms/service.ts +426 -0
- package/src/rooms/status.ts +146 -0
- package/src/rooms/store.ts +1210 -0
- package/src/rooms/wire.ts +465 -0
- package/src/ui/index.html +91 -4
- package/templates/packs/example.pack.json +13 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { constants, lstatSync, type Stats } from "node:fs";
|
|
2
2
|
import { open } from "node:fs/promises";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { relative } from "node:path";
|
|
4
|
+
import { relative, resolve } from "node:path";
|
|
5
5
|
import { guardPath } from "../local/tools.ts";
|
|
6
|
+
import { isDenied } from "../local/deny.ts";
|
|
6
7
|
import { isHubWritePath } from "./permission-mode.ts";
|
|
7
8
|
import { realPath } from "./project.ts";
|
|
9
|
+
import { insideRoot } from "./path-policy.ts";
|
|
8
10
|
import { DELIVERABLE_LIMITS, deliverableId, type DeliverableClaim, type DeliverableRecord, type DeliverableState } from "./deliverables.ts";
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -49,7 +51,11 @@ function locate(path: string, ports: DeliverablePorts): Located {
|
|
|
49
51
|
// git file ever reaches a record (a hash of a low-entropy file confirms guesses). Compared on the resolved path, folded like the write guard.
|
|
50
52
|
let root: string;
|
|
51
53
|
try { root = realPath(ports.cwd); } catch { return refuse(path, "the project root cannot be resolved"); }
|
|
52
|
-
|
|
54
|
+
// Judged on the resolved name and on the name the caller gave (#552, the #541 class): Bun's realpath can name another hardlink of the same file.
|
|
55
|
+
const typed = relative(root, resolve(root, path));
|
|
56
|
+
const inside = insideRoot(typed);
|
|
57
|
+
if (isHubWritePath(relative(root, real)) || inside && isHubWritePath(typed)) return refuse(path, "the hub's own state is not a deliverable");
|
|
58
|
+
if (inside && isDenied(typed, [...ports.deny])) return refuse(path, "the path is on the secrets denylist");
|
|
53
59
|
let stat: Stats;
|
|
54
60
|
try { stat = lstatSync(real); }
|
|
55
61
|
catch (error) { return refuse(path, (error as NodeJS.ErrnoException).code === "ENOENT" ? "the file does not exist" : "the file cannot be read"); }
|
|
@@ -2,10 +2,12 @@ import { chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { Database } from "bun:sqlite";
|
|
4
4
|
import type { BusSnapshot } from "./bus.ts";
|
|
5
|
+
import type { QueuePressure } from "./queue-backpressure.ts";
|
|
5
6
|
import { HUB, USER, type Envelope, type Kind } from "./envelope.ts";
|
|
6
7
|
import { heldDeliveryHints } from "./delivery-hints.ts";
|
|
7
8
|
import { processLiveness } from "../pi/process-signature.ts";
|
|
8
9
|
import { HUB_DB_BUSY_TIMEOUT_MS } from "./db-timeout.ts";
|
|
10
|
+
import { parseRemotePeerId } from "../rooms/application.ts";
|
|
9
11
|
import { validateDeferredHub } from "./deferred-hub.ts";
|
|
10
12
|
|
|
11
13
|
export type JournalDeliveryState =
|
|
@@ -18,8 +20,8 @@ export type JournalDeliveryState =
|
|
|
18
20
|
| "discarded";
|
|
19
21
|
export type JournalResolution = "completed" | "retry" | "discard";
|
|
20
22
|
export const MAX_ENVELOPE_BODY = 1_000_000;
|
|
23
|
+
export const TERMINAL_CAP = 2048;
|
|
21
24
|
const SCHEMA_VERSION = 1,
|
|
22
|
-
TERMINAL_CAP = 2048,
|
|
23
25
|
MAX_ROWS = 100_000,
|
|
24
26
|
MAX_JSON = 20 * 1024 * 1024;
|
|
25
27
|
const STATES = new Set<JournalDeliveryState>([
|
|
@@ -54,6 +56,8 @@ export interface DeliveryJournalOptions {
|
|
|
54
56
|
projectId: string;
|
|
55
57
|
instanceId: string;
|
|
56
58
|
operationId?: string;
|
|
59
|
+
/** Accepted live history target; only terminal mutations apply it, other pruning keeps the legacy cap. */
|
|
60
|
+
terminalHistoryLimit?: () => number;
|
|
57
61
|
}
|
|
58
62
|
export interface JournalSnapshot {
|
|
59
63
|
schemaVersion: number;
|
|
@@ -168,6 +172,32 @@ function validateBus(value: unknown, name = "bus"): BusSnapshot {
|
|
|
168
172
|
list(b.manualPaused, `${name}.manualPaused`, 4096).forEach((id) =>
|
|
169
173
|
text(id, `${name}.manualPaused[]`, 256),
|
|
170
174
|
);
|
|
175
|
+
if (b.remoteQueueMembers !== undefined) {
|
|
176
|
+
const members = obj(b.remoteQueueMembers, `${name}.remoteQueueMembers`);
|
|
177
|
+
if (Object.keys(members).length > 12800) fail(`${name}.remoteQueueMembers is too large`);
|
|
178
|
+
for (const [peer, member] of Object.entries(members)) {
|
|
179
|
+
if (!parseRemotePeerId(peer) || !Array.isArray(queues[peer]) || typeof member !== "string" || !/^[0-9a-f]{64}$/.test(member)) fail(`${name}.remoteQueueMembers is invalid`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (b.remoteQueueRooms !== undefined) {
|
|
183
|
+
const rooms = obj(b.remoteQueueRooms, `${name}.remoteQueueRooms`);
|
|
184
|
+
if (Object.keys(rooms).length > 12800) fail(`${name}.remoteQueueRooms is too large`);
|
|
185
|
+
for (const [peer, room] of Object.entries(rooms)) {
|
|
186
|
+
if (!parseRemotePeerId(peer) || !Array.isArray(queues[peer])) fail(`${name}.remoteQueueRooms key is invalid`);
|
|
187
|
+
text(room, `${name}.remoteQueueRooms room`, 256);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (b.remoteQueueExpires !== undefined) {
|
|
191
|
+
const expiries = obj(b.remoteQueueExpires, `${name}.remoteQueueExpires`);
|
|
192
|
+
if (Object.keys(expiries).length > 4096) fail(`${name}.remoteQueueExpires is too large`);
|
|
193
|
+
let count = 0;
|
|
194
|
+
for (const [peer, values] of Object.entries(expiries)) {
|
|
195
|
+
if (!parseRemotePeerId(peer)) fail(`${name}.remoteQueueExpires key is invalid`);
|
|
196
|
+
const entries = Object.entries(obj(values, `${name}.remoteQueueExpires.${peer}`));
|
|
197
|
+
if (entries.length > 200 || (count += entries.length) > 12800) fail(`${name}.remoteQueueExpires is too large`);
|
|
198
|
+
for (const [id, expiry] of entries) { text(id, `${name}.remoteQueueExpires id`, 256); integer(expiry, `${name}.remoteQueueExpires expiry`); }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
171
201
|
const occupied = new Set<string>([
|
|
172
202
|
...(b.seen as Envelope[]).map(env => env.id),
|
|
173
203
|
...Object.values(queues).flatMap(items => (items as Envelope[]).map(env => env.id)),
|
|
@@ -241,13 +271,18 @@ export class DeliveryJournal {
|
|
|
241
271
|
readonly instanceId: string;
|
|
242
272
|
private readonly db: Database;
|
|
243
273
|
private closed = false;
|
|
274
|
+
private queueMetadataTracking = false;
|
|
275
|
+
private queueMetadataInvalidated = false;
|
|
276
|
+
private pruningDeferred = 0;
|
|
244
277
|
private readonly operationId?: string;
|
|
278
|
+
private readonly terminalHistoryLimit?: () => number;
|
|
245
279
|
constructor(options: DeliveryJournalOptions) {
|
|
246
280
|
this.file = options.file;
|
|
247
281
|
this.projectRoot = text(options.projectRoot, "projectRoot", 4096);
|
|
248
282
|
this.projectId = text(options.projectId, "projectId", 256);
|
|
249
283
|
this.instanceId = text(options.instanceId, "instanceId", 256);
|
|
250
284
|
this.operationId = options.operationId;
|
|
285
|
+
this.terminalHistoryLimit = options.terminalHistoryLimit;
|
|
251
286
|
mkdirSync(dirname(options.file), { recursive: true, mode: 0o700 });
|
|
252
287
|
this.db = new Database(options.file, { create: true });
|
|
253
288
|
this.db.run(`PRAGMA busy_timeout = ${HUB_DB_BUSY_TIMEOUT_MS}`);
|
|
@@ -323,6 +358,8 @@ export class DeliveryJournal {
|
|
|
323
358
|
this.db.run(
|
|
324
359
|
`CREATE TABLE IF NOT EXISTS deliveries (id TEXT PRIMARY KEY, project_id TEXT NOT NULL, peer TEXT NOT NULL, state TEXT NOT NULL, revision INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, originals TEXT NOT NULL, out_json TEXT NOT NULL, reason TEXT, attempt INTEGER, previous_id TEXT)`,
|
|
325
360
|
);
|
|
361
|
+
this.db.run("CREATE TABLE IF NOT EXISTS delivery_queue_meta_state (project_id TEXT PRIMARY KEY, revision INTEGER NOT NULL)");
|
|
362
|
+
this.db.run("CREATE TABLE IF NOT EXISTS delivery_queue_meta (project_id TEXT NOT NULL, peer TEXT NOT NULL, queued INTEGER NOT NULL, oldest_at INTEGER, unknown_age INTEGER NOT NULL, PRIMARY KEY(project_id,peer))");
|
|
326
363
|
this.db.run("CREATE TABLE IF NOT EXISTS task_offer_reads (project_id TEXT NOT NULL, peer TEXT NOT NULL, envelope_id TEXT NOT NULL, delivery_id TEXT NOT NULL, source TEXT NOT NULL CHECK(source IN ('adapter','reply','pull')), offer_revision INTEGER NOT NULL CHECK(offer_revision >= 1), PRIMARY KEY(project_id,peer,envelope_id))");
|
|
327
364
|
this.db.run(
|
|
328
365
|
`CREATE TABLE IF NOT EXISTS resolution_history (id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL, delivery_id TEXT NOT NULL, observed_revision INTEGER NOT NULL, action TEXT NOT NULL, reason TEXT NOT NULL, result_revision INTEGER NOT NULL, result_state TEXT NOT NULL, UNIQUE(project_id, delivery_id, observed_revision, action, reason))`,
|
|
@@ -474,11 +511,71 @@ export class DeliveryJournal {
|
|
|
474
511
|
) !== 1
|
|
475
512
|
)
|
|
476
513
|
throw new Error("delivery journal instance fence lost");
|
|
514
|
+
// New writers carry unchanged summary provenance across non-queue revisions.
|
|
515
|
+
// An older writer does not, so its next revision makes these rows unknown.
|
|
516
|
+
if (this.queueMetadataTracking) this.db.query("UPDATE delivery_queue_meta_state SET revision=? WHERE project_id=? AND revision=?")
|
|
517
|
+
.run(revision, this.projectId, revision - 1);
|
|
477
518
|
return revision;
|
|
478
519
|
}
|
|
520
|
+
|
|
521
|
+
/** Atomically accompanies the bus snapshot, never extracts body JSON on a periodic path. */
|
|
522
|
+
private writeQueueMetadata(bus: BusSnapshot, revision: number, tracking: boolean): void {
|
|
523
|
+
if (!tracking) {
|
|
524
|
+
// Invalidate once per disabled period; ordinary revisions already stale old summaries.
|
|
525
|
+
if (!this.queueMetadataInvalidated) {
|
|
526
|
+
this.db.query("UPDATE delivery_queue_meta_state SET revision=-1 WHERE project_id=? AND revision>=0").run(this.projectId);
|
|
527
|
+
this.queueMetadataInvalidated = true;
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
this.db.query("DELETE FROM delivery_queue_meta WHERE project_id=?").run(this.projectId);
|
|
532
|
+
const put = this.db.query("INSERT INTO delivery_queue_meta(project_id,peer,queued,oldest_at,unknown_age) VALUES(?,?,?,?,?)");
|
|
533
|
+
for (const [peer, queue] of Object.entries(bus.queues)) {
|
|
534
|
+
const provenance = bus.queuedAt;
|
|
535
|
+
const times = provenance && typeof provenance === "object" && !Array.isArray(provenance) && Object.hasOwn(provenance, peer) ? provenance[peer] : undefined;
|
|
536
|
+
let oldest: number | undefined, unknown = false;
|
|
537
|
+
for (const env of queue) {
|
|
538
|
+
const at = times && typeof times === "object" && !Array.isArray(times) && Object.hasOwn(times, env.id) ? times[env.id] : null;
|
|
539
|
+
if (typeof at !== "number" || !Number.isSafeInteger(at) || at <= 0) unknown = true;
|
|
540
|
+
else oldest = oldest === undefined ? at : Math.min(oldest, at);
|
|
541
|
+
}
|
|
542
|
+
put.run(this.projectId, peer, queue.length, oldest ?? null, unknown ? 1 : 0);
|
|
543
|
+
}
|
|
544
|
+
this.db.query("INSERT INTO delivery_queue_meta_state(project_id,revision) VALUES(?,?) ON CONFLICT(project_id) DO UPDATE SET revision=excluded.revision")
|
|
545
|
+
.run(this.projectId, revision);
|
|
546
|
+
this.queueMetadataInvalidated = false;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Host policy only, refreshed before receipt/reply mutations as well as snapshot writes. No SQL. */
|
|
550
|
+
setQueueMetadataTracking(tracking: boolean): void { this.queueMetadataTracking = tracking === true; }
|
|
551
|
+
|
|
552
|
+
private queueMetadataTransaction<T>(tracking: boolean, fn: () => T): T {
|
|
553
|
+
return this.transaction(() => { this.setQueueMetadataTracking(tracking); return fn(); });
|
|
554
|
+
}
|
|
555
|
+
private queueMetadataSnapshot(bus: BusSnapshot, tracking: boolean): BusSnapshot {
|
|
556
|
+
if (tracking || bus.queuedAt === undefined) return bus;
|
|
557
|
+
const legacy = { ...bus }; delete legacy.queuedAt;
|
|
558
|
+
return legacy;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Indexed per-peer pending counts/age only, independent of delivery history and payload columns. */
|
|
562
|
+
pendingQueueMetadata(peer: string): QueuePressure | undefined {
|
|
563
|
+
this.ensureOpen();
|
|
564
|
+
const row = this.db.query("SELECT m.revision,s.revision AS metadata_revision,q.peer AS metadata_peer,q.queued,q.oldest_at,q.unknown_age FROM delivery_meta m LEFT JOIN delivery_queue_meta_state s ON s.project_id=m.project_id LEFT JOIN delivery_queue_meta q ON q.project_id=m.project_id AND q.peer=? WHERE m.project_id=?")
|
|
565
|
+
.get(peer, this.projectId) as { revision: number; metadata_revision: number | null; metadata_peer: string | null; queued: number | null; oldest_at: number | null; unknown_age: number | null } | null;
|
|
566
|
+
if (!row || !Number.isSafeInteger(row.revision) || row.revision < 0 || row.metadata_revision !== row.revision) return undefined;
|
|
567
|
+
if (row.metadata_peer === null) return { queued: 0, oldestQueuedAt: null };
|
|
568
|
+
if (typeof row.queued !== "number" || !Number.isSafeInteger(row.queued) || row.queued < 0 || row.queued > 2048 || ![0, 1].includes(row.unknown_age!)) return undefined;
|
|
569
|
+
if (row.queued === 0) return row.oldest_at === null && row.unknown_age === 0 ? { queued: 0, oldestQueuedAt: null } : undefined;
|
|
570
|
+
if (row.unknown_age === 1) return { queued: row.queued };
|
|
571
|
+
if (!Number.isSafeInteger(row.oldest_at) || row.oldest_at! <= 0) return undefined;
|
|
572
|
+
return { queued: row.queued, oldestQueuedAt: row.oldest_at! };
|
|
573
|
+
}
|
|
479
574
|
transaction<T>(fn: () => T): T {
|
|
480
575
|
this.ensureOpen();
|
|
481
|
-
|
|
576
|
+
const invalidated = this.queueMetadataInvalidated;
|
|
577
|
+
try { return this.db.transaction(fn)(); }
|
|
578
|
+
catch (error) { this.queueMetadataInvalidated = invalidated; throw error; }
|
|
482
579
|
}
|
|
483
580
|
snapshot(): JournalSnapshot {
|
|
484
581
|
this.ensureOpen();
|
|
@@ -513,26 +610,34 @@ export class DeliveryJournal {
|
|
|
513
610
|
validateJournalSnapshot(result);
|
|
514
611
|
return result;
|
|
515
612
|
}
|
|
516
|
-
persistBus(bus: unknown, manualPaused: string[]): number {
|
|
613
|
+
persistBus(bus: unknown, manualPaused: string[], trackQueuePressure = false): number {
|
|
517
614
|
this.ensureOpen();
|
|
518
|
-
validateBus(bus);
|
|
615
|
+
let checkedBus = this.queueMetadataSnapshot(validateBus(bus), trackQueuePressure);
|
|
616
|
+
let encodedBus: string;
|
|
617
|
+
try { encodedBus = JSON.stringify(checkedJSON(checkedBus, "bus")); }
|
|
618
|
+
catch (error) {
|
|
619
|
+
if (checkedBus.queuedAt === undefined || !(error instanceof Error) || error.message !== "invalid delivery journal: bus is too large") throw error;
|
|
620
|
+
checkedBus = { ...checkedBus }; delete checkedBus.queuedAt;
|
|
621
|
+
encodedBus = JSON.stringify(checkedJSON(checkedBus, "bus"));
|
|
622
|
+
}
|
|
519
623
|
list(manualPaused, "manualPaused", 4096).forEach((id) =>
|
|
520
624
|
text(id, "manualPaused[]", 256),
|
|
521
625
|
);
|
|
522
|
-
return this.
|
|
626
|
+
return this.queueMetadataTransaction(trackQueuePressure, () => {
|
|
523
627
|
const revision = this.bump();
|
|
524
628
|
this.db
|
|
525
629
|
.query(
|
|
526
630
|
"UPDATE delivery_meta SET bus_snapshot = ?, manual_paused = ? WHERE project_id = ?",
|
|
527
631
|
)
|
|
528
632
|
.run(
|
|
529
|
-
|
|
633
|
+
encodedBus,
|
|
530
634
|
JSON.stringify([...new Set(manualPaused)].sort()),
|
|
531
635
|
this.projectId,
|
|
532
636
|
);
|
|
637
|
+
this.writeQueueMetadata(checkedBus, revision, trackQueuePressure);
|
|
533
638
|
this.harden();
|
|
534
639
|
return revision;
|
|
535
|
-
})
|
|
640
|
+
});
|
|
536
641
|
}
|
|
537
642
|
/** Atomically persist a bus snapshot and any fan-out rows created for that snapshot. */
|
|
538
643
|
persistBusAndDeliveries(
|
|
@@ -541,9 +646,10 @@ export class DeliveryJournal {
|
|
|
541
646
|
deliveries: Array<
|
|
542
647
|
Omit<JournalDelivery, "revision" | "updatedAt"> & { updatedAt?: number }
|
|
543
648
|
>,
|
|
649
|
+
trackQueuePressure = false,
|
|
544
650
|
): number {
|
|
545
651
|
this.ensureOpen();
|
|
546
|
-
validateBus(bus);
|
|
652
|
+
bus = this.queueMetadataSnapshot(validateBus(bus), trackQueuePressure);
|
|
547
653
|
list(manualPaused, "manualPaused", 4096).forEach((id) =>
|
|
548
654
|
text(id, "manualPaused[]", 256),
|
|
549
655
|
);
|
|
@@ -554,7 +660,7 @@ export class DeliveryJournal {
|
|
|
554
660
|
updatedAt: d.updatedAt ?? d.createdAt,
|
|
555
661
|
}),
|
|
556
662
|
);
|
|
557
|
-
return this.
|
|
663
|
+
return this.queueMetadataTransaction(trackQueuePressure, () => {
|
|
558
664
|
const revision = this.bump();
|
|
559
665
|
this.db
|
|
560
666
|
.query(
|
|
@@ -565,6 +671,7 @@ export class DeliveryJournal {
|
|
|
565
671
|
JSON.stringify([...new Set(manualPaused)].sort()),
|
|
566
672
|
this.projectId,
|
|
567
673
|
);
|
|
674
|
+
this.writeQueueMetadata(bus, revision, trackQueuePressure);
|
|
568
675
|
for (const d of deliveries)
|
|
569
676
|
this.db
|
|
570
677
|
.query(
|
|
@@ -586,68 +693,73 @@ export class DeliveryJournal {
|
|
|
586
693
|
);
|
|
587
694
|
this.harden();
|
|
588
695
|
return revision;
|
|
589
|
-
})
|
|
696
|
+
});
|
|
590
697
|
}
|
|
591
698
|
checkpointDelivery(
|
|
592
699
|
bus: BusSnapshot,
|
|
593
700
|
delivery: Omit<JournalDelivery, "revision" | "updatedAt"> & {
|
|
594
701
|
state: "dispatching";
|
|
595
702
|
},
|
|
703
|
+
trackQueuePressure = false,
|
|
596
704
|
): JournalDelivery {
|
|
597
|
-
validateBus(bus);
|
|
705
|
+
bus = this.queueMetadataSnapshot(validateBus(bus), trackQueuePressure);
|
|
598
706
|
validateDelivery({
|
|
599
707
|
...delivery,
|
|
600
708
|
revision: 0,
|
|
601
709
|
updatedAt: delivery.createdAt,
|
|
602
710
|
});
|
|
603
|
-
return this.
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if ((row.
|
|
711
|
+
return this.queueMetadataTransaction(trackQueuePressure, () => {
|
|
712
|
+
// Grouped parents are not linked until the handoff INSERT.
|
|
713
|
+
return this.withPruningDeferred(() => {
|
|
714
|
+
const evidence = this.retryEvidence(delivery.peer, delivery.originals);
|
|
715
|
+
if (evidence === "used") {
|
|
716
|
+
// Inheritance commits with the handoff, before safe failure or pruning can erase a grouped parent.
|
|
717
|
+
this.recordRetryEvidence(delivery.peer, delivery.originals, delivery.id, this.retryWarning(delivery.peer, delivery.originals));
|
|
718
|
+
delivery = { ...delivery, attempt: Math.max(1, delivery.attempt ?? 0) };
|
|
719
|
+
} else if (evidence === "unknown") this.markRetryUnknown([{ ...delivery, revision: 0, updatedAt: delivery.createdAt }], {});
|
|
720
|
+
const ids = new Set(delivery.originals.map((env) => env.id)), immutableIds = new Set(retryOriginals(delivery.originals).map(env => env.id));
|
|
721
|
+
for (const row of this.list(delivery.peer)) {
|
|
722
|
+
if (row.state !== "queued" || !row.originals.some((env) => ids.has(env.id))) continue;
|
|
723
|
+
this.transition(row.id, "discarded", `grouped into delivery ${delivery.id}`);
|
|
724
|
+
if (retryOriginals(row.originals).some(env => immutableIds.has(env.id))) {
|
|
725
|
+
delivery = { ...delivery, previousId: delivery.previousId ?? row.id };
|
|
726
|
+
if ((row.attempt ?? 0) > 0) delivery = { ...delivery, attempt: Math.max(delivery.attempt ?? 0, row.attempt!) };
|
|
727
|
+
}
|
|
617
728
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
);
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
729
|
+
const revision = this.bump();
|
|
730
|
+
const now = Date.now();
|
|
731
|
+
this.db
|
|
732
|
+
.query(
|
|
733
|
+
"UPDATE delivery_meta SET bus_snapshot = ?, manual_paused = ? WHERE project_id = ?",
|
|
734
|
+
)
|
|
735
|
+
.run(
|
|
736
|
+
JSON.stringify(bus),
|
|
737
|
+
JSON.stringify(bus.manualPaused ?? []),
|
|
738
|
+
this.projectId,
|
|
739
|
+
);
|
|
740
|
+
this.writeQueueMetadata(bus, revision, trackQueuePressure);
|
|
741
|
+
this.db
|
|
742
|
+
.query(
|
|
743
|
+
"INSERT INTO deliveries (id, project_id, peer, state, revision, created_at, updated_at, originals, out_json, reason, attempt, previous_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
744
|
+
)
|
|
745
|
+
.run(
|
|
746
|
+
delivery.id,
|
|
747
|
+
this.projectId,
|
|
748
|
+
delivery.peer,
|
|
749
|
+
delivery.state,
|
|
750
|
+
revision,
|
|
751
|
+
delivery.createdAt,
|
|
752
|
+
now,
|
|
753
|
+
JSON.stringify(delivery.originals),
|
|
754
|
+
JSON.stringify(delivery.out),
|
|
755
|
+
delivery.reason ?? null,
|
|
756
|
+
delivery.attempt ?? null,
|
|
757
|
+
delivery.previousId ?? null,
|
|
758
|
+
);
|
|
759
|
+
this.harden();
|
|
760
|
+
return { ...delivery, revision, updatedAt: now };
|
|
761
|
+
});
|
|
762
|
+
});
|
|
651
763
|
}
|
|
652
764
|
createDelivery(
|
|
653
765
|
input: Omit<JournalDelivery, "revision" | "updatedAt"> & {
|
|
@@ -681,7 +793,7 @@ export class DeliveryJournal {
|
|
|
681
793
|
input.attempt ?? null,
|
|
682
794
|
input.previousId ?? null,
|
|
683
795
|
);
|
|
684
|
-
this.pruneTerminal();
|
|
796
|
+
this.pruneTerminal(input.state, input.id);
|
|
685
797
|
return { ...input, revision, updatedAt };
|
|
686
798
|
})();
|
|
687
799
|
}
|
|
@@ -698,9 +810,14 @@ export class DeliveryJournal {
|
|
|
698
810
|
return this.db.transaction((): JournalDelivery => {
|
|
699
811
|
const row = this.get(deliveryId);
|
|
700
812
|
if (!row || row.peer !== peer || !["dispatching", "accepted"].includes(row.state)) throw new Error("delivery is not active for this peer");
|
|
701
|
-
const completed = this.
|
|
702
|
-
|
|
703
|
-
|
|
813
|
+
const completed = this.withPruningDeferred(() => {
|
|
814
|
+
const result = this.transition(deliveryId, "completed", reason, row.revision);
|
|
815
|
+
const put = this.db.query("INSERT OR IGNORE INTO task_offer_reads (project_id,peer,envelope_id,delivery_id,source,offer_revision) VALUES (?,?,?,?,?,?)");
|
|
816
|
+
for (const env of row.originals) if (env.from === HUB && env.kind === "task" && Number.isSafeInteger(env.refs?.taskOffer) && env.refs!.taskOffer! >= 1) put.run(this.projectId, peer, env.id, deliveryId, source, env.refs!.taskOffer!);
|
|
817
|
+
return result;
|
|
818
|
+
});
|
|
819
|
+
// Proof is now durable in this transaction, and the return receipt is captured.
|
|
820
|
+
this.pruneTerminal("completed", "");
|
|
704
821
|
return completed;
|
|
705
822
|
})();
|
|
706
823
|
}
|
|
@@ -730,7 +847,7 @@ export class DeliveryJournal {
|
|
|
730
847
|
"UPDATE deliveries SET state = ?, revision = ?, updated_at = ?, reason = ? WHERE id = ? AND project_id = ?",
|
|
731
848
|
)
|
|
732
849
|
.run(state, revision, Date.now(), reason ?? null, id, this.projectId);
|
|
733
|
-
this.pruneTerminal();
|
|
850
|
+
this.pruneTerminal(state, id);
|
|
734
851
|
return this.get(id)!;
|
|
735
852
|
})();
|
|
736
853
|
}
|
|
@@ -919,7 +1036,7 @@ export class DeliveryJournal {
|
|
|
919
1036
|
JSON.stringify(retryOriginals(current.originals).map(env => env.id)),
|
|
920
1037
|
);
|
|
921
1038
|
if (action === "retry") this.recordRetryEvidence(current.peer, current.originals, id, actor === `peer:${current.peer}`);
|
|
922
|
-
this.pruneTerminal();
|
|
1039
|
+
this.pruneTerminal(result.state, result.id);
|
|
923
1040
|
return result;
|
|
924
1041
|
})();
|
|
925
1042
|
}
|
|
@@ -927,7 +1044,15 @@ export class DeliveryJournal {
|
|
|
927
1044
|
const row = this.db.query("SELECT action,reason,observed_revision,actor FROM resolution_history WHERE project_id=? AND delivery_id=? ORDER BY id DESC LIMIT 1").get(this.projectId, id) as { action: JournalResolution; reason: string; observed_revision: number; actor: string } | null;
|
|
928
1045
|
return row ? { action: row.action, reason: row.reason, observedRevision: row.observed_revision, actor: row.actor } : undefined;
|
|
929
1046
|
}
|
|
930
|
-
private
|
|
1047
|
+
private withPruningDeferred<T>(body: () => T): T {
|
|
1048
|
+
this.pruningDeferred++;
|
|
1049
|
+
try { return body(); } finally { this.pruningDeferred--; }
|
|
1050
|
+
}
|
|
1051
|
+
private pruneTerminal(state: JournalDeliveryState, currentId: string): void {
|
|
1052
|
+
if (this.pruningDeferred) return;
|
|
1053
|
+
const terminal = state === "completed" || state === "discarded" || state === "failed";
|
|
1054
|
+
const terminalCap = terminal ? this.terminalHistoryLimit?.() ?? TERMINAL_CAP : TERMINAL_CAP;
|
|
1055
|
+
// Never delete the receipt this mutation/resolve is still using, at any target.
|
|
931
1056
|
const count = Number(
|
|
932
1057
|
(
|
|
933
1058
|
this.db
|
|
@@ -937,16 +1062,17 @@ export class DeliveryJournal {
|
|
|
937
1062
|
.get(this.projectId) as { n: number }
|
|
938
1063
|
).n,
|
|
939
1064
|
);
|
|
940
|
-
if (count <=
|
|
1065
|
+
if (count <= terminalCap) return;
|
|
941
1066
|
this.db
|
|
942
1067
|
.query(
|
|
943
|
-
"DELETE FROM deliveries WHERE id IN (SELECT d.id FROM deliveries d WHERE d.project_id = ? AND d.state IN ('completed','discarded','failed') AND NOT EXISTS (SELECT 1 FROM deliveries c WHERE c.project_id = d.project_id AND c.previous_id = d.id) ORDER BY d.updated_at, d.id LIMIT ?)",
|
|
1068
|
+
"DELETE FROM deliveries WHERE id IN (SELECT d.id FROM deliveries d WHERE d.project_id = ? AND d.state IN ('completed','discarded','failed') AND d.id != ? AND NOT EXISTS (SELECT 1 FROM deliveries c WHERE c.project_id = d.project_id AND c.previous_id = d.id) ORDER BY d.updated_at, d.id LIMIT ?)",
|
|
944
1069
|
)
|
|
945
|
-
.run(this.projectId, count -
|
|
1070
|
+
.run(this.projectId, currentId, count - terminalCap);
|
|
946
1071
|
}
|
|
947
|
-
importSnapshot(snapshot: JournalSnapshot, operationId: string): boolean {
|
|
1072
|
+
importSnapshot(snapshot: JournalSnapshot, operationId: string, trackQueuePressure = false): boolean {
|
|
948
1073
|
this.ensureOpen();
|
|
949
1074
|
validateJournalSnapshot(snapshot);
|
|
1075
|
+
const bus = this.queueMetadataSnapshot(snapshot.bus, trackQueuePressure);
|
|
950
1076
|
text(operationId, "operationId", 256);
|
|
951
1077
|
if (this.operationId !== undefined && this.operationId !== operationId)
|
|
952
1078
|
throw new Error("recovery operation does not own delivery journal");
|
|
@@ -963,18 +1089,19 @@ export class DeliveryJournal {
|
|
|
963
1089
|
Object.keys(storedBus).length !== 0
|
|
964
1090
|
)
|
|
965
1091
|
return false;
|
|
966
|
-
return this.
|
|
1092
|
+
return this.queueMetadataTransaction(trackQueuePressure, () => {
|
|
967
1093
|
this.db
|
|
968
1094
|
.query(
|
|
969
1095
|
"UPDATE delivery_meta SET revision = ?, bus_snapshot = ?, manual_paused = ?, operation_id = ? WHERE project_id = ?",
|
|
970
1096
|
)
|
|
971
1097
|
.run(
|
|
972
1098
|
snapshot.revision,
|
|
973
|
-
JSON.stringify(
|
|
1099
|
+
JSON.stringify(bus),
|
|
974
1100
|
JSON.stringify(snapshot.manualPaused),
|
|
975
1101
|
operationId,
|
|
976
1102
|
this.projectId,
|
|
977
1103
|
);
|
|
1104
|
+
this.writeQueueMetadata(bus, snapshot.revision, trackQueuePressure);
|
|
978
1105
|
for (const row of snapshot.deliveries)
|
|
979
1106
|
this.db
|
|
980
1107
|
.query(
|
|
@@ -995,9 +1122,9 @@ export class DeliveryJournal {
|
|
|
995
1122
|
row.previousId ?? null,
|
|
996
1123
|
);
|
|
997
1124
|
// Snapshots deliberately omit resolution history; only these imported originals have unknown retry rights.
|
|
998
|
-
this.markRetryUnknown(snapshot.deliveries,
|
|
1125
|
+
this.markRetryUnknown(snapshot.deliveries, bus);
|
|
999
1126
|
return true;
|
|
1000
|
-
})
|
|
1127
|
+
});
|
|
1001
1128
|
}
|
|
1002
1129
|
close(): void {
|
|
1003
1130
|
if (!this.closed) {
|
|
@@ -8,10 +8,12 @@ import type { StampedEvent } from "./events.ts";
|
|
|
8
8
|
|
|
9
9
|
export interface ProfileHistory {
|
|
10
10
|
at: number; event: string; criteria?: string[]; criteriaPrivate?: true; deliverables?: string[];
|
|
11
|
+
/** #901: on the hub's own check entries, the check key and the example check's unresolved-key count; on a `claim verdict` entry, the person's verdict. Closed values only, no claim text. */
|
|
12
|
+
checkKey?: string; missingKeys?: number; claim?: { slot: number; verdict: "supported" | "unsupported" | "unclear"; model?: string; modelVerdict?: "supported" | "unsupported" | "unclear"; list?: string };
|
|
11
13
|
}
|
|
12
14
|
export interface ProfileTaskMetadata {
|
|
13
15
|
profile: ProfileName; state?: string; class: string; alias: string | null; history: ProfileHistory[];
|
|
14
|
-
plan?: { sources: number | null; claims: number | null };
|
|
16
|
+
plan?: { sources: number | null; claims: number | null; /** #901: `claimListId` of the plan's claims now (an opaque hash, never their text); absent when the list could not be read. */ claimList?: string };
|
|
15
17
|
}
|
|
16
18
|
const complete = (s: Source<unknown>) => s.available && s.complete;
|
|
17
19
|
const number = (n: unknown): n is number => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
@@ -24,7 +26,7 @@ const criterion = (s: unknown): s is string => typeof s === "string" && /^[A-Za-
|
|
|
24
26
|
|
|
25
27
|
// Exhaustive maps make producer union changes a compile-time review point.
|
|
26
28
|
const taskStates: Record<TaskState,true> = {backlog:true,proposed:true,in_progress:true,in_review:true,approved:true,changes_requested:true,cancelled:true};
|
|
27
|
-
const usageSources: Record<Extract<StampedEvent,{type:"usage"}>["source"],true> = {omniroute:true,claude_transcript:true,claude_result:true,codex_appserver:true,kimi_wire:true};
|
|
29
|
+
const usageSources: Record<Extract<StampedEvent,{type:"usage"}>["source"],true> = {omniroute:true,claude_transcript:true,claude_result:true,codex_appserver:true,kimi_wire:true,jcode_acp:true};
|
|
28
30
|
const attributions: Record<Attribution,true> = {delivery:true,single_open:true,unattributed:true};
|
|
29
31
|
const screenItems: Record<ScreenItem,true> = {task:true,summary:true,"review note":true,handoff:true,note:true};
|
|
30
32
|
const screenMisses: Record<ScreenMiss,true> = {"too long":true,"off campus":true,timeout:true,unreadable:true,failed:true};
|
|
@@ -228,12 +230,37 @@ export function profileScorecard(input: DogfoodInput) {
|
|
|
228
230
|
const count=t.metadata.plan?.[key];
|
|
229
231
|
return full && declarationCount(count) ? count : null;
|
|
230
232
|
};
|
|
233
|
+
// #901: claim verdicts and check provenance, read only from entries the hub or the person wrote with closed values. Unknown stays unknown, never zero: a count exists only when its source
|
|
234
|
+
// history is complete, and a row without its basis (no verdict, no model verdict, no recorded check key) is null.
|
|
235
|
+
type Verdict = "supported" | "unsupported" | "unclear";
|
|
236
|
+
// A verdict counts only for the claim list it evaluated: its `list` must equal the plan's current claim-list id. After the owner replaces or reorders the claims, an earlier verdict names no claim
|
|
237
|
+
// of the new list (it is unknown again, not carried over to whatever now sits in its slot), and a verdict from before ids were recorded cannot be bound, so it does not count.
|
|
238
|
+
const lastVerdict = (t: BoundTask, slot: number) => t.metadata.history.filter(h => h.event === "claim verdict" && h.claim?.slot === slot && inWindow(h.at) && t.metadata.plan?.claimList !== undefined && h.claim.list === t.metadata.plan.claimList).sort((a, b) => a.at - b.at).at(-1)?.claim;
|
|
239
|
+
const slotRows = group.tasks.map(t => {
|
|
240
|
+
const count = planCount(t, "claims");
|
|
241
|
+
return { task: t.id, claims: count === null ? null : Array.from({ length: count }, (_, i) => {
|
|
242
|
+
const c = lastVerdict(t, i + 1);
|
|
243
|
+
return { slot: i + 1, modelLabel: c?.model ?? null, personVerdict: c?.verdict ?? null, agreement: c?.modelVerdict ? c.modelVerdict === c.verdict : null };
|
|
244
|
+
}) };
|
|
245
|
+
});
|
|
246
|
+
const slotted = slotRows.flatMap(row => row.claims ?? []);
|
|
247
|
+
const decided = slotted.filter(c => c.personVerdict !== null);
|
|
248
|
+
const checkEntries = history.filter(h => (h.event === "check passed" || h.event === "check failed") && h.checkKey === "fact_check");
|
|
249
|
+
const everyCheckKeyed = full && history.filter(h => h.event === "check passed" || h.event === "check failed").every(h => typeof h.checkKey === "string");
|
|
250
|
+
const compared = decided.filter(c => c.agreement !== null);
|
|
231
251
|
const research = profile === "research" ? {research:{
|
|
232
252
|
declarations:{scope:"current_plan",perTask:group.tasks.map(t=>({task:t.id,sources:planCount(t,"sources"),claims:planCount(t,"claims")}))},
|
|
233
|
-
|
|
253
|
+
// A verdict is the person's, entered with `ahub task claim`; task approval never supplies one. Null until at least the declarations are known; then the number of slots with a recorded verdict.
|
|
254
|
+
claims:{reachedPersonVerdict:full && slotRows.every(r => r.claims !== null) && decided.length > 0 ? decided.length : null,perTask:slotRows},
|
|
234
255
|
observedCheckOutcomes:{scope:"task_check_history",passed:full?history.filter(h=>h.event==="check passed").length:null,failed:full?history.filter(h=>h.event==="check failed").length:null},
|
|
235
|
-
|
|
236
|
-
|
|
256
|
+
// Selection and pass/fail are proven only for entries that carry the hub's own check key, and only when every check entry of the group carries one (an older entry has no key: that is unknown, not "another check").
|
|
257
|
+
factCheck:{scope:everyCheckKeyed && checkEntries.length > 0 ? "task_check_history_with_key" : null,selection:everyCheckKeyed && checkEntries.length > 0 ? "checks.fact_check" : null,
|
|
258
|
+
passed:everyCheckKeyed && checkEntries.length > 0 ? checkEntries.filter(h => h.event === "check passed").length : null,
|
|
259
|
+
failed:everyCheckKeyed && checkEntries.length > 0 ? checkEntries.filter(h => h.event === "check failed").length : null,
|
|
260
|
+
missingKeysPerRun:everyCheckKeyed && checkEntries.length > 0 && checkEntries.every(h => h.missingKeys !== undefined) ? checkEntries.map(h => h.missingKeys!) : null,
|
|
261
|
+
// Agreement is a count of slots where the model's own verdict was entered and equals the person's, per label. A slot without the model's verdict (agreement null) is unknown and counts for nothing; one where they differ
|
|
262
|
+
// (agreement false) is a disagreement and counts for no label. With no comparable slot every label is null, never zero.
|
|
263
|
+
agreement:(["supported","unsupported","unclear"] as Verdict[]).map(label=>({label,count:compared.length > 0 ? compared.filter(c => c.agreement === true && c.personVerdict === label).length : null}))}
|
|
237
264
|
}} : {};
|
|
238
265
|
return {...research,profile, alias:group.alias, baseClass:group.base, tasks:[...ids].sort((a,b)=>a-b),
|
|
239
266
|
flow:{scope,autoApprovalTransitions:{scope:"recorded_events",value:eventsFull?new Set(autoApprovals.map(e=>e.id)).size:null},proposed:count("proposed"),accepted:count("accepted"),approved:milestoneKnown&&!approvalUnknown?approvalIds.size:null,cancelled:count("cancelled"),proposeToApproveMs:full&&!approvalUnknown?quantiles(leads):{samples:null,p50:null,p90:null},reviewRounds:milestoneKnown?rounds:null,firstPassApproval:full&&!approvalUnknown&&first.length?first.reduce((a,b)=>a+b,0)/first.length:null,escalations:count("escalated")},
|
package/src/hub/dogfood-read.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { claimListId } from "./board.ts";
|
|
1
2
|
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { Database } from "bun:sqlite";
|
|
@@ -96,7 +97,10 @@ export function dogfoodTasks(file: string): Source<DogfoodTask[]> {
|
|
|
96
97
|
if (history.some((h:any) => h && (h.unmetCriteria !== undefined && (!Array.isArray(h.unmetCriteria) || h.unmetCriteria.length>32 || new Set(h.unmetCriteria).size!==h.unmetCriteria.length || h.unmetCriteria.some((v:unknown)=>typeof v!=="string" || !/^[A-Za-z][A-Za-z0-9-]{0,47}$/.test(v))) || h.deliverables !== undefined && (!Array.isArray(h.deliverables) || h.deliverables.length>16 || h.deliverables.some((v:any)=>typeof v?.id!=="string" || !/^d-[0-9a-f]{12}$/.test(v.id)))))) complete=false;
|
|
97
98
|
const projection = history.filter((h:any)=>h && Number.isFinite(h.at) && typeof h.event==="string").map((h:any) => ({at:h.at,event:h.event,
|
|
98
99
|
...(Array.isArray(h.unmetCriteria) ? {criteria:h.unmetCriteria.slice(0,32).filter((v:unknown)=>typeof v === "string" && /^[A-Za-z][A-Za-z0-9-]{0,47}$/.test(v)).map((v:string)=>pii || h.withheld ? "withheld" : v), ...(h.withheld ? {criteriaPrivate:true as const} : {})} : {}),
|
|
99
|
-
...(Array.isArray(h.deliverables) ? {deliverables:h.deliverables.slice(0,16).filter((v:any)=>typeof v?.id==="string" && /^d-[0-9a-f]{12}$/.test(v.id)).map((v:any)=>v.id)} : {})
|
|
100
|
+
...(Array.isArray(h.deliverables) ? {deliverables:h.deliverables.slice(0,16).filter((v:any)=>typeof v?.id==="string" && /^d-[0-9a-f]{12}$/.test(v.id)).map((v:any)=>v.id)} : {}),
|
|
101
|
+
// #901: only closed values cross: a check key is a bounded identifier, a count a safe integer, a verdict one of three words and a model label a bounded token. Anything else is dropped (unknown).
|
|
102
|
+
...(h.event==="check passed" || h.event==="check failed" ? {...(typeof h.checkKey==="string" && /^[a-z][a-z0-9_]{0,47}$/.test(h.checkKey) ? {checkKey:h.checkKey} : {}), ...(Number.isSafeInteger(h.missingKeys) && h.missingKeys>=0 && h.missingKeys<=100000 ? {missingKeys:h.missingKeys} : {})} : {}),
|
|
103
|
+
...(h.event==="claim verdict" && h.claim && Number.isSafeInteger(h.claim.slot) && h.claim.slot>=1 && h.claim.slot<=50 && ["supported","unsupported","unclear"].includes(h.claim.verdict) ? {claim:{slot:h.claim.slot,verdict:h.claim.verdict,...(typeof h.claim.list==="string" && /^[0-9a-f]{16}$/.test(h.claim.list) ? {list:h.claim.list} : {}),...(typeof h.claim.model==="string" && /^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,63}$/.test(h.claim.model) && !pii ? {model:h.claim.model} : {}),...(["supported","unsupported","unclear"].includes(h.claim.modelVerdict) ? {modelVerdict:h.claim.modelVerdict} : {})}} : {})}));
|
|
100
104
|
let declarations: ProfileTaskMetadata["plan"];
|
|
101
105
|
if (!row.plan_cut && row.plan !== null) {
|
|
102
106
|
try {
|
|
@@ -110,7 +114,7 @@ export function dogfoodTasks(file: string): Source<DogfoodTask[]> {
|
|
|
110
114
|
if (!Array.isArray(value) || value.length>50 || value.some((v:unknown)=>typeof v!=="string" || v.length>300)) throw new Error("plan list malformed");
|
|
111
115
|
return value.length;
|
|
112
116
|
};
|
|
113
|
-
declarations={sources:count("sources"),claims:count("claims")};
|
|
117
|
+
declarations={sources:count("sources"),claims:count("claims"),claimList:claimListId(Array.isArray(rawPlan.claims) ? rawPlan.claims : undefined)};
|
|
114
118
|
}
|
|
115
119
|
} catch {planBad=true;}
|
|
116
120
|
}
|
package/src/hub/envelope.ts
CHANGED
|
@@ -78,6 +78,17 @@ export const STANDING_INSTRUCTION =
|
|
|
78
78
|
"with [IMPORTANT] only when the recipient must see it before finishing what it is doing. Unmarked messages are batched. " +
|
|
79
79
|
HUB_MESSAGE_INSTRUCTION;
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* #912: Jcode defers MCP tools. Only its built-in tools are listed, and an MCP tool appears once the model searches for it, so a Jcode peer with no hint
|
|
83
|
+
* concludes it has no `hub_*` tools and answers in its final message instead of calling `hub_send`, `hub_task_accept`, `hub_task_done` or `hub_review`.
|
|
84
|
+
* This is the ONE place the guidance is written: `JcodePeer` puts it in front of the session's role contract (sent once, with the first prompt of a session),
|
|
85
|
+
* and no other peer's prompt, the AGENTS block or a profile template repeats it. The server name is `JCODE_MCP_NAME` (`src/cli/jcode-mcp.ts`); a test pins them equal.
|
|
86
|
+
*/
|
|
87
|
+
export const JCODE_DEFERRED_TOOLS_INSTRUCTION =
|
|
88
|
+
"[agent-hub] Your hub tools (hub_send, hub_inbox, hub_task_accept, hub_task_done, hub_review and the rest) are deferred MCP tools from the agent-hub-jcode server: " +
|
|
89
|
+
"they are not in your visible tool list until you load them. Before you conclude a hub tool is missing, search for it by name with your tool search and load it, " +
|
|
90
|
+
"then call it. Never report the hub as unavailable without having searched. Answer a hub message with hub_send, not only in your final message.";
|
|
91
|
+
|
|
81
92
|
const MARKER = /^\s*\[(IMPORTANT|STATUS|FYI)\]\s*/i;
|
|
82
93
|
|
|
83
94
|
/** A leading [IMPORTANT] / [STATUS] / [FYI] sets the priority and is stripped from the body. */
|