@staix/agent-hub 0.34.0 → 0.35.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 +51 -0
- package/README.md +35 -6
- package/docs/adding-an-ai-tool.md +2 -2
- package/docs/addons/claude-mem.md +4 -4
- package/docs/addons/open-code-review.md +1 -1
- package/docs/agent-notes/README.md +6 -0
- package/docs/agent-notes/adapters.md +9 -5
- package/docs/agent-notes/budget.md +1 -1
- package/docs/agent-notes/bus.md +1 -1
- package/docs/agent-notes/daemon.md +4 -3
- package/docs/agent-notes/local-worker.md +1 -1
- package/docs/agent-notes/models.md +2 -0
- package/docs/agent-notes/tasks.md +1 -1
- package/docs/agent-notes/tests.md +5 -0
- package/docs/design/adapters.md +110 -2
- package/docs/design/agent-hub-architecture.md +63 -1
- package/docs/design/agent-hub.md +6 -0
- package/docs/design/bus.md +18 -0
- package/docs/design/execution-reference.md +146 -0
- package/docs/design/task-board.md +10 -0
- package/docs/dogfood.md +33 -12
- package/docs/events.md +38 -20
- package/docs/memory.md +21 -7
- package/docs/operations.md +539 -24
- package/docs/quickstart.md +18 -4
- package/docs/research.md +71 -13
- package/docs/roles.md +1 -1
- package/docs/sdlc.md +9 -7
- package/docs/security.md +87 -11
- package/docs/smoke.md +14 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +8 -3
- package/docs/verification/2026-11-07-0.34.1.md +54 -0
- package/docs/verification/2026-11-07-compat-python-startup-1248.md +51 -0
- package/docs/verification/2026-11-07-compat-receipt-harness-1248.md +105 -0
- package/docs/verification/2026-11-07-compat-receipt-producers-1248.md +96 -0
- package/docs/verification/2026-11-07-compat-workflow-1248.md +94 -0
- package/docs/verification/2026-11-07-daemon-startup-1232.md +169 -0
- package/docs/verification/2026-11-07-jcode-receipt-reconstruction-1248.md +118 -0
- package/docs/verification/2026-11-07-kimi-delayed-usage.md +34 -0
- package/docs/verification/2026-11-07-kimi-review-watch-1284.md +115 -0
- package/docs/verification/2026-11-08-agy-1.5.3.md +96 -0
- package/docs/verification/2026-11-08-agy-acp-t0.md +56 -0
- package/docs/verification/2026-11-08-hub-visual-1058.md +161 -0
- package/docs/verification/2026-11-08-jcode-0991-model-setter-1317.md +143 -0
- package/docs/verification/2026-11-08-kimi-252-model-setter-1317.md +137 -0
- package/docs/verification/2026-11-08-kimi-tui-completion-1282.md +45 -0
- package/docs/verification/2026-11-08-kimi-wire-yield-1283.md +31 -0
- package/docs/verification/2026-11-08-live-model-override-1317.md +250 -0
- package/docs/verification/2026-11-08-qwen-0269-acp.md +138 -0
- package/docs/verification/2026-11-08-qwen-0269-model-setter-1317.md +91 -0
- package/docs/verification/2026-11-09-0.35.0.md +129 -0
- package/docs/verification/2026-11-09-headless-records-1320.md +157 -0
- package/docs/verification/claude-permission-transport-native.py +10 -3
- package/docs/verification/claude-shell-wrapper-native.py +8 -8
- package/docs/verified.json +245 -15
- package/package.json +3 -3
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/kimi-hook.js +20397 -0
- package/plugins/agent-hub/kimi-review-policy-worker.js +217 -0
- package/plugins/agent-hub/server.js +2701 -664
- package/scripts/check-docs.mjs +12 -1
- package/src/adapters/acp-model-policy.ts +56 -0
- package/src/adapters/acp.ts +175 -5
- package/src/adapters/agy/entry.ts +8 -5
- package/src/adapters/agy/hooks.ts +65 -15
- package/src/adapters/channel-peer.ts +33 -1
- package/src/adapters/claude-implementer.ts +16 -3
- package/src/adapters/codex-appserver.ts +118 -8
- package/src/adapters/codex-review-isolation.ts +3 -1
- package/src/adapters/compat.ts +11 -6
- package/src/adapters/definitions/builtin-metadata.ts +8 -1
- package/src/adapters/definitions/native-cli-entry-kimi.ts +76 -7
- package/src/adapters/definitions/native-cli-preview-common.ts +2 -2
- package/src/adapters/definitions/native-cli-preview-kimi.ts +9 -2
- package/src/adapters/definitions/reviewer-variants.ts +3 -1
- package/src/adapters/kimi-review-policy-worker.js +2 -0
- package/src/adapters/kimi-review-policy-worker.ts +85 -0
- package/src/adapters/kimi-review-policy.ts +68 -0
- package/src/adapters/kimi-review.ts +41 -41
- package/src/adapters/kimi-tui-facts.ts +153 -0
- package/src/adapters/kimi-wire-usage.ts +33 -8
- package/src/adapters/local-worker.ts +59 -20
- package/src/adapters/model-token.ts +3 -0
- package/src/adapters/pi.ts +27 -5
- package/src/adapters/registry-types.ts +34 -3
- package/src/adapters/registry.ts +1 -0
- package/src/adapters/reviewer-policy-watch.ts +86 -0
- package/src/adapters/reviewer-print.ts +80 -18
- package/src/cli/agents.ts +1 -1
- package/src/cli/facts-hook.ts +4 -2
- package/src/cli/help.ts +3 -2
- package/src/cli/identity.ts +3 -0
- package/src/cli/kimi-facts-hook.ts +11 -0
- package/src/cli/kimi-plugin.ts +157 -0
- package/src/cli/main.ts +48 -9
- package/src/cli/model.ts +52 -0
- package/src/cli/output.ts +23 -1
- package/src/cli/recovery-package.ts +15 -7
- package/src/cli/reset.ts +1 -1
- package/src/cli/status-lines.ts +8 -4
- package/src/hub/budget.ts +1 -1
- package/src/hub/bus.ts +40 -2
- package/src/hub/code-facts.ts +10 -3
- package/src/hub/completions.ts +3 -1
- package/src/hub/config-apply.ts +2 -1
- package/src/hub/control-client.ts +1 -1
- package/src/hub/daemon.ts +1050 -106
- package/src/hub/deliverable-files.ts +38 -5
- package/src/hub/dogfood-profile.ts +1 -1
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +28 -18
- package/src/hub/execution-lineage.ts +120 -0
- package/src/hub/execution-reference.ts +36 -0
- package/src/hub/hub-tools.ts +21 -2
- package/src/hub/instances.ts +4 -3
- package/src/hub/kimi-events.ts +46 -0
- package/src/hub/kimi-usage.ts +132 -29
- package/src/hub/lifecycle.ts +31 -15
- package/src/hub/limits.ts +13 -4
- package/src/hub/manager.ts +15 -2
- package/src/hub/memory-policy.ts +2 -1
- package/src/hub/memory-sync-attestation.ts +54 -0
- package/src/hub/memory-sync-state.ts +20 -5
- package/src/hub/model-overrides.ts +115 -0
- package/src/hub/native-hook-launch.ts +12 -3
- package/src/hub/operational-view.ts +2 -2
- package/src/hub/peers.ts +27 -0
- package/src/hub/permission-mode.ts +1 -1
- package/src/hub/person-figures.ts +88 -0
- package/src/hub/report.ts +38 -4
- package/src/hub/research-dataset.ts +158 -0
- package/src/hub/research-memory.ts +68 -0
- package/src/hub/research-quality.ts +118 -0
- package/src/hub/research-record.ts +279 -0
- package/src/hub/research.ts +126 -27
- package/src/hub/restart.ts +14 -0
- package/src/hub/settings.ts +3 -0
- package/src/hub/startup-event-seeds.ts +51 -0
- package/src/hub/startup-readiness.ts +65 -0
- package/src/hub/startup-timing.ts +38 -0
- package/src/hub/tasks.ts +29 -9
- package/src/hub/ui.ts +9 -5
- package/src/hub/visual-assets.ts +50 -0
- package/src/hub/visual.ts +257 -0
- package/src/memory/brief.ts +169 -14
- package/src/memory/capture.ts +2 -1
- package/src/memory/recall.ts +46 -5
- package/src/memory/record-provenance.ts +82 -0
- package/src/memory/records.ts +116 -0
- package/src/models/route/adec.ts +190 -0
- package/src/models/route/config.ts +37 -3
- package/src/models/route/decision-evidence.ts +83 -0
- package/src/models/route/decision.ts +143 -0
- package/src/models/route/runtime.ts +13 -6
- package/src/ui/index.html +134 -6
- package/src/ui/plugin.ts +65 -4
- package/templates/AGENTS.block.md +1 -1
- package/templates/config.json +1 -1
- package/templates/profiles/general.json +1 -1
- package/templates/profiles/research.json +1 -1
- package/templates/profiles/writing.json +1 -1
package/src/cli/model.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isNativeModelToken } from "../adapters/model-token.ts";
|
|
2
|
+
import type { RuntimeModelStatus } from "../hub/model-overrides.ts";
|
|
3
|
+
|
|
4
|
+
export const MODEL_USAGE = "usage: ahub model set <peer> <model> | clear <peer> | list [--json]";
|
|
5
|
+
const peerToken = (value: unknown): value is string => typeof value === "string" && /^[a-z][a-z0-9-]{0,31}$/.test(value);
|
|
6
|
+
export function parseModelCommand(args: readonly string[]): { op: "list" | "set" | "clear"; peer?: string; model?: string; json: boolean } {
|
|
7
|
+
const [op, peer, model, ...extra] = args;
|
|
8
|
+
if (op === "list" && (args.length === 1 || args.length === 2 && peer === "--json")) return { op, json: peer === "--json" };
|
|
9
|
+
if (op === "clear" && args.length === 2 && peerToken(peer)) return { op, peer, json: false };
|
|
10
|
+
if (op === "set" && !extra.length && args.length === 3 && peerToken(peer) && isNativeModelToken(model)) return { op, peer, model, json: false };
|
|
11
|
+
throw new Error(MODEL_USAGE);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Closed metadata projection: an older or malformed reply never supplies display prose. */
|
|
15
|
+
export function projectModelStatus(value: unknown): RuntimeModelStatus | null {
|
|
16
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
17
|
+
const row: Record<string, unknown> = {};
|
|
18
|
+
try {
|
|
19
|
+
for (const key of ["peer", "effectiveModel", "desiredModel", "source", "pendingNextTurn", "unsupported"]) {
|
|
20
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
21
|
+
if (descriptor && "value" in descriptor) row[key] = descriptor.value;
|
|
22
|
+
}
|
|
23
|
+
} catch { return null; }
|
|
24
|
+
if (!peerToken(row.peer) || !(row.effectiveModel === null || isNativeModelToken(row.effectiveModel)) || !(row.desiredModel === null || isNativeModelToken(row.desiredModel)) || !(typeof row.source === "string" && ["override", "instance", "launch", "native default", "native thread default (was override)"].includes(row.source)) || typeof row.pendingNextTurn !== "boolean") return null;
|
|
25
|
+
let unsupported: RuntimeModelStatus["unsupported"];
|
|
26
|
+
if (row.unsupported !== undefined) {
|
|
27
|
+
if (!row.unsupported || typeof row.unsupported !== "object" || Array.isArray(row.unsupported)) return null;
|
|
28
|
+
let reasonCode: unknown, kind: unknown;
|
|
29
|
+
try {
|
|
30
|
+
const code = Object.getOwnPropertyDescriptor(row.unsupported, "reasonCode"), adapter = Object.getOwnPropertyDescriptor(row.unsupported, "kind");
|
|
31
|
+
reasonCode = code && "value" in code ? code.value : undefined;
|
|
32
|
+
kind = adapter && "value" in adapter ? adapter.value : undefined;
|
|
33
|
+
} catch { return null; }
|
|
34
|
+
if (kind !== undefined && !peerToken(kind)) return null;
|
|
35
|
+
if (reasonCode === "unverified_setter") unsupported = { reasonCode, ...(kind ? { kind } : {}), reason: `unsupported live: ${kind ?? "retained peer"} (version unverified) has no verified model setter`, nextStep: `restart this peer with ahub restart ${row.peer}` };
|
|
36
|
+
else if (reasonCode === "unmanaged_attachment") unsupported = { reasonCode, reason: "unsupported live: this attachment has no verified live model setter", nextStep: `change the native setting or restart this peer with ahub restart ${row.peer}` };
|
|
37
|
+
else if (reasonCode === "acp_setter_unavailable") {
|
|
38
|
+
if (!peerToken(kind)) return null;
|
|
39
|
+
let version: unknown;
|
|
40
|
+
try {
|
|
41
|
+
const descriptor = Object.getOwnPropertyDescriptor(row.unsupported, "version");
|
|
42
|
+
if (descriptor && !("value" in descriptor)) return null;
|
|
43
|
+
version = descriptor?.value;
|
|
44
|
+
} catch { return null; }
|
|
45
|
+
if (version !== undefined && !(isNativeModelToken(version) && /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(version))) return null;
|
|
46
|
+
unsupported = { reasonCode, kind, ...(typeof version === "string" ? { version } : {}), reason: `unsupported live: ${kind} ${version ?? "unknown"} lacks a setter receipt or compat admission`, nextStep: `restart this peer with ahub restart ${row.peer}` };
|
|
47
|
+
}
|
|
48
|
+
else return null;
|
|
49
|
+
}
|
|
50
|
+
return { peer: row.peer, effectiveModel: row.effectiveModel, desiredModel: row.desiredModel, source: row.source as RuntimeModelStatus["source"], pendingNextTurn: row.pendingNextTurn,
|
|
51
|
+
...(unsupported ? { unsupported } : {}) };
|
|
52
|
+
}
|
package/src/cli/output.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { projectModelStatus } from "./model.ts";
|
|
2
|
+
import type { RuntimeModelStatus } from "../hub/model-overrides.ts";
|
|
1
3
|
import type { QueueBackpressureStatus } from "../hub/queue-backpressure.ts";
|
|
2
4
|
import { definitionOf } from "../adapters/registry.ts";
|
|
3
5
|
import { windowKind } from "../hub/window-kind.ts";
|
|
@@ -14,6 +16,8 @@ import type { SpecsStatus } from "../hub/specs-task.ts";
|
|
|
14
16
|
|
|
15
17
|
export interface DoctorCheck { section: "Tools" | "Hub" | "Config" | "Models" | "Memory"; level: "ok" | "warn" | "fail" | "unknown"; name: string; detail: string }
|
|
16
18
|
export interface StatusOutput {
|
|
19
|
+
runtimeModels?: RuntimeModelStatus[];
|
|
20
|
+
runtimeModelsUnavailable?: boolean;
|
|
17
21
|
roomsUnavailable?: unknown;
|
|
18
22
|
roomsOpening?: boolean;
|
|
19
23
|
controlWriteTiming?: { active: number; slowRequests: number; blockedLoopSamples: number; droppedActive: number; droppedRecent: number; droppedLogs: number; droppedSpans?: number; droppedMetadata?: number; expiredRequests?: number };
|
|
@@ -33,7 +37,7 @@ export interface StatusOutput {
|
|
|
33
37
|
pendingTaskOffers?: readonly { task: number; peer: string }[];
|
|
34
38
|
isolatedChecks?: { pending?: number; orphans?: string[]; unknownReceipts?: readonly string[]; maintenanceUnavailable?: boolean };
|
|
35
39
|
memoryRead?: { truncated: boolean; scanned: number; limit: number };
|
|
36
|
-
memoryProfile?: { profile?: string; generation?: string; state?: string; agentMemVersion?: string; skippedRows?: number; error?: string; sync?: { state: "disabled" | "unavailable" | "notrun" | "not-configured" | "stopped" | "ok" | "failed" | "unknown"; at?: number; cause?: string; declinedCount?: number; declinedIdsKnown?: false; withheldCount?: number; exportedCount?: number; placementActive?: false; nextStep?: string } };
|
|
40
|
+
memoryProfile?: { profile?: string; generation?: string; state?: string; agentMemVersion?: string; skippedRows?: number; error?: string; sync?: { state: "disabled" | "unavailable" | "notrun" | "not-configured" | "stopped" | "ok" | "failed" | "unknown"; at?: number; cause?: string; declinedCount?: number; declinedIdsKnown?: false; withheldCount?: number; exportedCount?: number; placementActive?: false; writerCategory?: string; writerCauseClass?: string; nextStep?: string } };
|
|
37
41
|
/** #784 AC3: `active`, `reviewWait` and `reserved` add up to `wip`; absent from an older hub. */
|
|
38
42
|
taskWip?: Record<string, { wip: number; wipLimit: number; active?: number; reviewWait?: number; reserved?: number }>;
|
|
39
43
|
pid?: number; controlPort?: number; cwd?: string; version?: string; peers: Record<string, any>; roleInfo?: Record<string, any>; actionPolicyInfo?: Record<string, { note: string; enforceable: boolean }>; budget?: Record<string, any>; planner?: { peers?: string[]; source?: string };
|
|
@@ -43,6 +47,14 @@ export interface StatusOutput {
|
|
|
43
47
|
/** `peers`: one note per peer that shows no number, and why (no quota, nothing read yet); absent from an older hub. */
|
|
44
48
|
export interface BudgetOutput { budget: Record<string, any>; gate?: number; peers?: { peer: string; kind?: string; note: string }[] }
|
|
45
49
|
export interface RosterRow { peer: string; on: boolean; startMode: string; state: string; queued: number; tasks: number }
|
|
50
|
+
export function renderRuntimeModels(models: readonly RuntimeModelStatus[], columns?: number): Span[][] {
|
|
51
|
+
const out = rows(["PEER", "EFFECTIVE MODEL", "SOURCE", "STATE"], models.map(row => [span(row.peer, "info", true), span(row.effectiveModel ?? "not reported"), span(row.source), span(row.pendingNextTurn ? "pending next turn" : "current")]), columns);
|
|
52
|
+
for (const row of models) {
|
|
53
|
+
if (row.pendingNextTurn) out.push(...lines(`${row.peer}: next turn ${row.desiredModel ?? "native default"}; the active turn is unchanged.`, columns, "attention"));
|
|
54
|
+
if (row.unsupported) out.push(...lines(`${row.peer}: ${row.unsupported.reason}; ${row.unsupported.nextStep}`, columns, "attention"));
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
46
58
|
export function renderRoster(data: RosterRow[], columns?: number): Span[][] {
|
|
47
59
|
return rows(["PEER", "ROSTER", "START", "STATE", "Q", "TASKS"], data.map(row => [span(row.peer, "info", true), span(row.on ? "on" : "off", row.on ? "success" : "muted", true), span(row.startMode, undefined, true), span(row.state, stateTone(row.state), true), span(row.queued, undefined, true), span(row.tasks, undefined, true)]), columns);
|
|
48
60
|
}
|
|
@@ -218,6 +230,8 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
218
230
|
if (data.roleInfo) data = { ...data, peers: Object.fromEntries(Object.entries(data.peers).map(([peer, value]) => [peer, { ...value, ...data.roleInfo?.[peer] }])) };
|
|
219
231
|
const out = lines(`agent-hub${data.version ? ` ${data.version}` : ""} pid ${data.pid ?? "unknown"} control 127.0.0.1:${data.controlPort ?? "unknown"}`, columns, "info");
|
|
220
232
|
if (data.cwd) out.push(...lines(data.cwd, columns));
|
|
233
|
+
if (data.runtimeModelsUnavailable === true) out.push(...lines("Runtime models unavailable: could not read a bounded model catalog; run ahub model list for the cause. For a known peer, use ahub model set <peer> <model> or ahub model clear <peer>; repair storage or reduce the retained peer catalog as indicated before listing again.", columns, "attention"));
|
|
234
|
+
else if (Array.isArray(data.runtimeModels)) out.push(...renderRuntimeModels(data.runtimeModels.map(projectModelStatus).filter((row): row is RuntimeModelStatus => row !== null), columns));
|
|
221
235
|
const timing = data.controlWriteTiming;
|
|
222
236
|
if (full && timing && Object.values(timing).every(value => Number.isSafeInteger(value) && value >= 0)) {
|
|
223
237
|
out.push(...lines(`control writes: active ${timing.active}, slow ${timing.slowRequests}, loop delays ${timing.blockedLoopSamples}, dropped ${timing.droppedActive + timing.droppedLogs + (timing.droppedSpans ?? 0) + (timing.droppedMetadata ?? 0)}, expired ${timing.expiredRequests ?? 0}; timings in hub.log`, columns, timing.slowRequests || timing.blockedLoopSamples ? "attention" : undefined));
|
|
@@ -290,6 +304,14 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
290
304
|
let settling = false;
|
|
291
305
|
const peerDetails = Object.entries(data.peers).map(([id, p]) => {
|
|
292
306
|
const out: Span[][] = [];
|
|
307
|
+
// #1310: the current owner, when the hub reported one. Shown before the standby rows so the owner is never inferred from them. Every value is re-checked here: a pid is a positive integer, a time is
|
|
308
|
+
// finite, and a native session id is shown only when it is a short plain token; anything else is left out rather than cleaned up.
|
|
309
|
+
const owner = p.ownerAttachment;
|
|
310
|
+
if (owner && typeof owner === "object" && Number.isFinite(owner.attachedAt)) {
|
|
311
|
+
const pid = Number.isSafeInteger(owner.ownerPid) && owner.ownerPid > 0 ? `pid ${owner.ownerPid}` : "pid unknown";
|
|
312
|
+
const native = typeof owner.nativeSessionId === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(owner.nativeSessionId) ? `native session ${owner.nativeSessionId}` : "native session unverified";
|
|
313
|
+
out.push(...detail(`${id} owner`, `${pid}; attached ${relative(owner.attachedAt, now)}; ${native}`, columns, "muted"));
|
|
314
|
+
}
|
|
293
315
|
for (const session of Array.isArray(p.standbySessions) ? p.standbySessions : []) {
|
|
294
316
|
if (!session || typeof session.id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(session.id)) continue;
|
|
295
317
|
const pid = Number.isSafeInteger(session.pid) && session.pid > 0 ? String(session.pid) : "unknown";
|
|
@@ -212,24 +212,32 @@ export function stageCurrentRuntime(sourceRoot: string, version: string, home =
|
|
|
212
212
|
} finally { if (!registry) owner.close(); }
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/** #1253: npm 12 wraps `npm view --json` output for one package in a one-element array; npm 11 printed the value itself. Anything else is not one release. */
|
|
216
|
+
function npmViewJson(stdout: string): unknown {
|
|
217
|
+
let value: unknown;
|
|
218
|
+
try { value = JSON.parse(stdout); } catch { return undefined; }
|
|
219
|
+
if (!Array.isArray(value)) return value;
|
|
220
|
+
return value.length === 1 ? value[0] : undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
215
223
|
export async function registryRelease(version: string, run: RunCommand = runCommand): Promise<{ version: string; integrity: string }> {
|
|
216
224
|
exactVersion(version);
|
|
217
225
|
const result = await run(["npm", "view", `@staix/agent-hub@${version}`, "version", "dist.integrity", "--json"], { timeoutMs: 30_000 });
|
|
218
226
|
if (result.code !== 0) throw new Error(`registry metadata unavailable for @staix/agent-hub@${version}`);
|
|
219
|
-
const data =
|
|
220
|
-
|
|
221
|
-
|
|
227
|
+
const data = npmViewJson(result.stdout) as Record<string, unknown> | undefined;
|
|
228
|
+
const integrity = data?.["dist.integrity"];
|
|
229
|
+
if (data?.version !== version || typeof integrity !== "string" || !integrity.startsWith("sha512-")) {
|
|
230
|
+
throw new Error(`registry returned an unexpected release identity for @staix/agent-hub@${version} (expected one release with a sha512 integrity from \`npm view --json\`); check \`npm view @staix/agent-hub@${version} version dist.integrity --json\` and your npm version`);
|
|
222
231
|
}
|
|
223
|
-
return { version, integrity
|
|
232
|
+
return { version, integrity };
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
/** #272: the registry's latest release, the target when `--to` is left out. */
|
|
227
236
|
export async function latestRelease(run: RunCommand = runCommand): Promise<string> {
|
|
228
237
|
const result = await run(["npm", "view", "@staix/agent-hub", "version", "--json"], { timeoutMs: 30_000 });
|
|
229
238
|
if (result.code !== 0) throw new Error("registry metadata unavailable for @staix/agent-hub; name the release with --to <version>");
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (typeof version !== "string") throw new Error("registry returned an unexpected latest release; name the release with --to <version>");
|
|
239
|
+
const version = npmViewJson(result.stdout);
|
|
240
|
+
if (typeof version !== "string") throw new Error("registry returned an unexpected latest release (expected one version from `npm view --json`); check `npm view @staix/agent-hub version --json`, or name the release with --to <version>");
|
|
233
241
|
return exactVersion(version);
|
|
234
242
|
}
|
|
235
243
|
|
package/src/cli/reset.ts
CHANGED
|
@@ -149,7 +149,7 @@ export function resetRuntime(stateDir: string, project: Project): { settled: str
|
|
|
149
149
|
const db = openHubDatabase(file);
|
|
150
150
|
try {
|
|
151
151
|
db.transaction(() => {
|
|
152
|
-
for (const table of ["budget_pauses", "conductor_holds"]) {
|
|
152
|
+
for (const table of ["budget_pauses", "conductor_holds", "model_overrides"]) {
|
|
153
153
|
if (db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table)) db.run(`DELETE FROM ${table}`);
|
|
154
154
|
}
|
|
155
155
|
})();
|
package/src/cli/status-lines.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { projectRoomUnavailable } from "../rooms/status.ts";
|
|
2
2
|
import type { ContextView } from "../hub/context-window.ts";
|
|
3
|
-
import { memorySyncNextStep, type MemorySyncCause } from "../hub/memory-sync-state.ts";
|
|
3
|
+
import { memorySyncNextStep, WRITER_CATEGORIES, WRITER_CAUSE_CLASSES, type MemorySyncCause } from "../hub/memory-sync-state.ts";
|
|
4
4
|
// What `ahub status` prints for a peer and for a model backend, kept pure so it can be checked.
|
|
5
5
|
|
|
6
6
|
export interface PeerRow { permissionMode?: string; context?: ContextView; state?: string; queued?: number; queuedImportant?: number; needsReview?: number; heldBy?: string; holdNote?: string; reviewObserverHoldNote?: string; liveAccepted?: string[]; oldestQueuedAt?: number; importantWaitingSince?: number; importantWaitingNote?: string; attached?: boolean; attachMode?: "channel" | "tools-only"; attachEvidence?: string; toolsOnly?: string; pullOnly?: boolean; lastInboxAt?: number | null; permissionNote?: string; reviewerUnavailable?: string; paused?: string; servedBy?: string; requestedModel?: string }
|
|
7
7
|
export interface BackendRow { kind?: string; alias?: string; state?: string; active?: number; requestedModel?: string; actualModel?: string; provider?: string; coolingUntil?: string; failures?: number; failingUntil?: string }
|
|
8
|
-
export interface MemorySyncRow { state?: string; at?: number; cause?: string; declinedCount?: number; declinedIdsKnown?: false; withheldCount?: number; exportedCount?: number; placementActive?: false; nextStep?: string }
|
|
8
|
+
export interface MemorySyncRow { state?: string; at?: number; cause?: string; declinedCount?: number; declinedIdsKnown?: false; withheldCount?: number; exportedCount?: number; placementActive?: false; writerCategory?: string; writerCauseClass?: string; nextStep?: string }
|
|
9
9
|
export interface ReviewInputHealthRow { state?: string; cause?: string | null }
|
|
10
10
|
|
|
11
11
|
/** Aliases are already namespaced ("dgx/coding"); only an alias that is not gets its kind in front of it. */
|
|
@@ -32,11 +32,15 @@ export function memorySyncLine(sync: MemorySyncRow, time = (at: number) => new D
|
|
|
32
32
|
&& Number.isSafeInteger(sync.exportedCount) && sync.exportedCount! >= 0)
|
|
33
33
|
return `ok${stamp}; ${sync.withheldCount} row${sync.withheldCount === 1 ? "" : "s"} withheld locally; ${sync.exportedCount} exported`;
|
|
34
34
|
if (state !== "failed") return `${state}${stamp}`;
|
|
35
|
-
const causes = ["declined", "remote", "binding", "stopped", "transport", "unknown"];
|
|
35
|
+
const causes = ["declined", "remote", "binding", "stopped", "transport", "writer", "unknown"];
|
|
36
36
|
const cause = (causes.includes(sync.cause ?? "") ? sync.cause : "unknown") as MemorySyncCause;
|
|
37
37
|
if (cause === "declined" && sync.placementActive === false) {
|
|
38
38
|
const declined = Number.isSafeInteger(sync.declinedCount) && sync.declinedCount! >= 0 ? String(sync.declinedCount) : "unknown";
|
|
39
|
-
return "machine sync blocked: " + declined + " declined rows; per-row placement is not active on this store. Activation is person-only: preview with `amem migrate --sync-placement --dry-run` and declare every writer via --writer-root.
|
|
39
|
+
return "machine sync blocked: " + declined + " declined rows; per-row placement is not active on this store. Activation is person-only: preview with `amem migrate --sync-placement --dry-run` and declare every writer via --writer-root. Inspect with `ahub memory declined`.";
|
|
40
|
+
}
|
|
41
|
+
if (cause === "writer") {
|
|
42
|
+
const category = WRITER_CATEGORIES.includes(sync.writerCategory ?? "") ? sync.writerCategory : "unknown", causeClass = WRITER_CAUSE_CLASSES.includes(sync.writerCauseClass ?? "") ? sync.writerCauseClass : "unavailable";
|
|
43
|
+
return `failed (writer attestation refused: ${category}, ${causeClass})${stamp}; next: ${memorySyncNextStep("writer")}`;
|
|
40
44
|
}
|
|
41
45
|
const count = Number.isSafeInteger(sync.declinedCount) && sync.declinedCount! >= 0 ? `, ${sync.declinedCount} declined` : "";
|
|
42
46
|
const ids = cause === "declined" && sync.declinedIdsKnown === false ? "; declined ID set unknown" : "";
|
package/src/hub/budget.ts
CHANGED
|
@@ -54,7 +54,7 @@ export interface BudgetConfig {
|
|
|
54
54
|
wait_max_min: number;
|
|
55
55
|
}
|
|
56
56
|
// wait_max_min is off here, as approvals.notify is: a project config turns it on (30 unless it says otherwise).
|
|
57
|
-
export const DEFAULT_BUDGET: BudgetConfig = { gate: 0.
|
|
57
|
+
export const DEFAULT_BUDGET: BudgetConfig = { gate: 0.99, stale_min: 30, poll_min: 10, checkpoint_timeout_s: 90, kimi_tokens_5h: 0, wait_max_min: 0 };
|
|
58
58
|
|
|
59
59
|
export type BudgetLedgerEntry =
|
|
60
60
|
| { type: "budget_pause"; peer: PeerId; reason: string; window: string; resetsAt: number; by: "hub" | "person" }
|
package/src/hub/bus.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { DELIVERY_RETRY_WARNING } from "./delivery-hints.ts";
|
|
|
9
9
|
import { MAX_DEFERRED_HUB, RecoveryDeferredHubError, validateDeferredHub } from "./deferred-hub.ts";
|
|
10
10
|
import { parseRemotePeerId } from "../rooms/application.ts";
|
|
11
11
|
import { PreEffectToolRefusal } from "./tool-refusal.ts";
|
|
12
|
+
import { stripRecordLines } from "../memory/records.ts";
|
|
12
13
|
|
|
13
14
|
export type { DeliveryReceipt } from "./peers.ts";
|
|
14
15
|
|
|
@@ -214,6 +215,10 @@ export class Bus {
|
|
|
214
215
|
* peer was handed (the request is in it, never condensed), so a reply that answers an invocation is found by identity, not by addressing.
|
|
215
216
|
*/
|
|
216
217
|
onAnswer?: (peer: PeerId, text: string, answers: readonly Envelope[]) => boolean;
|
|
218
|
+
/** Completed adapter answers only, under the same public-input observer ceiling as memory delivery. */
|
|
219
|
+
onFinalAnswer?: (peer: PeerId, text: string, context: { out: readonly Envelope[]; originals: readonly Envelope[] }) => void;
|
|
220
|
+
/** Count-only forwarding telemetry; never emitted for a private, review-scoped or unbound answer. */
|
|
221
|
+
onFinalAnswerBytes?: (peer: PeerId, counts: { before: number; after: number }) => void;
|
|
217
222
|
/** Host ledger authority; absent or unknown never permits invocation replay. */
|
|
218
223
|
canRetryInvocation?: (id: string) => boolean;
|
|
219
224
|
onInvocationUncertain?: (id: string) => void;
|
|
@@ -315,7 +320,10 @@ export class Bus {
|
|
|
315
320
|
peer.onDelivery = (receipt) => { if (this.peers.get(peer.id) === peer) this.deliveryReceipt(peer.id, receipt); };
|
|
316
321
|
peer.onDoing = (kind, text) => { if (kind === "reset") this.clearReviewProvenance(peer.id); if (!this.closed && this.peers.get(peer.id) === peer) this.opts.doing?.(peer.id, kind, text); };
|
|
317
322
|
this.opts.doing?.(peer.id, "reset"); // a new adapter never shows what the one it replaces was doing
|
|
318
|
-
|
|
323
|
+
// A final still traverses the current onMessage observer exactly once. This one-shot
|
|
324
|
+
// marker is host-local, never a caller-supplied envelope or wire field.
|
|
325
|
+
let finalMessage: { body: string; options: EnvelopeOpts | undefined; complete: boolean } | undefined;
|
|
326
|
+
const receive = (text: string, opts?: EnvelopeOpts, final = false, complete = true) => {
|
|
319
327
|
if (!this.isRostered(peer.id)) return `off roster: ${peer.id}`;
|
|
320
328
|
// #445 C2: an answer to an invocation is found by what the peer was handed, never by how the answer is addressed, so a headless agent whose final answer is
|
|
321
329
|
// normally shared with every peer cannot leak it. It is the invocation's answer when it names the request itself (an adapter may report its delivery complete before
|
|
@@ -331,12 +339,29 @@ export class Bus {
|
|
|
331
339
|
: parentId === undefined && this.invocationTurnOpen(peer.id)
|
|
332
340
|
? pushedInvocation ? pushed : pulledInvocation ? pulled : undefined
|
|
333
341
|
: undefined;
|
|
342
|
+
let finalPriority: Priority | undefined;
|
|
343
|
+
if (final) {
|
|
344
|
+
const context = parentId === undefined ? pulled ?? pushed : namedPush ? pushed : namedPull ? pulled : undefined;
|
|
345
|
+
const raw = text;
|
|
346
|
+
finalPriority = parseMarker(raw).priority;
|
|
347
|
+
// Strip before invocation routing, publication, journaling, digests or the console event tap.
|
|
348
|
+
text = stripRecordLines(raw);
|
|
349
|
+
let observable = !this.closed && this.peers.get(peer.id) === peer && !!context && opts?.private !== true && !remoteId(peer.id) && !opts?.to?.some(remoteId) && !answerParent;
|
|
350
|
+
try {
|
|
351
|
+
observable &&= this.opts.reviewObserverInputExcluded?.(peer.id, context!.originals) !== true &&
|
|
352
|
+
![...context!.out, ...context!.originals].some(env => env.private || isReviewObserverExcluded(env) || isInvocationKind(env) || remoteId(env.from) || env.to?.some(remoteId));
|
|
353
|
+
} catch { observable = false; }
|
|
354
|
+
if (observable) {
|
|
355
|
+
if (complete) try { this.onFinalAnswer?.(peer.id, raw, context!); } catch { /* capture is optional and never changes answer routing */ }
|
|
356
|
+
try { this.onFinalAnswerBytes?.(peer.id, { before: Buffer.byteLength(parseMarker(raw).body), after: Buffer.byteLength(parseMarker(text).body) }); } catch { /* count-only observer */ }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
334
359
|
if (this.onAnswer && answerParent && (namedPush || namedPull || parentId === undefined && this.invocationTurnOpen(peer.id))) {
|
|
335
360
|
let taken = false;
|
|
336
361
|
try { taken = this.onAnswer(peer.id, text, answerParent.out) === true; } catch { taken = true; } // a throwing host never turns a callee's answer into a broadcast
|
|
337
362
|
if (taken) return;
|
|
338
363
|
}
|
|
339
|
-
const
|
|
364
|
+
const parsed = parseMarker(text), priority = finalPriority ?? parsed.priority, body = parsed.body;
|
|
340
365
|
if (!body) return;
|
|
341
366
|
// What the peer was handed differs from what it stands for once a delivery was condensed; both the audience
|
|
342
367
|
// and the priority ceiling are about what it stands for.
|
|
@@ -393,6 +418,19 @@ export class Bus {
|
|
|
393
418
|
if (hushed.length) this.note(peer.id, noteLine(HUB, "decision", `your message was not delivered to ${hushed.map((h) => h.peer).join(", ")}: ${hushed[0]!.reason}`));
|
|
394
419
|
return heldBack ? hushed[0]!.reason : published.dropped === "hop" ? published : undefined;
|
|
395
420
|
};
|
|
421
|
+
peer.onMessage = (text, opts) => {
|
|
422
|
+
if (this.peers.get(peer.id) !== peer) return;
|
|
423
|
+
const marker = finalMessage?.body === text && finalMessage.options === opts ? finalMessage : undefined;
|
|
424
|
+
if (marker) finalMessage = undefined; // Consume before observers can emit nested diagnostics.
|
|
425
|
+
return receive(text, opts, marker !== undefined, marker?.complete ?? true);
|
|
426
|
+
};
|
|
427
|
+
peer.onFinalAnswer = (text, opts, complete = true) => {
|
|
428
|
+
if (this.closed || this.peers.get(peer.id) !== peer) return;
|
|
429
|
+
const previous = finalMessage;
|
|
430
|
+
finalMessage = { body: text, options: opts, complete };
|
|
431
|
+
try { return peer.onMessage?.(text, opts); }
|
|
432
|
+
finally { finalMessage = previous; }
|
|
433
|
+
};
|
|
396
434
|
peer.onFailed = (envs, failure) => {
|
|
397
435
|
// The adapter got the condensed list; what has to come back is what that list replaced.
|
|
398
436
|
const last = this.lastDelivery.get(peer.id);
|
package/src/hub/code-facts.ts
CHANGED
|
@@ -95,9 +95,13 @@ export class CodeFacts {
|
|
|
95
95
|
catch{throw Error("code facts cleanup unconfirmed");}
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
/** #1261: `trace` is a read-only timing tap for tests (the step's name, its wall time in ms and its outcome). Absent in production, where it costs one undefined check per step; it never changes an answer, a bound or a cap. */
|
|
99
|
+
constructor(private readonly options:{env?:NodeJS.ProcessEnv;timeoutMs?:number;now?:()=>number;trace?:(step:string,ms:number,outcome:string)=>void}={}) {}
|
|
99
100
|
private git(repo:string,args:string[]) {
|
|
100
|
-
|
|
101
|
+
const trace=this.options.trace,t0=trace?performance.now():0;
|
|
102
|
+
const r=hubGitSync(["-C",repo,...args],{encoding:"utf8",env:hubGitEnv(childEnv(),{repositoryIsolation:true}),timeout:5000,maxBuffer:1024*1024});
|
|
103
|
+
trace?.(`git ${args.slice(0,2).join(" ")}`,performance.now()-t0,r.error?.message??`status ${r.status}`);
|
|
104
|
+
return r;
|
|
101
105
|
}
|
|
102
106
|
private value(repo:string,args:string[]):string {const r=this.git(repo,args);if(r.status!==0)throw Error();return r.stdout.trim();}
|
|
103
107
|
private nativeEnv(cwd:string):NodeJS.ProcessEnv {
|
|
@@ -115,7 +119,7 @@ export class CodeFacts {
|
|
|
115
119
|
private async run(bin:string,args:string[],cwd:string):Promise<{code:number;out:string}> {
|
|
116
120
|
if(this.closing || this.active?.cancelled)return {code:1,out:""};
|
|
117
121
|
return new Promise(resolve=>{
|
|
118
|
-
let child:ReturnType<typeof spawn>;
|
|
122
|
+
let child:ReturnType<typeof spawn>;const trace=this.options.trace,t0=trace?performance.now():0;
|
|
119
123
|
try{child=spawn(process.execPath,["-e",LAUNCHER,"--",bin,...args],{cwd,env:this.nativeEnv(cwd),detached:true,stdio:["ignore","pipe","ignore","pipe"]});}catch{resolve({code:1,out:""});return;}
|
|
120
124
|
let size=0,done=false,failed=false,control="";const parts:Buffer[]=[];
|
|
121
125
|
let ended=false;const outputEnd=new Promise<void>(resolveEnd=>child.stdout!.once("end",()=>{ended=true;resolveEnd();}));
|
|
@@ -125,6 +129,7 @@ export class CodeFacts {
|
|
|
125
129
|
if(!ended)code=null;
|
|
126
130
|
try{await stopOwnedProcess(child,{group:true,termMs:50,killMs:500});}catch{this.failedChild=child;code=null;}
|
|
127
131
|
if(this.cancelActive===cancel)this.cancelActive=undefined;
|
|
132
|
+
trace?.(`ripwire ${args.find(a=>a.startsWith("--"))?.split("=")[0]??"launch"}`,performance.now()-t0,failed?"failed, capped or cancelled":`code ${code}`);
|
|
128
133
|
resolve({code:failed?1:code??1,out:!failed && code!==null?Buffer.concat(parts).toString("utf8"):""});
|
|
129
134
|
};
|
|
130
135
|
const cancel=()=>{void finish(null);};this.cancelActive=cancel;
|
|
@@ -157,7 +162,9 @@ export class CodeFacts {
|
|
|
157
162
|
if(this.git(repo,["worktree","add","--detach","--no-checkout",cwd,head]).status!==0)throw Error();registered=true;
|
|
158
163
|
if(this.git(cwd,["read-tree",head]).status!==0)throw Error();
|
|
159
164
|
// --batch needs explicit object ids; no working-tree filters or checkout hooks are invoked.
|
|
165
|
+
const catAt=this.options.trace?performance.now():0;
|
|
160
166
|
const result=hubGitSync(["-C",repo,"cat-file","--batch"],{input:entries.map(e=>e.oid).join("\n")+"\n",encoding:"latin1",env:hubGitEnv(childEnv(),{repositoryIsolation:true}),timeout:5000,maxBuffer:MAX_TREE_BYTES+1024*1024});
|
|
167
|
+
this.options.trace?.("git cat-file --batch",performance.now()-catAt,result.error?.message??`status ${result.status}`);
|
|
161
168
|
if(result.status!==0)throw Error();
|
|
162
169
|
const bytes=Buffer.from(result.stdout,"latin1");let offset=0,total=0;
|
|
163
170
|
for(const entry of entries){
|
package/src/hub/completions.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface SettleFacts {
|
|
|
26
26
|
runningChecks: number;
|
|
27
27
|
/** Rows held or in `needs_review`: uncertain, so never settled. */
|
|
28
28
|
heldDeliveries: number;
|
|
29
|
+
/** Native background work when the adapter exposes an inventory; null means the inventory exists but is unreadable or incomplete. */
|
|
30
|
+
nativeBackground?: number | null;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export type RunState = "working" | "needs-input" | "settled";
|
|
@@ -33,7 +35,7 @@ export type RunState = "working" | "needs-input" | "settled";
|
|
|
33
35
|
/** The one rule. A permission or question wins over everything else: the person has to act, so the run is needs-input and never "done". */
|
|
34
36
|
export function settleVerdict(facts: SettleFacts): RunState {
|
|
35
37
|
if (facts.openPermissions > 0 || facts.openQuestions > 0) return "needs-input";
|
|
36
|
-
if (!facts.signal || facts.queuedContinuations > 0 || facts.ownedChildren > 0 || facts.runningChecks > 0 || facts.heldDeliveries > 0) return "working";
|
|
38
|
+
if (!facts.signal || facts.queuedContinuations > 0 || facts.ownedChildren > 0 || facts.runningChecks > 0 || facts.heldDeliveries > 0 || facts.nativeBackground === null || (facts.nativeBackground ?? 0) > 0) return "working";
|
|
37
39
|
return "settled";
|
|
38
40
|
}
|
|
39
41
|
|
package/src/hub/config-apply.ts
CHANGED
|
@@ -226,6 +226,7 @@ const schema: Record<string, Check> = {
|
|
|
226
226
|
else if (key === "remote") remoteRunnerConfig(value);
|
|
227
227
|
else if (/^[a-z][a-z0-9_]{0,47}$/.test(key)) text(value, `${k}.${key}`); else fail(`${k}.${key}`, "unknown key");
|
|
228
228
|
} },
|
|
229
|
+
visuals: block({ retention_days: number(1, 3650, true) }),
|
|
229
230
|
snapshots: block({ enabled: bool, keep: number(1, 1_000_000, true) }), research: block({ enabled: bool, runners: researchRunners }), bench: block({ enabled: bool }),
|
|
230
231
|
worktrees: block({ quiet_days: number(0, 3650, true), alert_over: number(0, 100_000, true) }),
|
|
231
232
|
// #1053: how long an unanswered question waits (`ttl_s`, 1 minute to 30 days) and, per adapter kind, how long a `hub_ask` call waits for the person before it answers `pending`.
|
|
@@ -276,7 +277,7 @@ const restart: Record<string, string> = {
|
|
|
276
277
|
profile: "Session tool schemas and profile contracts are bound at startup", profile_overrides: "Session tool schemas and profile contracts are bound at startup",
|
|
277
278
|
text_projection: "Converter and cache ownership are bound at startup", corpus: "The corpus index is bound to its roots at startup", terminal: "Terminal provider is supplied by the daemon lifecycle", bench: "Benchmark execution environment is selected before work starts",
|
|
278
279
|
};
|
|
279
|
-
const live = new Set(["action_policy", "permission", "roster", "roles", "planner", "conductor", "protect_instruction_files", "task_sweep", "capabilities", "invocation", "research", "snapshots", "worktrees", "tasks.wip_limits", "tasks.escalate_after", "tasks.auto_pull", "tasks.queue_backpressure", "schedules", "approvals.notify", "questions", "pet", "specs"]);
|
|
280
|
+
const live = new Set(["visuals", "action_policy", "permission", "roster", "roles", "planner", "conductor", "protect_instruction_files", "task_sweep", "capabilities", "invocation", "research", "snapshots", "worktrees", "tasks.wip_limits", "tasks.escalate_after", "tasks.auto_pull", "tasks.queue_backpressure", "schedules", "approvals.notify", "questions", "pet", "specs"]);
|
|
280
281
|
const nextMemory = new Set(["memory.inject_tokens", "memory.brief_items", "memory.suggest_edges"]);
|
|
281
282
|
/** Closed top-level schema; leaf overrides retain the existing session/runtime boundaries. */
|
|
282
283
|
export function configApply(key: string, scopes: readonly ConfigIdentityScope[] = []): ApplyRule {
|
|
@@ -21,7 +21,7 @@ export const PROTOCOL = 17;
|
|
|
21
21
|
/** The descriptor's legacy protocol field remains this floor for previous published clients. */
|
|
22
22
|
export const PROTOCOL_MIN = 16;
|
|
23
23
|
// Literal metadata is read without executing a recovery target; consistency is pinned in tests.
|
|
24
|
-
export const DAEMON_FEATURES = ["project_autonomy", "conductor_recovery", "encrypted_rooms", "external_adapter_instances", "external_adapter_modules", "configured_acp", "native_hook_launch", "task_cancel", "invocation", "recovery_deferred_hub", "peer_instances", "role_stages", "task_planners", "release_trains", "reviewer_pool", "native_tui_launch", "terminal_owner", "cli_audit_delegated", "action_policy_floor", "calendar_schedules", "research_records", "research_dispatch", "research_dropped_offers", "research_experiments", "research_comparisons", "research_runners", "task_handoff", "session_identity", "schema_guard", "design_specs", "queue_discard_states", "tool_write_idempotency", "research_evidence", "review_comments", "questions", "accepted_delivery_settlement", "completions", "review_observer_input_state", "task_handoff_key", "task_handoff_seq", "peer_takeover"] as const;
|
|
24
|
+
export const DAEMON_FEATURES = ["model_overrides", "visuals", "project_autonomy", "conductor_recovery", "encrypted_rooms", "external_adapter_instances", "external_adapter_modules", "configured_acp", "native_hook_launch", "task_cancel", "invocation", "recovery_deferred_hub", "peer_instances", "role_stages", "task_planners", "release_trains", "reviewer_pool", "native_tui_launch", "terminal_owner", "cli_audit_delegated", "action_policy_floor", "calendar_schedules", "research_records", "research_dispatch", "research_dropped_offers", "research_experiments", "research_comparisons", "research_runners", "task_handoff", "session_identity", "schema_guard", "design_specs", "queue_discard_states", "tool_write_idempotency", "research_evidence", "review_comments", "questions", "accepted_delivery_settlement", "completions", "review_observer_input_state", "task_handoff_key", "task_handoff_seq", "peer_takeover"] as const;
|
|
25
25
|
/** Protocols a current coordinator may authenticate while upgrading a running source. */
|
|
26
26
|
export const RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, 14, 15, 16, PROTOCOL] as const;
|
|
27
27
|
|