@staix/agent-hub 0.16.0 → 0.17.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 +18 -0
- package/README.md +2 -2
- package/docs/agent-notes/adapters.md +18 -5
- package/docs/agent-notes/daemon.md +4 -3
- package/docs/agent-notes/tasks.md +2 -2
- package/docs/agent-notes/tests.md +5 -3
- package/docs/dogfood.md +6 -3
- package/docs/operations.md +45 -18
- package/docs/quickstart.md +6 -2
- package/docs/roles.md +1 -0
- package/docs/sdlc.md +11 -11
- package/docs/security.md +15 -5
- package/docs/smoke.md +51 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +40 -4
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +3 -1
- package/docs/verification/2026-10-25-0.17.0.md +74 -0
- package/docs/verification/2026-10-25-codex-0164-reviewer-tool-call.md +52 -0
- package/docs/verification/2026-10-25-kimi-231-reviewer-compat.md +48 -0
- package/docs/verification/2026-10-25-kimi-usage-ac11.md +48 -0
- package/docs/verification/2026-10-25-qwen-dual-output-hub-e2e.md +23 -0
- package/docs/verification/2026-10-25-qwen-dual-output-t0.md +20 -0
- package/docs/verification/2026-10-25-reviewer-no-verdict-diagnostics.md +35 -0
- package/docs/verified.json +20 -17
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1022 -160
- package/src/adapters/codex-review-isolation.ts +10 -1
- package/src/adapters/compat.ts +7 -2
- package/src/adapters/conformance/enrollment.ts +18 -4
- package/src/adapters/conformance/factories.ts +4 -1
- package/src/adapters/conformance/kit.ts +11 -0
- package/src/adapters/definitions/native-cli-entry-qwen.ts +32 -4
- package/src/adapters/definitions/native-cli-preview-qwen.ts +9 -1
- package/src/adapters/definitions/native-factories.ts +8 -0
- package/src/adapters/definitions/native-recovery.ts +8 -0
- package/src/adapters/definitions/qwen.ts +7 -4
- package/src/adapters/qwen-dual.ts +578 -0
- package/src/adapters/registry-types.ts +5 -1
- package/src/adapters/registry.ts +9 -1
- package/src/adapters/reviewer-mcp.ts +12 -6
- package/src/adapters/reviewer-print.ts +100 -17
- package/src/cli/upgrade.ts +4 -3
- package/src/hub/daemon.ts +134 -22
- package/src/hub/memory-plan-guard.ts +8 -33
- package/src/hub/tasks.ts +25 -12
- package/src/memory/brief.ts +45 -5
|
@@ -10,6 +10,8 @@ export interface ReviewerMcpOptions {
|
|
|
10
10
|
tools: readonly HubTool[];
|
|
11
11
|
/** Fixed host port; the host still applies Tasks' role, privacy, head and running-child fences. */
|
|
12
12
|
call(name: ReviewerToolName, args: Record<string, unknown>): Promise<string>;
|
|
13
|
+
/** Diagnostic metadata only; never tool arguments or successful result text. */
|
|
14
|
+
onTool?(name: ReviewerToolName, refusal?: string): void;
|
|
13
15
|
}
|
|
14
16
|
const NAMES: readonly ReviewerToolName[] = ["hub_review", "hub_task_show"];
|
|
15
17
|
const BODY_CAP = 256 * 1024, RESULT_CAP = 4 * 1024 * 1024;
|
|
@@ -60,20 +62,24 @@ export function createReviewerMcpSession(options: ReviewerMcpOptions): ReviewerM
|
|
|
60
62
|
if (!record(body.params)) return error("tool parameters required");
|
|
61
63
|
const name = body.params.name;
|
|
62
64
|
if (name !== "hub_review" && name !== "hub_task_show") return error("tool unavailable");
|
|
65
|
+
const observe = (refusal?: string) => { try { options.onTool?.(name, refusal); } catch { /* Diagnostics never change tool authority or results. */ } };
|
|
66
|
+
const refuse = (message: string, code?: number) => { observe(message); return error(message, code); };
|
|
67
|
+
observe();
|
|
63
68
|
let args = body.params.arguments;
|
|
64
|
-
if (typeof args === "string") { try { args = JSON.parse(args); } catch { return
|
|
65
|
-
if (!record(args) || args.id !== options.task || !Number.isSafeInteger(args.id)) return
|
|
66
|
-
if (++calls > 64) return
|
|
67
|
-
if (!current()) return
|
|
69
|
+
if (typeof args === "string") { try { args = JSON.parse(args); } catch { return refuse("invalid tool arguments"); } }
|
|
70
|
+
if (!record(args) || args.id !== options.task || !Number.isSafeInteger(args.id)) return refuse("tool is bound to its review task");
|
|
71
|
+
if (++calls > 64) return refuse("review tool call limit", -32002);
|
|
72
|
+
if (!current()) return refuse("review authority ended", -32001);
|
|
68
73
|
try {
|
|
69
74
|
const result = await options.call(name, args);
|
|
70
75
|
// Tasks.review rechecks its captured authority through its final write. A successful verdict
|
|
71
76
|
// ends that in_review binding itself; acknowledge its committed result, never fresh reads.
|
|
72
|
-
if (name !== "hub_review" && !current()) return
|
|
73
|
-
if (Buffer.byteLength(result) > RESULT_CAP) return
|
|
77
|
+
if (name !== "hub_review" && !current()) return refuse("review authority ended", -32001);
|
|
78
|
+
if (Buffer.byteLength(result) > RESULT_CAP) return refuse("tool result exceeds review limit");
|
|
74
79
|
return reply({ content: [{ type: "text", text: result }] });
|
|
75
80
|
} catch (failure) {
|
|
76
81
|
// The same host tool boundary supplies refusals to the released MCP transport.
|
|
82
|
+
observe(failure instanceof Error ? failure.message : "review tool refused");
|
|
77
83
|
const message = failure instanceof Error ? sanitize(failure.message).slice(0, 4000) : "review tool refused";
|
|
78
84
|
return reply({ isError: true, content: [{ type: "text", text: message }] });
|
|
79
85
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ChildProcess } from "node:child_process";
|
|
2
2
|
import { OwnedPrintChild, PrintChildCleanupError } from "./claude-print-child.ts";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { HUB, frame } from "../hub/envelope.ts";
|
|
4
|
+
import { HUB, frame, sanitize } from "../hub/envelope.ts";
|
|
5
5
|
import { BasePeer } from "../hub/peers.ts";
|
|
6
6
|
import type { Envelope } from "../hub/envelope.ts";
|
|
7
|
-
import type { ReviewerMcpSession } from "./reviewer-mcp.ts";
|
|
7
|
+
import type { ReviewerMcpSession, ReviewerToolName } from "./reviewer-mcp.ts";
|
|
8
8
|
|
|
9
9
|
/** #722: the closed list of legs a reviewer's per-review start can fail on. A class names a leg, never a path, content or token. */
|
|
10
10
|
export const REVIEWER_CAUSE_CLASSES = ["version", "binary", "mcp_inventory", "plugins_hooks_apps", "sandbox_proof", "credential_root", "timeout", "unknown"] as const;
|
|
@@ -27,6 +27,10 @@ export function reviewerUnavailableReason(cause: ReviewerCauseClass, detail?: st
|
|
|
27
27
|
export class ReviewerUnavailableError extends Error {
|
|
28
28
|
constructor(message: string, readonly causeClass: ReviewerCauseClass, readonly detail?: string) { super(message); }
|
|
29
29
|
}
|
|
30
|
+
/** A refused request says nothing about the reviewer's native isolation or ability to start. */
|
|
31
|
+
class ReviewerAdmissionError extends Error {
|
|
32
|
+
constructor(readonly reason: "stale_binding" | "policy_changed") { super("review admission changed"); }
|
|
33
|
+
}
|
|
30
34
|
|
|
31
35
|
export interface ReviewerPrintUsage { input_tokens?: number; output_tokens?: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number }
|
|
32
36
|
export interface ReviewerIsolationReceipt {
|
|
@@ -76,6 +80,31 @@ export interface ReviewerBackend {
|
|
|
76
80
|
prepare(context: ReviewerPrepareContext): Promise<ReviewerLaunch | { unavailable: string; causeClass?: ReviewerCauseClass; detail?: string }>;
|
|
77
81
|
usage?(event: unknown): ReviewerPrintUsage | undefined;
|
|
78
82
|
failure?(event: unknown): "quota" | undefined;
|
|
83
|
+
/** Structured native MCP events only, never model messages or tool arguments. */
|
|
84
|
+
toolTrace?(event: unknown): ReviewerToolTrace | undefined;
|
|
85
|
+
}
|
|
86
|
+
export interface ReviewerToolTrace { tool: ReviewerToolName; refusal?: string }
|
|
87
|
+
export interface ReviewerNoVerdict extends ReviewerReviewBinding {
|
|
88
|
+
task: number;
|
|
89
|
+
exitCode: number | null;
|
|
90
|
+
reviewCalled: boolean;
|
|
91
|
+
lastTool?: ReviewerToolName;
|
|
92
|
+
refusal?: string;
|
|
93
|
+
/** Preserve the first full host error across later truncated native echoes; differing host errors stay uncertain. */
|
|
94
|
+
hostRefusal?: string;
|
|
95
|
+
refusalUncertain?: true;
|
|
96
|
+
}
|
|
97
|
+
type ReviewerTraceState = Pick<ReviewerNoVerdict, "reviewCalled" | "lastTool" | "refusal" | "hostRefusal" | "refusalUncertain">;
|
|
98
|
+
/** Host-produced receipt reason: an unconfirmed normal review fallback still needs a person's settlement. */
|
|
99
|
+
export const REVIEWER_FALLBACK_UNCONFIRMED = "reviewer fallback unconfirmed: ";
|
|
100
|
+
/** `safeRefusal` must have passed the host's complete privacy/secret screen before truncation. */
|
|
101
|
+
export function reviewerNoVerdictLine(peer: string, diagnostic: ReviewerNoVerdict, safeRefusal?: string, withheld = false): string {
|
|
102
|
+
const prefix = `[${peer}] review of task #${diagnostic.task}: no verdict exit=${diagnostic.exitCode ?? "unknown"}; `;
|
|
103
|
+
if (withheld) return prefix + "diagnostic withheld";
|
|
104
|
+
const detail = diagnostic.lastTool ? `last hub tool ${diagnostic.lastTool}; ${diagnostic.reviewCalled ? "hub_review called" : "no hub_review call"}` : "no hub_review call";
|
|
105
|
+
const line = safeRefusal === undefined ? undefined : Array.from(sanitize(safeRefusal).replace(/\s+/g, " "))
|
|
106
|
+
.filter(char => !/\p{C}/u.test(char)).slice(0, 240).join("");
|
|
107
|
+
return prefix + detail + (diagnostic.refusal === undefined && diagnostic.hostRefusal === undefined ? "" : `; refusal: ${line ?? "withheld"}`);
|
|
79
108
|
}
|
|
80
109
|
export interface ReviewerFailure {
|
|
81
110
|
task: number;
|
|
@@ -99,7 +128,9 @@ export interface ReviewerPrintOptions {
|
|
|
99
128
|
facts?(env: Envelope): string | undefined;
|
|
100
129
|
verdictSince(task: number, since: number): boolean;
|
|
101
130
|
onUsage?(usage: ReviewerPrintUsage, task: number): void;
|
|
102
|
-
|
|
131
|
+
/** True confirms that the host re-offered the current review; void/false retains uncertainty. */
|
|
132
|
+
onUnavailable?(failure: ReviewerFailure, current: () => boolean): boolean | void | Promise<boolean | void>;
|
|
133
|
+
onNoVerdict?(diagnostic: ReviewerNoVerdict, current: () => boolean): void;
|
|
103
134
|
log?(line: string): void;
|
|
104
135
|
taskMcp?(task: number, current: () => boolean): ReviewerMcpSession;
|
|
105
136
|
/** Synchronous host task/privacy/policy fence, checked again after awaited preparation. */
|
|
@@ -139,6 +170,24 @@ export class ReviewerPrintPeer extends BasePeer {
|
|
|
139
170
|
reviewing: number | undefined;
|
|
140
171
|
private reviewSeq = 0;
|
|
141
172
|
private runningBinding: { task: number; binding: ReviewerReviewBinding } | undefined;
|
|
173
|
+
private activeTrace: { task: number; generation: number; value: ReviewerTraceState } | undefined;
|
|
174
|
+
/** Host observations are diagnostic data and never grant tool or verdict authority. */
|
|
175
|
+
recordToolTrace(task: number, trace: ReviewerToolTrace): void {
|
|
176
|
+
if (trace.tool !== "hub_review" && trace.tool !== "hub_task_show") return;
|
|
177
|
+
const active = this.activeTrace, binding = this.runningBinding;
|
|
178
|
+
if (!active || active.task !== task || active.generation !== this.generation || this.reviewing !== task || binding?.task !== task || this.opts.bindingCurrent?.(task, binding.binding) === false) return;
|
|
179
|
+
if (typeof trace.refusal === "string") {
|
|
180
|
+
if (active.value.hostRefusal === undefined) active.value.hostRefusal = trace.refusal;
|
|
181
|
+
else if (active.value.hostRefusal !== trace.refusal) active.value.refusalUncertain = true;
|
|
182
|
+
}
|
|
183
|
+
this.observeTool(active.value, trace);
|
|
184
|
+
}
|
|
185
|
+
private observeTool(value: ReviewerTraceState, trace: ReviewerToolTrace): void {
|
|
186
|
+
if (trace.tool !== "hub_review" && trace.tool !== "hub_task_show") return;
|
|
187
|
+
value.lastTool = trace.tool;
|
|
188
|
+
value.refusal = typeof trace.refusal === "string" ? trace.refusal : undefined;
|
|
189
|
+
if (trace.tool === "hub_review") value.reviewCalled = true;
|
|
190
|
+
}
|
|
142
191
|
/** Immutable envelope identity of this running child, never the board's later head. */
|
|
143
192
|
reviewBinding(task: number): ReviewerReviewBinding | undefined {
|
|
144
193
|
return this.reviewing === task && this.runningBinding?.task === task ? this.runningBinding.binding : undefined;
|
|
@@ -234,10 +283,20 @@ export class ReviewerPrintPeer extends BasePeer {
|
|
|
234
283
|
if (task === undefined) continue; // a withdrawal or other notice: nothing to review
|
|
235
284
|
if (env.private) { failure = { state: "needs_review", reason: "a private review is the person's, never a cloud reviewer's" }; break; }
|
|
236
285
|
const since = Date.now();
|
|
237
|
-
let result: { code: number | null; quota: boolean };
|
|
286
|
+
let result: { code: number | null; quota: boolean; trace: ReviewerTraceState };
|
|
238
287
|
try { result = await this.review(env, task, generation); }
|
|
239
288
|
catch (error) {
|
|
240
289
|
if (generation !== this.generation) return;
|
|
290
|
+
if (error instanceof ReviewerAdmissionError) {
|
|
291
|
+
this.opts.log?.(`[${this.id}] review of task #${task} skipped: ${error.reason === "stale_binding" ? "stale binding" : "review policy changed"}`);
|
|
292
|
+
if (error.reason === "policy_changed") {
|
|
293
|
+
missing.push(task);
|
|
294
|
+
const target = batch.targets[index];
|
|
295
|
+
if (target) reroute.push({ ...target, cause: "no_verdict" });
|
|
296
|
+
}
|
|
297
|
+
batch.next = index + 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
241
300
|
if (error instanceof PrintChildCleanupError) {
|
|
242
301
|
// Unavailable until a stop confirms the group gone: no new review runs beside the survivors.
|
|
243
302
|
this.opts.log?.(`[${this.id}] review of task #${task}: ${error.message}`);
|
|
@@ -259,6 +318,11 @@ export class ReviewerPrintPeer extends BasePeer {
|
|
|
259
318
|
if (!recorded) {
|
|
260
319
|
missing.push(task);
|
|
261
320
|
const target = batch.targets[index];
|
|
321
|
+
const diagnostic = { ...target, task, exitCode: result.code, ...result.trace };
|
|
322
|
+
try {
|
|
323
|
+
if (this.opts.onNoVerdict) this.opts.onNoVerdict(diagnostic, () => generation === this.generation);
|
|
324
|
+
else this.opts.log?.(reviewerNoVerdictLine(this.id, diagnostic));
|
|
325
|
+
} catch { this.opts.log?.(reviewerNoVerdictLine(this.id, { task, exitCode: result.code, reviewCalled: false }, undefined, true)); }
|
|
262
326
|
if (target) reroute.push({ ...target, cause: result.quota ? "quota" : "no_verdict" });
|
|
263
327
|
if (result.quota) {
|
|
264
328
|
reroute.push(...remainingFailures(batch, index + 1, "quota"));
|
|
@@ -267,25 +331,30 @@ export class ReviewerPrintPeer extends BasePeer {
|
|
|
267
331
|
}
|
|
268
332
|
}
|
|
269
333
|
batch.next = index + 1;
|
|
270
|
-
if (result.code !== 0) this.opts.log?.(`[${this.id}] review of task #${task} exited ${result.code}`);
|
|
334
|
+
if (recorded && result.code !== 0) this.opts.log?.(`[${this.id}] review of task #${task} exited ${result.code}`);
|
|
271
335
|
}
|
|
272
336
|
if (generation !== this.generation) return;
|
|
273
337
|
this.clearWatchdog(); // the child is gone; keep busy while the host settles the remaining walk
|
|
274
|
-
|
|
275
|
-
|
|
338
|
+
const reoffered = await this.reportFailures(reroute, generation);
|
|
339
|
+
if (generation !== this.generation) return;
|
|
340
|
+
const unresolved = missing.length > 0 && !reoffered;
|
|
341
|
+
const normalNoVerdict = !failure && reroute.length > 0 && reroute.every(value => value.cause === "no_verdict");
|
|
342
|
+
this.settle(failure?.state ?? (unresolved ? "needs_review" : "completed"), failure?.reason ?? (unresolved ? `${normalNoVerdict ? REVIEWER_FALLBACK_UNCONFIRMED : ""}no verdict recorded for task ${missing.map(id => `#${id}`).join(", ")}` : undefined));
|
|
276
343
|
if (generation === this.generation) { this.batch = undefined; this.setState("idle"); }
|
|
277
344
|
}
|
|
278
345
|
|
|
279
|
-
private async reportFailures(failures: readonly ReviewerFailure[], generation: number): Promise<
|
|
346
|
+
private async reportFailures(failures: readonly ReviewerFailure[], generation: number): Promise<boolean> {
|
|
280
347
|
const seen = new Set<string>();
|
|
348
|
+
let reoffered = true;
|
|
281
349
|
for (const failed of failures) {
|
|
282
|
-
if (generation !== this.generation) return;
|
|
350
|
+
if (generation !== this.generation) return false;
|
|
283
351
|
const identity = JSON.stringify([failed.task, failed.head, failed.flowRevision]);
|
|
284
352
|
if (seen.has(identity)) continue;
|
|
285
353
|
seen.add(identity);
|
|
286
|
-
try { await this.opts.onUnavailable?.(failed, () => generation === this.generation); }
|
|
287
|
-
catch { this.opts.log?.(`[${this.id}] reviewer fallback could not be recorded; inspect the task`); }
|
|
354
|
+
try { if (await this.opts.onUnavailable?.(failed, () => generation === this.generation) !== true) reoffered = false; }
|
|
355
|
+
catch { reoffered = false; this.opts.log?.(`[${this.id}] reviewer fallback could not be recorded; inspect the task`); }
|
|
288
356
|
}
|
|
357
|
+
return reoffered;
|
|
289
358
|
}
|
|
290
359
|
|
|
291
360
|
private prepare(sessionId: string, input?: string, task?: number, mcpConfig?: string): Promise<ReviewerLaunch> {
|
|
@@ -313,14 +382,21 @@ export class ReviewerPrintPeer extends BasePeer {
|
|
|
313
382
|
return done;
|
|
314
383
|
}
|
|
315
384
|
|
|
316
|
-
private
|
|
317
|
-
if (this.opts.
|
|
385
|
+
private assertAdmission(env: Envelope, task: number, binding: ReviewerReviewBinding): void {
|
|
386
|
+
if (this.opts.bindingCurrent?.(task, binding) === false) throw new ReviewerAdmissionError("stale_binding");
|
|
387
|
+
if (this.opts.admit?.(env) === false) throw new ReviewerAdmissionError(this.opts.bindingCurrent?.(task, binding) === false ? "stale_binding" : "policy_changed");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private async review(env: Envelope, task: number, generation: number): Promise<{ code: number | null; quota: boolean; trace: ReviewerTraceState }> {
|
|
391
|
+
const revision=Number(env.refs?.reviewRevision);
|
|
392
|
+
const binding: ReviewerReviewBinding=Object.freeze({head:env.refs?.commit,flowRevision:Number.isSafeInteger(revision)&&revision>0?revision:undefined});
|
|
393
|
+
this.assertAdmission(env, task, binding);
|
|
318
394
|
let facts: string | undefined;
|
|
319
395
|
try { facts = this.opts.facts?.(env); } catch (error) { this.opts.log?.(`[${this.id}] review facts for task #${task} failed: ${(error as Error).message}`); }
|
|
320
396
|
const input = `${frame(env)}${facts ? `\n\n${facts}` : ""}`;
|
|
321
397
|
let launch: ReviewerLaunch | undefined;
|
|
322
|
-
const
|
|
323
|
-
|
|
398
|
+
const trace: ReviewerTraceState = { reviewCalled: false };
|
|
399
|
+
this.activeTrace = { task, generation, value: trace };
|
|
324
400
|
const mcp = this.opts.backend.taskMcp ? this.opts.taskMcp?.(task, () => generation === this.generation && this.reviewToken(task) !== undefined && this.opts.bindingCurrent?.(task,binding) !== false) : undefined;
|
|
325
401
|
let mcpClosed=false;
|
|
326
402
|
const closeMcp=()=>{ if(mcpClosed)return; mcpClosed=true; mcp?.close(); if(this.reviewMcpClose===closeMcp)this.reviewMcpClose=undefined; };
|
|
@@ -330,7 +406,9 @@ export class ReviewerPrintPeer extends BasePeer {
|
|
|
330
406
|
const release = () => releasing ??= Promise.resolve().then(() => launch?.cleanup?.()).then(() => {});
|
|
331
407
|
try {
|
|
332
408
|
launch = await this.prepare(randomUUID(), input, task, mcp?.config);
|
|
333
|
-
if (generation !== this.generation
|
|
409
|
+
if (generation !== this.generation) throw new ReviewerAdmissionError("stale_binding");
|
|
410
|
+
this.assertAdmission(env, task, binding);
|
|
411
|
+
if (launch.current?.() === false) throw new ReviewerUnavailableError("reviewer isolation changed before child spawn", "binary");
|
|
334
412
|
this.receipt = launch.isolation;
|
|
335
413
|
let quota = false;
|
|
336
414
|
const child = new OwnedPrintChild({ peer: this.id, command: launch.command, args: launch.args,
|
|
@@ -343,6 +421,10 @@ export class ReviewerPrintPeer extends BasePeer {
|
|
|
343
421
|
if (this.childCurrent) policyTimer = setInterval(() => { this.reviewToken(task); }, 100);
|
|
344
422
|
},
|
|
345
423
|
onFrame: event => {
|
|
424
|
+
try {
|
|
425
|
+
const observed = this.opts.backend.toolTrace?.(event);
|
|
426
|
+
if (observed && generation === this.generation) this.observeTool(trace, observed);
|
|
427
|
+
} catch { /* Optional trace metadata never changes review execution. */ }
|
|
346
428
|
const usage = this.opts.backend.usage?.(event);
|
|
347
429
|
if (usage) this.opts.onUsage?.(usage, task);
|
|
348
430
|
if (this.opts.backend.failure?.(event) === "quota") quota = true;
|
|
@@ -359,8 +441,9 @@ export class ReviewerPrintPeer extends BasePeer {
|
|
|
359
441
|
this.childCurrent = launch.watchCurrent ? launch.current : undefined;
|
|
360
442
|
const code = await child.done;
|
|
361
443
|
if (this.child === child) this.child = undefined;
|
|
362
|
-
return { code, quota };
|
|
444
|
+
return { code, quota, trace };
|
|
363
445
|
} finally {
|
|
446
|
+
if (this.activeTrace?.value === trace) this.activeTrace = undefined;
|
|
364
447
|
closeMcp();
|
|
365
448
|
if (policyTimer) clearInterval(policyTimer);
|
|
366
449
|
try { if (!ownedChild || this.child !== ownedChild) { await release(); if (this.childCleanup === release) this.childCleanup = undefined; } }
|
package/src/cli/upgrade.ts
CHANGED
|
@@ -143,8 +143,8 @@ export interface RecoveryOperation {
|
|
|
143
143
|
/** Positive typed store observation; disposal rechecks it before accepting a repaired-session loss. */
|
|
144
144
|
missingRecordedSession?: { projectId: string; peer: RecoveryNativePeer; sessionId: string; kind: "rollout" | "transcript"; checkedPath?: string };
|
|
145
145
|
freshOverride?: { peer: RecoveryNativePeer; evidence: NonNullable<RecoveryOperation["missingRecordedSession"]>; checked: "missing"; at: number };
|
|
146
|
-
/** #215: every human disposition, in order. Operator reasons only; never task or message text. */
|
|
147
|
-
audit?: { at: number; action: "fresh-session" | "stop-and-archive"; reason: string; peer?: string; projects: string[] }[];
|
|
146
|
+
/** #215: every human disposition, in order. Operator reasons only; never task or message text. #776: `cause` is the operation's own `error` at that moment, the text the receipt already held. */
|
|
147
|
+
audit?: { at: number; action: "fresh-session" | "stop-and-archive"; reason: string; peer?: string; cause?: string; projects: string[] }[];
|
|
148
148
|
disposition?: { choice: "stop-and-archive"; at: number; projects: Record<string, string>; continuity?: Record<string, "not-archived" | "unavailable"> };
|
|
149
149
|
/** #215: a refusal no resume can get past was recorded (FinalRefusal); `next` no longer offers resume. */
|
|
150
150
|
final?: boolean;
|
|
@@ -1070,7 +1070,8 @@ export async function disposeRecovery(id: string, choice: { fresh: string } | {
|
|
|
1070
1070
|
delete op.blockedReattachment;
|
|
1071
1071
|
delete op.presentationCause;
|
|
1072
1072
|
abandoned(op); // #227: in the same write as the disposition
|
|
1073
|
-
|
|
1073
|
+
// #776: the error that blocked the operation is cleared on disposal, so the audit keeps it.
|
|
1074
|
+
(op.audit ??= []).push({ at, action: "stop-and-archive", reason, ...(op.error ? { cause: op.error } : {}), projects: acts.map((a) => a.planned.project.id) });
|
|
1074
1075
|
// Recorded before the first act: a crash inside a stop leaves a disposition that resume refuses to override.
|
|
1075
1076
|
delete op.failure;
|
|
1076
1077
|
op.updatedAt = driver.now(); writeOperation(id, op, home);
|