@gajae-code/coding-agent 0.17.4 → 0.17.6
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 +82 -0
- package/dist/types/config/autorouting-tier-map.d.ts +1031 -3
- package/dist/types/config/file-lock.d.ts +4 -2
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
- package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
- package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
- package/dist/types/harness-control-plane/finalize.d.ts +14 -3
- package/dist/types/main.d.ts +1 -1
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +13 -6
- package/dist/types/sdk/broker/broker.d.ts +4 -2
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/identity.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +11 -3
- package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
- package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +8 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -1
- package/dist/types/sdk/prompt-deadline-flush.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +7 -2
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tools/bash-interactive.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +10 -1
- package/dist/types/tools/subagent.d.ts +8 -1
- package/package.json +7 -7
- package/src/cli/public-command-registry.ts +8 -2
- package/src/cli/setup-cli.ts +1 -0
- package/src/cli/stats-cli.ts +11 -1
- package/src/config/autorouting-tier-map.ts +529 -3
- package/src/config/file-lock.ts +59 -14
- package/src/config/model-discovery-manager.ts +4 -4
- package/src/config/model-profile-activation.ts +83 -23
- package/src/config/model-registry.ts +72 -12
- package/src/config/model-resolver.ts +6 -1
- package/src/coordinator-mcp/question-state.ts +4 -1
- package/src/coordinator-mcp/server.ts +14 -6
- package/src/exec/bash-executor.ts +3 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
- package/src/gjc-runtime/state-writer.ts +317 -6
- package/src/harness-control-plane/control-endpoint.ts +24 -6
- package/src/harness-control-plane/finalize.ts +312 -25
- package/src/harness-control-plane/owner.ts +323 -89
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/main.ts +69 -19
- package/src/modes/acp/acp-agent.ts +581 -37
- package/src/modes/components/gjc-bundle-settings.ts +21 -1
- package/src/modes/components/tool-status-header.ts +72 -6
- package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
- package/src/runtime-mcp/loader.ts +6 -3
- package/src/runtime-mcp/manager.ts +177 -75
- package/src/sdk/broker/broker.ts +1058 -150
- package/src/sdk/broker/ensure.ts +65 -8
- package/src/sdk/broker/identity.ts +13 -0
- package/src/sdk/broker/lifecycle-ledger.ts +16 -4
- package/src/sdk/broker/managed-task-dag.ts +1927 -0
- package/src/sdk/broker/managed-task-verification.ts +642 -0
- package/src/sdk/broker/spawn-authority.ts +140 -7
- package/src/sdk/broker/startup-failure.ts +80 -1
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +22 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +5 -1
- package/src/sdk/cli/session-cli.ts +18 -0
- package/src/sdk/host/session-runtime.test.ts +46 -0
- package/src/sdk/host/session-runtime.ts +13 -6
- package/src/sdk/lifecycle/service.ts +2 -0
- package/src/sdk/prompt-deadline-flush.ts +26 -4
- package/src/sdk/session.ts +587 -115
- package/src/session/agent-session.ts +60 -14
- package/src/session/internal/managed-session-scope.ts +10 -0
- package/src/session/internal/managed-session-storage.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/task/executor.ts +46 -17
- package/src/task/types.ts +2 -0
- package/src/tools/bash-interactive.ts +2 -0
- package/src/tools/bash.ts +20 -0
- package/src/tools/fetch.ts +21 -5
- package/src/tools/subagent-render.ts +24 -1
- package/src/tools/subagent.ts +27 -1
- package/src/utils/mupdf.ts +11 -4
|
@@ -17,13 +17,21 @@ import { existsSync } from "node:fs";
|
|
|
17
17
|
import type { AgentWireOwnerObservation } from "../modes/shared/agent-wire/event-contract";
|
|
18
18
|
import { observeAgentWireFrame } from "../modes/shared/agent-wire/event-observation";
|
|
19
19
|
import { classifyRecovery } from "./classifier";
|
|
20
|
-
import { ControlServer, type EndpointHandler, type
|
|
21
|
-
import {
|
|
20
|
+
import { ControlServer, type EndpointHandler, type EndpointHandlerRequest } from "./control-endpoint";
|
|
21
|
+
import {
|
|
22
|
+
defaultFinalizeChecks,
|
|
23
|
+
type FinalizeChecks,
|
|
24
|
+
runFinalize,
|
|
25
|
+
type ValidationCommandSpec,
|
|
26
|
+
ValidationObservationUncertainError,
|
|
27
|
+
type ValidationRun,
|
|
28
|
+
} from "./finalize";
|
|
22
29
|
import { type OperateResult, operate } from "./operate";
|
|
23
30
|
import { preserveDirtyWorktree } from "./preserve";
|
|
24
31
|
import { RECEIPT_SPOOL_DIR_ENV, withReceiptSpoolDir } from "./receipt-spool";
|
|
25
32
|
import {
|
|
26
33
|
buildReceipt,
|
|
34
|
+
type ReceiptEnvelope,
|
|
27
35
|
type ReceiptSubject,
|
|
28
36
|
requiresVanishBeforeAction,
|
|
29
37
|
type ValidationEvidence,
|
|
@@ -50,6 +58,7 @@ import { buildStateView, nextAllowedActions, submitUnavailableReason } from "./s
|
|
|
50
58
|
import {
|
|
51
59
|
appendEvent,
|
|
52
60
|
controlSocketPath,
|
|
61
|
+
type ReceiptIndexEntry,
|
|
53
62
|
readEvents,
|
|
54
63
|
readSessionState,
|
|
55
64
|
sessionPaths,
|
|
@@ -165,6 +174,15 @@ export class RuntimeOwner {
|
|
|
165
174
|
#framePumpFailure: unknown = null;
|
|
166
175
|
#coalesced = new Map<string, true>();
|
|
167
176
|
#stopPromise: Promise<void> | null = null;
|
|
177
|
+
#retiring = false;
|
|
178
|
+
#retirePromise: Promise<PrimitiveResponse> | null = null;
|
|
179
|
+
#lifecycleMutation: Promise<void> = Promise.resolve();
|
|
180
|
+
#activeRetirementSensitiveWork = new Set<Promise<void>>();
|
|
181
|
+
#activeValidationWork = new Set<{
|
|
182
|
+
controller: AbortController;
|
|
183
|
+
done: Promise<void>;
|
|
184
|
+
}>();
|
|
185
|
+
#validationStopFailure: unknown = null;
|
|
168
186
|
#lastStopFailures: unknown[] = [];
|
|
169
187
|
#reportedStopFailures = new Set<string>();
|
|
170
188
|
#transportClosed = false;
|
|
@@ -270,10 +288,14 @@ export class RuntimeOwner {
|
|
|
270
288
|
const state = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
271
289
|
if (!state) throw new Error(`session_not_found:${this.#opts.sessionId}`);
|
|
272
290
|
const reconciled = reconcileLiveOwnerState(state);
|
|
273
|
-
if (reconciled.reconciled)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
291
|
+
if (reconciled.reconciled)
|
|
292
|
+
return this.#withLifecycleMutation(async () => {
|
|
293
|
+
const latest = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
294
|
+
if (!latest) throw new Error(`session_not_found:${this.#opts.sessionId}`);
|
|
295
|
+
const current = reconcileLiveOwnerState(latest);
|
|
296
|
+
if (current.reconciled) await this.#framePersistence.writeSessionState(this.#opts.root, current.state);
|
|
297
|
+
return current.state;
|
|
298
|
+
});
|
|
277
299
|
return state;
|
|
278
300
|
}
|
|
279
301
|
|
|
@@ -425,24 +447,158 @@ export class RuntimeOwner {
|
|
|
425
447
|
return fn();
|
|
426
448
|
}
|
|
427
449
|
|
|
428
|
-
async #
|
|
450
|
+
async #withLifecycleMutation<T>(fn: () => Promise<T>): Promise<T> {
|
|
451
|
+
const previous = this.#lifecycleMutation;
|
|
452
|
+
let unlock!: () => void;
|
|
453
|
+
this.#lifecycleMutation = new Promise<void>(resolve => {
|
|
454
|
+
unlock = resolve;
|
|
455
|
+
});
|
|
456
|
+
await previous;
|
|
457
|
+
try {
|
|
458
|
+
return await fn();
|
|
459
|
+
} finally {
|
|
460
|
+
unlock();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
#withRetirementSensitiveWork<T>(work: () => Promise<T>): Promise<T> {
|
|
465
|
+
const operation = work();
|
|
466
|
+
let done!: Promise<void>;
|
|
467
|
+
done = operation
|
|
468
|
+
.then(
|
|
469
|
+
() => undefined,
|
|
470
|
+
() => undefined,
|
|
471
|
+
)
|
|
472
|
+
.then(() => {
|
|
473
|
+
this.#activeRetirementSensitiveWork.delete(done);
|
|
474
|
+
});
|
|
475
|
+
this.#activeRetirementSensitiveWork.add(done);
|
|
476
|
+
return operation;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async #joinRetirementSensitiveWork(): Promise<void> {
|
|
480
|
+
await Promise.all([...this.#activeRetirementSensitiveWork]);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
#withOwnedValidationWork<T>(requestSignal: AbortSignal, work: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
484
|
+
const controller = new AbortController();
|
|
485
|
+
const done = Promise.withResolvers<void>();
|
|
486
|
+
const active = { controller, done: done.promise };
|
|
487
|
+
const abortFromRequest = (): void => controller.abort(requestSignal.reason);
|
|
488
|
+
if (requestSignal.aborted) abortFromRequest();
|
|
489
|
+
else requestSignal.addEventListener("abort", abortFromRequest, { once: true });
|
|
490
|
+
if (this.#stopPromise) controller.abort(new Error("owner_stopping"));
|
|
491
|
+
this.#activeValidationWork.add(active);
|
|
492
|
+
return (async () => {
|
|
493
|
+
try {
|
|
494
|
+
return await work(controller.signal);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (controller.signal.aborted && !(error instanceof ValidationObservationUncertainError)) {
|
|
497
|
+
this.#validationStopFailure = error;
|
|
498
|
+
}
|
|
499
|
+
throw error;
|
|
500
|
+
} finally {
|
|
501
|
+
requestSignal.removeEventListener("abort", abortFromRequest);
|
|
502
|
+
this.#activeValidationWork.delete(active);
|
|
503
|
+
done.resolve();
|
|
504
|
+
}
|
|
505
|
+
})();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async #stopActiveValidationWork(): Promise<void> {
|
|
509
|
+
const active = [...this.#activeValidationWork];
|
|
510
|
+
for (const work of active) work.controller.abort(new Error("owner_stopping"));
|
|
511
|
+
await Promise.all(active.map(work => work.done));
|
|
512
|
+
if (this.#validationStopFailure !== null) {
|
|
513
|
+
throw new AggregateError([this.#validationStopFailure], "Runtime owner validation cleanup failed.");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async #persistValidationLifecycle(
|
|
518
|
+
signal: AbortSignal,
|
|
519
|
+
lifecycle: SessionState["lifecycle"],
|
|
520
|
+
blockers?: string[],
|
|
521
|
+
): Promise<{ state: SessionState; persisted: boolean }> {
|
|
522
|
+
return this.#withLifecycleMutation(async () => {
|
|
523
|
+
const state = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
524
|
+
if (!state) throw new Error(`session_not_found:${this.#opts.sessionId}`);
|
|
525
|
+
if (signal.aborted || state.lifecycle === "retired") {
|
|
526
|
+
return { state, persisted: false };
|
|
527
|
+
}
|
|
528
|
+
if (state.lifecycle === "completed") return { state, persisted: lifecycle === "completed" };
|
|
529
|
+
state.lifecycle = lifecycle;
|
|
530
|
+
if (blockers) state.blockers = blockers;
|
|
531
|
+
state.updatedAt = new Date(this.#opts.clock ? this.#opts.clock() : Date.now()).toISOString();
|
|
532
|
+
await writeSessionState(this.#opts.root, state);
|
|
533
|
+
return { state, persisted: true };
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async #persistOwnedReceipt(
|
|
538
|
+
signal: AbortSignal,
|
|
539
|
+
family: "validation" | "completion" | "review-failure" | "review-verdict",
|
|
540
|
+
receipt: ReceiptEnvelope<unknown>,
|
|
541
|
+
): Promise<ReceiptIndexEntry | null> {
|
|
542
|
+
return this.#withLifecycleMutation(async () => {
|
|
543
|
+
const state = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
544
|
+
if (!state) throw new Error(`session_not_found:${this.#opts.sessionId}`);
|
|
545
|
+
if (
|
|
546
|
+
signal.aborted ||
|
|
547
|
+
this.#retiring ||
|
|
548
|
+
this.#stopPromise ||
|
|
549
|
+
state.lifecycle === "retired" ||
|
|
550
|
+
state.lifecycle === "completed"
|
|
551
|
+
) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
return writeReceiptImmutable(this.#opts.root, this.#opts.sessionId, family, receipt.receiptId, receipt);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#finalizeChecksForOwner(checks: FinalizeChecks, signal: AbortSignal): FinalizeChecks {
|
|
559
|
+
return {
|
|
560
|
+
runValidation: (spec, validationSignal) => checks.runValidation(spec, validationSignal ?? signal),
|
|
561
|
+
resolveCommit: () => checks.resolveCommit(),
|
|
562
|
+
commitOnBranch: (commit, branch) => checks.commitOnBranch(commit, branch),
|
|
563
|
+
prOrIssue: () => checks.prOrIssue(),
|
|
564
|
+
writeReceipt: (family, receipt) => this.#persistOwnedReceipt(signal, family, receipt),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async #handle(req: EndpointHandlerRequest): Promise<unknown> {
|
|
569
|
+
if (this.#stopPromise && !this.#retiring && req.verb === "retire") {
|
|
570
|
+
return { ok: false, error: "owner_stopping" };
|
|
571
|
+
}
|
|
572
|
+
if (
|
|
573
|
+
(this.#stopPromise || this.#retiring) &&
|
|
574
|
+
(req.verb === "submit" ||
|
|
575
|
+
req.verb === "recover" ||
|
|
576
|
+
req.verb === "observe" ||
|
|
577
|
+
req.verb === "validate" ||
|
|
578
|
+
req.verb === "finalize" ||
|
|
579
|
+
req.verb === "operate")
|
|
580
|
+
) {
|
|
581
|
+
return { ok: false, error: this.#retiring ? "owner_retiring" : "owner_stopping" };
|
|
582
|
+
}
|
|
429
583
|
switch (req.verb) {
|
|
430
584
|
case "ping":
|
|
431
585
|
return { ok: true, ownerId: this.ownerId, leaseEpoch: this.#leaseEpoch };
|
|
432
586
|
case "submit":
|
|
433
|
-
return this.#submit(req.input);
|
|
587
|
+
return this.#withRetirementSensitiveWork(() => this.#submit(req.input));
|
|
434
588
|
case "observe":
|
|
435
|
-
return this.#observe();
|
|
589
|
+
return this.#withRetirementSensitiveWork(() => this.#observe());
|
|
436
590
|
case "retire":
|
|
437
591
|
return this.#retire();
|
|
438
592
|
case "finalize":
|
|
439
|
-
return this.#withReceiptSpoolFromInput(req.input, () => this.#finalize(req.input));
|
|
593
|
+
return this.#withReceiptSpoolFromInput(req.input, () => this.#finalize(req.input, req.signal));
|
|
440
594
|
case "recover":
|
|
441
|
-
return this.#
|
|
595
|
+
return this.#withRetirementSensitiveWork(() =>
|
|
596
|
+
this.#withReceiptSpoolFromInput(req.input, () => this.#recover()),
|
|
597
|
+
);
|
|
442
598
|
case "validate":
|
|
443
|
-
return this.#withReceiptSpoolFromInput(req.input, () => this.#validate());
|
|
599
|
+
return this.#withReceiptSpoolFromInput(req.input, () => this.#validate(req.signal));
|
|
444
600
|
case "operate":
|
|
445
|
-
return this.#withReceiptSpoolFromInput(req.input, () => this.#operate(req.input));
|
|
601
|
+
return this.#withReceiptSpoolFromInput(req.input, () => this.#operate(req.input, req.signal));
|
|
446
602
|
default:
|
|
447
603
|
return { ok: false, error: `owner_unsupported_verb:${req.verb}` };
|
|
448
604
|
}
|
|
@@ -512,52 +668,79 @@ export class RuntimeOwner {
|
|
|
512
668
|
};
|
|
513
669
|
}
|
|
514
670
|
|
|
515
|
-
async #validate(): Promise<PrimitiveResponse> {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
await this.#emit("info", "validated", { count: 0, reviewOnly: true });
|
|
523
|
-
return this.#response(state, { validation: [], reviewOnly: true });
|
|
524
|
-
}
|
|
525
|
-
const checks = this.#finalizeChecks ?? defaultFinalizeChecks(state.handle.workspace);
|
|
526
|
-
const commit = await checks.resolveCommit();
|
|
527
|
-
const subject: ReceiptSubject = {
|
|
528
|
-
workspace: state.handle.workspace,
|
|
529
|
-
branch: state.handle.branch,
|
|
530
|
-
head: commit,
|
|
531
|
-
commit,
|
|
532
|
-
};
|
|
533
|
-
const validation: { name: string; valid: boolean; exitStatus: number }[] = [];
|
|
534
|
-
for (const spec of this.#validationCommands ?? []) {
|
|
535
|
-
const run = await checks.runValidation(spec);
|
|
536
|
-
const evidence: ValidationEvidence = {
|
|
537
|
-
command: spec.name,
|
|
538
|
-
exactCommand: run.exactCommand,
|
|
539
|
-
cwd: run.cwd,
|
|
540
|
-
exitStatus: run.exitStatus,
|
|
541
|
-
pass: run.pass,
|
|
542
|
-
commitUnderTest: commit,
|
|
671
|
+
async #validate(requestSignal: AbortSignal): Promise<PrimitiveResponse> {
|
|
672
|
+
return this.#withOwnedValidationWork(requestSignal, async signal => {
|
|
673
|
+
const state = await this.#loadState();
|
|
674
|
+
const canceledResponse = async (evidence: Record<string, unknown>): Promise<PrimitiveResponse> => {
|
|
675
|
+
const latest = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
676
|
+
if (!latest) throw new Error(`session_not_found:${this.#opts.sessionId}`);
|
|
677
|
+
return this.#response(latest, evidence, false);
|
|
543
678
|
};
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
679
|
+
if (signal.aborted) return canceledResponse({ canceled: true, validation: [] });
|
|
680
|
+
if (state.handle.mode === "review") {
|
|
681
|
+
// Review-only sessions do not run implementation validation and never attach PR metadata.
|
|
682
|
+
const updated = await this.#persistValidationLifecycle(signal, "validating");
|
|
683
|
+
if (updated.persisted) await this.#emit("info", "validated", { count: 0, reviewOnly: true });
|
|
684
|
+
return this.#response(updated.state, { validation: [], reviewOnly: true }, updated.persisted);
|
|
685
|
+
}
|
|
686
|
+
const checks = this.#finalizeChecks ?? defaultFinalizeChecks(state.handle.workspace);
|
|
687
|
+
const commit = await checks.resolveCommit();
|
|
688
|
+
if (signal.aborted) return canceledResponse({ canceled: true, validation: [] });
|
|
689
|
+
const subject: ReceiptSubject = {
|
|
690
|
+
workspace: state.handle.workspace,
|
|
691
|
+
branch: state.handle.branch,
|
|
692
|
+
head: commit,
|
|
693
|
+
commit,
|
|
694
|
+
};
|
|
695
|
+
const validation: { name: string; valid: boolean; exitStatus: number }[] = [];
|
|
696
|
+
for (const spec of this.#validationCommands ?? []) {
|
|
697
|
+
if (signal.aborted) return canceledResponse({ canceled: true, validation });
|
|
698
|
+
let run: ValidationRun;
|
|
699
|
+
try {
|
|
700
|
+
run = await checks.runValidation(spec, signal);
|
|
701
|
+
} catch (caught) {
|
|
702
|
+
if (caught instanceof ValidationObservationUncertainError) {
|
|
703
|
+
if (signal.aborted) return canceledResponse({ canceled: true, validation });
|
|
704
|
+
const updated = await this.#persistValidationLifecycle(signal, "blocked", [
|
|
705
|
+
`validation-unknown:${spec.name}`,
|
|
706
|
+
]);
|
|
707
|
+
if (updated.persisted) {
|
|
708
|
+
await this.#emit("critical", "validation_uncertain", {
|
|
709
|
+
name: spec.name,
|
|
710
|
+
exactCommand: caught.exactCommand,
|
|
711
|
+
cwd: caught.cwd,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return this.#response(updated.state, { uncertain: true, name: spec.name, validation }, false);
|
|
715
|
+
}
|
|
716
|
+
throw caught;
|
|
717
|
+
}
|
|
718
|
+
if (signal.aborted) return canceledResponse({ canceled: true, validation });
|
|
719
|
+
const evidence: ValidationEvidence = {
|
|
720
|
+
command: spec.name,
|
|
721
|
+
exactCommand: run.exactCommand,
|
|
722
|
+
cwd: run.cwd,
|
|
723
|
+
exitStatus: run.exitStatus,
|
|
724
|
+
pass: run.pass,
|
|
725
|
+
commitUnderTest: commit,
|
|
726
|
+
};
|
|
727
|
+
const receipt = buildReceipt<ValidationEvidence>({
|
|
728
|
+
receiptId: `val-${Date.now()}-${randomBytes(4).toString("hex")}`,
|
|
729
|
+
sessionId: this.#opts.sessionId,
|
|
730
|
+
family: "validation",
|
|
731
|
+
source: "owner",
|
|
732
|
+
subject,
|
|
733
|
+
evidence,
|
|
734
|
+
valid: run.pass,
|
|
735
|
+
});
|
|
736
|
+
const entry = await this.#persistOwnedReceipt(signal, "validation", receipt);
|
|
737
|
+
if (!entry) return canceledResponse({ canceled: true, validation });
|
|
738
|
+
validation.push({ name: spec.name, valid: validateReceipt(receipt).valid, exitStatus: run.exitStatus });
|
|
739
|
+
}
|
|
740
|
+
const updated = await this.#persistValidationLifecycle(signal, "validating");
|
|
741
|
+
if (updated.persisted) await this.#emit("info", "validated", { count: validation.length });
|
|
742
|
+
return this.#response(updated.state, { validation }, updated.persisted);
|
|
743
|
+
});
|
|
561
744
|
}
|
|
562
745
|
|
|
563
746
|
async #recover(): Promise<PrimitiveResponse> {
|
|
@@ -610,26 +793,31 @@ export class RuntimeOwner {
|
|
|
610
793
|
return this.#response(state, { decision, observation: recoveryObservation, vanishReceiptId });
|
|
611
794
|
}
|
|
612
795
|
|
|
613
|
-
async #operate(input: Record<string, unknown
|
|
796
|
+
async #operate(input: Record<string, unknown>, requestSignal: AbortSignal): Promise<PrimitiveResponse> {
|
|
797
|
+
return this.#withOwnedValidationWork(requestSignal, signal => this.#operateOwned(input, signal));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
async #operateOwned(input: Record<string, unknown>, signal: AbortSignal): Promise<PrimitiveResponse> {
|
|
614
801
|
const goal = typeof input.goal === "string" ? input.goal : "";
|
|
615
|
-
|
|
802
|
+
const state = await this.#loadState();
|
|
616
803
|
if (!goal) return this.#response(state, { error: "empty-goal" }, false);
|
|
804
|
+
const baseChecks = this.#finalizeChecks ?? defaultFinalizeChecks(state.handle.workspace);
|
|
805
|
+
const finalizeChecks = this.#finalizeChecksForOwner(baseChecks, signal);
|
|
617
806
|
const emitOperateEvent = async (
|
|
618
807
|
severity: Severity,
|
|
619
808
|
kind: string,
|
|
620
809
|
evidence: Record<string, unknown>,
|
|
621
810
|
): Promise<void> => {
|
|
811
|
+
if (signal.aborted) return;
|
|
622
812
|
if (kind === "operate_blocked" || kind === "operate_finalized") {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
kind === "operate_finalized" && evidence.completed === true ? "completed" : "blocked";
|
|
626
|
-
terminalState.blockers = Array.isArray(evidence.blockers)
|
|
813
|
+
const lifecycle = kind === "operate_finalized" && evidence.completed === true ? "completed" : "blocked";
|
|
814
|
+
const blockers = Array.isArray(evidence.blockers)
|
|
627
815
|
? evidence.blockers.filter((blocker): blocker is string => typeof blocker === "string")
|
|
628
|
-
:
|
|
816
|
+
: lifecycle === "completed"
|
|
629
817
|
? []
|
|
630
|
-
:
|
|
631
|
-
|
|
632
|
-
|
|
818
|
+
: undefined;
|
|
819
|
+
const persisted = await this.#persistValidationLifecycle(signal, lifecycle, blockers);
|
|
820
|
+
if (!persisted.persisted) return;
|
|
633
821
|
}
|
|
634
822
|
await this.#emit(severity, kind, evidence);
|
|
635
823
|
};
|
|
@@ -640,24 +828,30 @@ export class RuntimeOwner {
|
|
|
640
828
|
branch: state.handle.branch ?? "",
|
|
641
829
|
transport: this.#opts.transport,
|
|
642
830
|
observe: () => this.#observeGit(),
|
|
643
|
-
finalizeChecks
|
|
831
|
+
finalizeChecks,
|
|
644
832
|
validationCommands: this.#validationCommands,
|
|
645
833
|
maxIterations: typeof input.maxIterations === "number" ? input.maxIterations : 5,
|
|
646
834
|
emit: emitOperateEvent,
|
|
647
835
|
});
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
836
|
+
if (signal.aborted) {
|
|
837
|
+
const latest = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
838
|
+
if (!latest) throw new Error(`session_not_found:${this.#opts.sessionId}`);
|
|
839
|
+
return this.#response(latest, { canceled: true, operate: result }, false);
|
|
840
|
+
}
|
|
841
|
+
// Persist the loop's lifecycle/blockers from a fresh state under the same lock
|
|
842
|
+
// as retire so an older operation snapshot cannot resurrect terminal state.
|
|
843
|
+
const persisted = await this.#persistValidationLifecycle(signal, result.lifecycle, result.blockers);
|
|
844
|
+
return this.#response(persisted.state, { operate: result }, result.completed && persisted.persisted);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async #finalize(input: Record<string, unknown>, requestSignal: AbortSignal): Promise<PrimitiveResponse> {
|
|
848
|
+
return this.#withOwnedValidationWork(requestSignal, signal => this.#finalizeOwned(input, signal));
|
|
655
849
|
}
|
|
656
850
|
|
|
657
|
-
async #
|
|
851
|
+
async #finalizeOwned(input: Record<string, unknown>, signal: AbortSignal): Promise<PrimitiveResponse> {
|
|
658
852
|
const state = await this.#loadState();
|
|
659
853
|
const workspace = state.handle.workspace;
|
|
660
|
-
const checks = this.#finalizeChecks ?? defaultFinalizeChecks(workspace);
|
|
854
|
+
const checks = this.#finalizeChecksForOwner(this.#finalizeChecks ?? defaultFinalizeChecks(workspace), signal);
|
|
661
855
|
const reviewOnly = state.handle.mode === "review";
|
|
662
856
|
const inputVerdict = reviewOnly ? (typeof input.verdict === "string" ? input.verdict : null) : undefined;
|
|
663
857
|
// Review-only finalize with no explicit verdict pulls the final assistant text from the live
|
|
@@ -666,6 +860,11 @@ export class RuntimeOwner {
|
|
|
666
860
|
if (reviewOnly && inputVerdict == null && this.#opts.transport.getLastAssistantText) {
|
|
667
861
|
assistantText = await this.#opts.transport.getLastAssistantText().catch(() => null);
|
|
668
862
|
}
|
|
863
|
+
if (signal.aborted) {
|
|
864
|
+
const latest = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
865
|
+
if (!latest) throw new Error(`session_not_found:${this.#opts.sessionId}`);
|
|
866
|
+
return this.#response(latest, { canceled: true }, false);
|
|
867
|
+
}
|
|
669
868
|
const fin = await runFinalize({
|
|
670
869
|
root: this.#opts.root,
|
|
671
870
|
sessionId: this.#opts.sessionId,
|
|
@@ -680,18 +879,28 @@ export class RuntimeOwner {
|
|
|
680
879
|
requirePr: input.requirePr !== false,
|
|
681
880
|
validationCommands: this.#validationCommands,
|
|
682
881
|
checks,
|
|
882
|
+
signal,
|
|
683
883
|
clock: this.#opts.clock,
|
|
684
884
|
});
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
885
|
+
if (signal.aborted) {
|
|
886
|
+
const latest = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
887
|
+
if (!latest) throw new Error(`session_not_found:${this.#opts.sessionId}`);
|
|
888
|
+
return this.#response(latest, { canceled: true, finalize: fin }, false);
|
|
889
|
+
}
|
|
890
|
+
const persisted = await this.#persistValidationLifecycle(
|
|
891
|
+
signal,
|
|
892
|
+
fin.completed ? "completed" : "blocked",
|
|
893
|
+
fin.completed ? undefined : fin.blockers,
|
|
894
|
+
);
|
|
895
|
+
if (!persisted.persisted) {
|
|
896
|
+
return this.#response(persisted.state, { canceled: signal.aborted, finalize: fin }, false);
|
|
897
|
+
}
|
|
689
898
|
await this.#emit(fin.completed ? "info" : "critical", "finalized", {
|
|
690
899
|
completed: fin.completed,
|
|
691
900
|
blockers: fin.blockers,
|
|
692
901
|
...(reviewOnly ? { verdict: fin.verdict ?? null, reviewOnly: true } : {}),
|
|
693
902
|
});
|
|
694
|
-
return this.#response(state, { finalize: fin }, fin.completed);
|
|
903
|
+
return this.#response(persisted.state, { finalize: fin }, fin.completed);
|
|
695
904
|
}
|
|
696
905
|
|
|
697
906
|
async #submit(input: Record<string, unknown>): Promise<PrimitiveResponse> {
|
|
@@ -775,13 +984,31 @@ export class RuntimeOwner {
|
|
|
775
984
|
return this.#response(state, { observation, ownerRouted: true }, true, submitGateReason);
|
|
776
985
|
}
|
|
777
986
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
987
|
+
#retire(): Promise<PrimitiveResponse> {
|
|
988
|
+
if (this.#retirePromise) return this.#retirePromise;
|
|
989
|
+
this.#retiring = true;
|
|
990
|
+
this.#retirePromise = this.#retireOnce();
|
|
991
|
+
void this.#retirePromise.then(
|
|
992
|
+
() => queueMicrotask(() => void this.stop().catch(() => {})),
|
|
993
|
+
() => {},
|
|
994
|
+
);
|
|
995
|
+
return this.#retirePromise;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async #retireOnce(): Promise<PrimitiveResponse> {
|
|
999
|
+
// Fence new mutating work first, then abort validation and join every request
|
|
1000
|
+
// that could still commit state or a receipt before recording retirement.
|
|
1001
|
+
await this.#stopActiveValidationWork();
|
|
1002
|
+
await this.#joinRetirementSensitiveWork();
|
|
1003
|
+
const state = await this.#withLifecycleMutation(async () => {
|
|
1004
|
+
const current = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
1005
|
+
if (!current) throw new Error(`session_not_found:${this.#opts.sessionId}`);
|
|
1006
|
+
current.lifecycle = "retired";
|
|
1007
|
+
current.updatedAt = new Date(this.#opts.clock ? this.#opts.clock() : Date.now()).toISOString();
|
|
1008
|
+
await writeSessionState(this.#opts.root, current);
|
|
1009
|
+
return current;
|
|
1010
|
+
});
|
|
783
1011
|
await this.#emit("info", "owner_retired", {});
|
|
784
|
-
queueMicrotask(() => void this.stop().catch(() => {}));
|
|
785
1012
|
return this.#response(state, { retired: true });
|
|
786
1013
|
}
|
|
787
1014
|
|
|
@@ -865,6 +1092,13 @@ export class RuntimeOwner {
|
|
|
865
1092
|
const reportFailure = await this.#reportStopFailure(kind, error);
|
|
866
1093
|
if (reportFailure !== null) failures.push(reportFailure);
|
|
867
1094
|
};
|
|
1095
|
+
// A retirement already in progress must finish its state commit before stop
|
|
1096
|
+
// tears down the endpoint or relinquishes the lease.
|
|
1097
|
+
if (this.#retirePromise) await this.#retirePromise;
|
|
1098
|
+
// Validation owns independent subprocess groups. Cancel and join those exact
|
|
1099
|
+
// requests before transport/endpoint teardown can surrender the owner lease.
|
|
1100
|
+
await this.#stopActiveValidationWork();
|
|
1101
|
+
await this.#joinRetirementSensitiveWork();
|
|
868
1102
|
const unsubscribe = this.#unsubscribeFrames;
|
|
869
1103
|
if (unsubscribe) {
|
|
870
1104
|
try {
|