@akagilnc/pi-workflow-roles 0.1.2383 → 0.1.2397

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.
@@ -4,8 +4,7 @@
4
4
  * Controlled failures and audit human decisions settle here without washing causes.
5
5
  */
6
6
  import { randomUUID } from "node:crypto";
7
- import { constants as fsConstants } from "node:fs";
8
- import { appendFile, lstat, mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
7
+ import { appendFile, lstat, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
9
8
  import { dirname, join } from "node:path";
10
9
 
11
10
  import {
@@ -32,7 +31,6 @@ import {
32
31
  AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE,
33
32
  COMPLIANCE_RESPONSE_ENTRY_TYPE,
34
33
  readComplianceCandidate,
35
- type ComplianceAuditIncomplete,
36
34
  type ComplianceDecision,
37
35
  } from "../compliance-transport.ts";
38
36
  import {
@@ -118,10 +116,8 @@ import {
118
116
  formatTerminalResult,
119
117
  isLawfulTypedTerminalOutcome,
120
118
  recommendationNavigatorFact,
121
- buildAuditIncompleteTerminalOutcome,
122
119
  buildResidualIncompleteTerminalOutcome,
123
120
  redactExactRunId,
124
- type AuditIncompleteResidual,
125
121
  type ControlledFailureCause,
126
122
  type TerminalArtifactRef,
127
123
  type TerminalMenxiaFact,
@@ -728,10 +724,16 @@ export async function readBoundEvidenceChildKnownFailure(
728
724
  return undefined;
729
725
  }
730
726
 
731
- /** Recover a provider stop from the auditor child bound to the current parent attempt. */
732
- export async function readBoundAuditorKnownFailure(
727
+ type BoundAuditorVolume = {
728
+ readonly entries: SessionEntry[];
729
+ readonly attemptEntryId?: string;
730
+ readonly parentId: string;
731
+ readonly sessionFile: string;
732
+ };
733
+
734
+ async function loadBoundAuditorVolumes(
733
735
  sessionFile: string,
734
- ): Promise<ExplicitInternalKnownFailure | undefined> {
736
+ ): Promise<readonly BoundAuditorVolume[] | undefined> {
735
737
  let parentEntries: SessionEntry[];
736
738
  try {
737
739
  parentEntries = await readBoundSessionEntries(sessionFile);
@@ -773,7 +775,7 @@ export async function readBoundAuditorKnownFailure(
773
775
  // first attempt's child after resume advanced latest, losing retentionFailure
774
776
  // when retry had no compliance entry. Fix: ignore envelope for staleness and
775
777
  // prefer any valid compliance failure before falling back to primary.
776
- const validAuditorFiles: Array<{ file: string; entries: SessionEntry[]; attemptEntryId?: string }> = [];
778
+ const valid: BoundAuditorVolume[] = [];
777
779
  for (const file of names.filter((name) => name.endsWith(".jsonl")).sort().reverse()) {
778
780
  let entries: SessionEntry[];
779
781
  try {
@@ -788,10 +790,20 @@ export async function readBoundAuditorKnownFailure(
788
790
  const attemptEntryId = typeof bindingParent?.attemptEntryId === "string" ? bindingParent.attemptEntryId : undefined;
789
791
  const attemptEntryIndex = attemptEntryId === undefined ? -1 : parentEntries.findIndex((entry) => entry.id === attemptEntryId);
790
792
  if (bindingParent?.sessionId !== parentId || bindingParent.sessionFile !== sessionFile || attemptEntryIndex < latestParentUserIndex) continue;
791
- validAuditorFiles.push({ file, entries, ...(attemptEntryId === undefined ? {} : { attemptEntryId }) });
793
+ valid.push({
794
+ entries,
795
+ parentId,
796
+ sessionFile,
797
+ ...(attemptEntryId === undefined ? {} : { attemptEntryId }),
798
+ });
792
799
  }
793
- // Prefer compliance failure (retention) from any valid attempt, newest first.
794
- for (const { entries, attemptEntryId } of validAuditorFiles) {
800
+ return valid;
801
+ }
802
+
803
+ function complianceFailureFromAuditorVolumes(
804
+ volumes: readonly BoundAuditorVolume[],
805
+ ): ExplicitInternalKnownFailure | undefined {
806
+ for (const { entries, attemptEntryId, parentId, sessionFile } of volumes) {
795
807
  const stop = extractSessionProviderStop(entries);
796
808
  if (stop === undefined) continue;
797
809
  for (let i = entries.length - 1; i >= 0; i -= 1) {
@@ -812,8 +824,13 @@ export async function readBoundAuditorKnownFailure(
812
824
  };
813
825
  }
814
826
  }
815
- // No compliance failure: fall back to most recent provider stop (auto-resume latest attempt).
816
- for (const { entries } of validAuditorFiles) {
827
+ return undefined;
828
+ }
829
+
830
+ function providerStopFallbackFromAuditorVolumes(
831
+ volumes: readonly BoundAuditorVolume[],
832
+ ): ExplicitInternalKnownFailure | undefined {
833
+ for (const { entries } of volumes) {
817
834
  const stop = extractSessionProviderStop(entries);
818
835
  if (stop === undefined) continue;
819
836
  const primary = knownFailureFromProviderStop(stop)!;
@@ -828,6 +845,34 @@ export async function readBoundAuditorKnownFailure(
828
845
  return undefined;
829
846
  }
830
847
 
848
+ /** Recover a provider stop from the auditor child bound to the current parent attempt. */
849
+ export async function readBoundAuditorKnownFailure(
850
+ sessionFile: string,
851
+ ): Promise<ExplicitInternalKnownFailure | undefined> {
852
+ const volumes = await loadBoundAuditorVolumes(sessionFile);
853
+ if (volumes === undefined) return undefined;
854
+ return complianceFailureFromAuditorVolumes(volumes)
855
+ ?? providerStopFallbackFromAuditorVolumes(volumes);
856
+ }
857
+
858
+ /** Strong auditor tier only — retained compliance-failure entries, no provider-stop fallback. */
859
+ async function readBoundAuditorComplianceFailure(
860
+ sessionFile: string,
861
+ ): Promise<ExplicitInternalKnownFailure | undefined> {
862
+ const volumes = await loadBoundAuditorVolumes(sessionFile);
863
+ if (volumes === undefined) return undefined;
864
+ return complianceFailureFromAuditorVolumes(volumes);
865
+ }
866
+
867
+ /** Weaker auditor tier: provider stop without a retained compliance-failure entry. */
868
+ async function readBoundAuditorProviderStopFallback(
869
+ sessionFile: string,
870
+ ): Promise<ExplicitInternalKnownFailure | undefined> {
871
+ const volumes = await loadBoundAuditorVolumes(sessionFile);
872
+ if (volumes === undefined) return undefined;
873
+ return providerStopFallbackFromAuditorVolumes(volumes);
874
+ }
875
+
831
876
  function typedFailedTerminatingToolKnownFailure(
832
877
  entries: readonly SessionEntry[],
833
878
  ): ExplicitInternalKnownFailure | undefined {
@@ -850,11 +895,14 @@ function typedFailedTerminatingToolKnownFailure(
850
895
  ? message.content.find((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
851
896
  : undefined;
852
897
  const diagnostic = isRecord(textPart) ? textPart.text : undefined;
898
+ // Durable details already carry fact + typed evidence from envelope one-shot projection (#475).
899
+ // Do not re-parse retained compliance responses here.
900
+ const details = isRecord(message.details) ? message.details : classification.fact;
853
901
  return {
854
902
  cause: "output",
855
903
  identity: { name: message.toolName, code: message.toolCallId },
856
904
  ...(typeof diagnostic === "string" && diagnostic.trim() !== "" ? { diagnostic } : {}),
857
- details: classification.fact,
905
+ details,
858
906
  };
859
907
  }
860
908
  return undefined;
@@ -913,12 +961,13 @@ export async function resolveAuditedRunnerFailureResolution(input: {
913
961
  });
914
962
  }
915
963
  }
916
- // Bound auditor evidence outranks a parent failure that the auditor path itself
917
- // caused (retention EISDIR race). A typed terminating-tool host failure is next:
918
- // it outranks provider/credential and nonzero fallbacks, but not its recorded cause.
964
+ // Bound auditor compliance-failure retention outranks a parent failure that the
965
+ // auditor path itself caused (retention EISDIR race). A typed terminating-tool
966
+ // host failure is next — it outranks weaker auditor provider-stop fallback so
967
+ // parent failInfrastructure abort pollution cannot wash a real diagnostic (#475).
919
968
  try {
920
- const auditorFailure = await readBoundAuditorKnownFailure(input.sessionFile);
921
- if (auditorFailure !== undefined) return resolutionOf(auditorFailure);
969
+ const auditorCompliance = await readBoundAuditorComplianceFailure(input.sessionFile);
970
+ if (auditorCompliance !== undefined) return resolutionOf(auditorCompliance);
922
971
  } catch (error) {
923
972
  const failure = sessionReadFailure(error, "failed to recover bound auditor failure");
924
973
  return resolutionOf({
@@ -942,6 +991,17 @@ export async function resolveAuditedRunnerFailureResolution(input: {
942
991
  });
943
992
  }
944
993
  }
994
+ try {
995
+ const auditorStop = await readBoundAuditorProviderStopFallback(input.sessionFile);
996
+ if (auditorStop !== undefined) return resolutionOf(auditorStop);
997
+ } catch (error) {
998
+ const failure = sessionReadFailure(error, "failed to recover bound auditor provider stop");
999
+ return resolutionOf({
1000
+ cause: "session",
1001
+ identity: thrownIdentity(failure),
1002
+ diagnostic: failure.message || failure.name,
1003
+ });
1004
+ }
945
1005
  // Reviewer axis evidence-children are next: fixed two-axis dispatch fails
946
1006
  // during activation with only child stops durable. Parent stop remains the
947
1007
  // fallback; credential is last.
@@ -1550,35 +1610,6 @@ export function assertCollectorReceiptMatchesAdmitted(
1550
1610
  }
1551
1611
  }
1552
1612
 
1553
- /**
1554
- * Shared audit-incomplete extraction for the four roles with Soul auditors.
1555
- * The role submission and retained auditor response are separate evidence faces;
1556
- * neither is converted into an accepted Receipt.
1557
- */
1558
- function isComplianceAuditIncomplete(value: unknown): value is ComplianceAuditIncomplete {
1559
- if (!isRecord(value) || value.status !== "audit-incomplete") return false;
1560
- const observation = value.observation;
1561
- if (!isRecord(observation)) return false;
1562
- if (observation.kind === "missing-dossier") return true;
1563
- if (observation.kind === "missing-subject") {
1564
- return typeof observation.subject === "string" && observation.subject.length > 0;
1565
- }
1566
- if (observation.kind === "object-status-unreadable") {
1567
- return observation.status === "missing" || observation.status === "unknown";
1568
- }
1569
- return observation.kind === "non-object-arguments" && [
1570
- "null",
1571
- "array",
1572
- "undefined",
1573
- "string",
1574
- "number",
1575
- "boolean",
1576
- "bigint",
1577
- "symbol",
1578
- "function",
1579
- ].includes(observation.type as string);
1580
- }
1581
-
1582
1613
  function auditToolNameForRole(
1583
1614
  role: (typeof AUDITOR_SOUL_ROLES)[number],
1584
1615
  ): string {
@@ -1592,19 +1623,6 @@ function auditToolNameForRole(
1592
1623
  }
1593
1624
  }
1594
1625
 
1595
- function outputToolNameForAuditedRole(
1596
- role: (typeof AUDITOR_SOUL_ROLES)[number],
1597
- ): string {
1598
- switch (role) {
1599
- case "judge":
1600
- return JUDGE_OUTPUT_TOOL_NAME;
1601
- case "reviewer":
1602
- return REVIEWER_OUTPUT_TOOL_NAME;
1603
- case "doctor":
1604
- return DOCTOR_OUTPUT_TOOL_NAME;
1605
- }
1606
- }
1607
-
1608
1626
  type BoundRoleToolCall = {
1609
1627
  callIndex: number;
1610
1628
  candidate: unknown;
@@ -1757,13 +1775,6 @@ function isUnboundAuditEscalationFace(details: unknown): boolean {
1757
1775
  return kind.readable && kind.value === "audit_escalation";
1758
1776
  }
1759
1777
 
1760
- function auditIncompleteFromCandidate(
1761
- candidate: unknown,
1762
- ): ComplianceAuditIncomplete | undefined {
1763
- const decision = readComplianceCandidate(candidate);
1764
- return decision.status === "audit-incomplete" ? decision : undefined;
1765
- }
1766
-
1767
1778
  function boundRetainedAuditResponse(
1768
1779
  entries: readonly SessionEntry[],
1769
1780
  callIndex: number,
@@ -1791,113 +1802,6 @@ function boundRetainedAuditResponse(
1791
1802
  return matches.length === 1 ? matches[0] : undefined;
1792
1803
  }
1793
1804
 
1794
- export function extractComplianceAuditIncompleteRoleOutcome(
1795
- entries: readonly SessionEntry[],
1796
- role: (typeof AUDITOR_SOUL_ROLES)[number],
1797
- outputToolName: string,
1798
- ): { outcome: ReturnType<typeof buildAuditIncompleteTerminalOutcome> } | undefined {
1799
- if (outputToolName !== outputToolNameForAuditedRole(role)) return undefined;
1800
- const auditToolName = auditToolNameForRole(role);
1801
- for (let index = entries.length - 1; index >= 0; index -= 1) {
1802
- const message = entries[index]?.message;
1803
- if (
1804
- entries[index]?.type !== "message" ||
1805
- message?.role !== "toolResult" ||
1806
- message.toolName !== outputToolName ||
1807
- message.isError === true ||
1808
- !isComplianceAuditIncomplete(message.details)
1809
- ) {
1810
- continue;
1811
- }
1812
- const roleCall = boundRoleToolCallForResult(
1813
- entries,
1814
- index,
1815
- message,
1816
- outputToolName,
1817
- );
1818
- if (roleCall === undefined) continue;
1819
- // Preflight missing-dossier / missing-subject never contacts the provider, so
1820
- // there is no retained auditor response — the role tool details are the audit.
1821
- // details already narrowed by isComplianceAuditIncomplete at the loop gate.
1822
- const details = message.details;
1823
- if (
1824
- details.observation.kind === "missing-dossier"
1825
- || details.observation.kind === "missing-subject"
1826
- ) {
1827
- return {
1828
- outcome: buildAuditIncompleteTerminalOutcome({
1829
- role,
1830
- roleCandidate: roleCall.candidate,
1831
- audit: details,
1832
- }),
1833
- };
1834
- }
1835
- const retained = boundRetainedAuditResponse(
1836
- entries,
1837
- roleCall.callIndex,
1838
- index,
1839
- auditToolName,
1840
- );
1841
- if (retained === undefined) continue;
1842
- const audit = auditIncompleteFromCandidate(retained.candidate);
1843
- if (audit === undefined) continue;
1844
- return {
1845
- outcome: buildAuditIncompleteTerminalOutcome({
1846
- role,
1847
- roleCandidate: roleCall.candidate,
1848
- audit,
1849
- }),
1850
- };
1851
- }
1852
- return undefined;
1853
- }
1854
-
1855
- function auditArtifactPublicationError(message: string, code: string): Error & {
1856
- code: string;
1857
- } {
1858
- const error = new Error(message) as Error & { code: string };
1859
- error.name = "ArtifactPublicationError";
1860
- error.code = code;
1861
- return error;
1862
- }
1863
-
1864
- async function ensureAuditEvidenceDirectory(runDirectory: string): Promise<string> {
1865
- const artifactsDir = join(runDirectory, "artifacts");
1866
- const runStat = await lstat(runDirectory);
1867
- if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
1868
- throw auditArtifactPublicationError(
1869
- "audit evidence run directory is not a real directory",
1870
- "ELOOP",
1871
- );
1872
- }
1873
- try {
1874
- const existing = await lstat(artifactsDir);
1875
- if (existing.isSymbolicLink()) {
1876
- throw auditArtifactPublicationError(
1877
- "audit evidence artifacts directory is a symlink",
1878
- "ELOOP",
1879
- );
1880
- }
1881
- if (!existing.isDirectory()) {
1882
- throw auditArtifactPublicationError(
1883
- "audit evidence artifacts path is not a directory",
1884
- "EEXIST",
1885
- );
1886
- }
1887
- } catch (error) {
1888
- if (!isMissingPathError(error)) throw error;
1889
- await mkdir(artifactsDir, { recursive: true });
1890
- const created = await lstat(artifactsDir);
1891
- if (created.isSymbolicLink() || !created.isDirectory()) {
1892
- throw auditArtifactPublicationError(
1893
- "audit evidence artifacts directory is not a real directory",
1894
- "ELOOP",
1895
- );
1896
- }
1897
- }
1898
- return artifactsDir;
1899
- }
1900
-
1901
1805
  /**
1902
1806
  * #419 per-attempt process history. 史必追加,指针可覆盖;指针可以覆盖的前提是史已落。
1903
1807
  * Reuses the run session principal's append-only JSONL custom-entry shape
@@ -1957,167 +1861,6 @@ export async function appendRunAttemptHistory(
1957
1861
  await appendFile(admitted.sessionFile, line, "utf8");
1958
1862
  }
1959
1863
 
1960
- /**
1961
- * Publish the retained residual with complete-write semantics (#419: the
1962
- * previous attempt's pointer file is a rebuildable view once this attempt's
1963
- * complete result is in the appended history; planted symlinks/directories
1964
- * still fail loudly with their #182-A identities).
1965
- */
1966
- export async function publishComplianceAuditIncompleteEvidence(
1967
- admitted: AdmittedRoleInvocation,
1968
- outcome: ReturnType<typeof buildAuditIncompleteTerminalOutcome>,
1969
- ): Promise<TerminalArtifactRef> {
1970
- await appendRunAttemptHistory(admitted, outcome);
1971
- if (
1972
- typeof fsConstants.O_NOFOLLOW !== "number" ||
1973
- typeof fsConstants.O_NONBLOCK !== "number"
1974
- ) {
1975
- // #418 fail-closed: on platforms without O_NOFOLLOW (e.g. Windows,
1976
- // nodejs/node#41590) the JS bitwise-or below would silently drop the flag
1977
- // and the #182-A anti-symlink protection would vanish. Refuse loudly via
1978
- // the existing publication-failure channel instead of publishing
1979
- // unprotected; the complete attempt result above stays in appended history.
1980
- throw auditArtifactPublicationError(
1981
- "audit evidence publication requires O_NOFOLLOW|O_NONBLOCK open-flag support (anti-symlink/anti-planted protection must not be silently dropped); refusing to publish",
1982
- "ENOSYS",
1983
- );
1984
- }
1985
- const artifactsDir = await ensureAuditEvidenceDirectory(admitted.runDirectory);
1986
- const evidencePath = join(artifactsDir, "audit-incomplete.json");
1987
- let existing: Awaited<ReturnType<typeof lstat>> | undefined;
1988
- try {
1989
- existing = await lstat(evidencePath);
1990
- } catch (error) {
1991
- if (!isMissingPathError(error)) throw error;
1992
- }
1993
- if (existing?.isSymbolicLink()) {
1994
- throw auditArtifactPublicationError(
1995
- "audit evidence destination is a symlink",
1996
- "ELOOP",
1997
- );
1998
- }
1999
- if (existing && !existing.isFile()) {
2000
- throw auditArtifactPublicationError(
2001
- "audit evidence destination is not a regular file",
2002
- "EEXIST",
2003
- );
2004
- }
2005
- // #182-A protection restored atomically at this open seam: O_NOFOLLOW makes
2006
- // the open itself refuse symlinks (ELOOP), so one swapped in after the lstat
2007
- // above can never be followed or written through; O_NONBLOCK keeps a
2008
- // race-planted FIFO from blocking this open forever. The fstat below
2009
- // backstops any non-regular object that wins the window.
2010
- const handle = await open(
2011
- evidencePath,
2012
- fsConstants.O_WRONLY |
2013
- fsConstants.O_CREAT |
2014
- fsConstants.O_TRUNC |
2015
- fsConstants.O_NOFOLLOW |
2016
- fsConstants.O_NONBLOCK,
2017
- 0o600,
2018
- );
2019
- try {
2020
- if (!(await handle.stat()).isFile()) {
2021
- throw auditArtifactPublicationError(
2022
- "audit evidence destination is not a regular file",
2023
- "EEXIST",
2024
- );
2025
- }
2026
- await handle.writeFile(`${JSON.stringify(outcome, null, 2)}\n`, "utf8");
2027
- await handle.sync();
2028
- } finally {
2029
- await handle.close();
2030
- }
2031
- return { kind: "evidence", path: evidencePath };
2032
- }
2033
-
2034
- function auditPublicationFailureTerminal(
2035
- admitted: AdmittedRoleInvocation,
2036
- entries: readonly SessionEntry[],
2037
- outcome: ReturnType<typeof buildAuditIncompleteTerminalOutcome>,
2038
- error: unknown,
2039
- ): TerminalResult {
2040
- const attempt = publicationAttemptFromError(
2041
- join(admitted.runDirectory, "artifacts", "audit-incomplete.json"),
2042
- error,
2043
- );
2044
- const diagnostic = `audit-incomplete evidence publication failed: ${attempt.diagnostic}`;
2045
- const decisiveFacts: Record<string, unknown> = {
2046
- ...outcome.decisiveFacts,
2047
- cause: "unrecognized",
2048
- diagnostic,
2049
- publicationFailure: attempt,
2050
- };
2051
- if (attempt.identity?.name !== undefined) decisiveFacts.errorName = attempt.identity.name;
2052
- if (attempt.identity?.code !== undefined) decisiveFacts.errorCode = attempt.identity.code;
2053
- const auditResidual: AuditIncompleteResidual = {
2054
- roleCandidate: outcome.roleCandidate,
2055
- audit: outcome.audit,
2056
- acceptedReceipt: false,
2057
- };
2058
- return {
2059
- roleOutcome: {
2060
- kind: "failure",
2061
- role: admitted.role,
2062
- cause: "unrecognized",
2063
- diagnostic,
2064
- decisiveFacts,
2065
- auditResidual,
2066
- },
2067
- navigator: extractNavigatorFact(entries),
2068
- artifacts: [],
2069
- runId: admitted.runId,
2070
- };
2071
- }
2072
-
2073
- /**
2074
- * Settle the shared audit-incomplete Terminal for Judge/Fixer/Reviewer/Doctor.
2075
- * Callers invoke this only after their ordinary lawful extractor found no result,
2076
- * which preserves the no-other-lawful-result invariant without a second validator.
2077
- */
2078
- export async function trySettleComplianceAuditIncompleteTerminalResult(
2079
- admitted: AdmittedRoleInvocation,
2080
- ): Promise<TerminalResult | undefined> {
2081
- if (!(AUDITOR_SOUL_ROLES as readonly string[]).includes(admitted.role)) {
2082
- return undefined;
2083
- }
2084
- const outputToolName =
2085
- admitted.role === "judge"
2086
- ? JUDGE_OUTPUT_TOOL_NAME
2087
- : admitted.role === "reviewer"
2088
- ? REVIEWER_OUTPUT_TOOL_NAME
2089
- : DOCTOR_OUTPUT_TOOL_NAME;
2090
- const entries = await readLawfulSettlementEntries(admitted);
2091
- if (entries === undefined) return undefined;
2092
- const extracted = extractComplianceAuditIncompleteRoleOutcome(
2093
- entries,
2094
- admitted.role as (typeof AUDITOR_SOUL_ROLES)[number],
2095
- outputToolName,
2096
- );
2097
- if (extracted === undefined) return undefined;
2098
- // Catch covers publication only — menxia read errors keep their session/JSONL identity
2099
- // and must not be washed into the publication-failure label.
2100
- let evidence: TerminalArtifactRef;
2101
- try {
2102
- evidence = await publishComplianceAuditIncompleteEvidence(
2103
- admitted,
2104
- extracted.outcome,
2105
- );
2106
- } catch (error) {
2107
- // Publication failure is a non-lawful terminal, never an accepted audit result.
2108
- return auditPublicationFailureTerminal(admitted, entries, extracted.outcome, error);
2109
- }
2110
- return withOptionalMenxiaProjection(
2111
- {
2112
- roleOutcome: extracted.outcome,
2113
- navigator: extractNavigatorFact(entries),
2114
- artifacts: [evidence],
2115
- runId: admitted.runId,
2116
- },
2117
- admitted.sessionDirectory,
2118
- );
2119
- }
2120
-
2121
1864
  /** Lawful Judge outcomes extracted from session (never a fabricated failure Receipt). */
2122
1865
  export type LawfulJudgeRoleOutcome = Extract<
2123
1866
  TerminalRoleOutcome,
@@ -2327,6 +2070,21 @@ async function withOptionalMenxiaProjection<
2327
2070
  artifacts: readonly TerminalArtifactRef[];
2328
2071
  },
2329
2072
  >(base: T, sessionDirectory: string): Promise<T & { menxia?: TerminalMenxiaFact }> {
2073
+ // A gate transport failure is already represented by typed evidence and has no
2074
+ // accepted gate cycle to project. Re-reading that rejected receipt as an
2075
+ // accepted cycle would replace the original failure with a projection error.
2076
+ const secondaryEvidence = base.roleOutcome.kind === "failure"
2077
+ ? base.roleOutcome.decisiveFacts.secondaryEvidence
2078
+ : undefined;
2079
+ if (
2080
+ isRecord(secondaryEvidence)
2081
+ && secondaryEvidence.kind === "role_infrastructure_failure"
2082
+ && (
2083
+ secondaryEvidence.stage === "gatekeeper"
2084
+ || secondaryEvidence.stage === "inspector"
2085
+ || secondaryEvidence.stage === "notary"
2086
+ )
2087
+ ) return base;
2330
2088
  const menxia = await extractMenxiaFactFromSessionDirectory(sessionDirectory);
2331
2089
  return menxia === undefined ? base : { ...base, menxia };
2332
2090
  }
@@ -3314,7 +3072,7 @@ export async function trySettleDoctorTerminalResult(
3314
3072
  return settleLawfulDoctorTerminalResult(admitted);
3315
3073
  }
3316
3074
 
3317
- /** Lawful Notary accepted outcome (pass/bounce/incomplete-with-reason). */
3075
+ /** Lawful Notary accepted outcome (pass/bounce). */
3318
3076
  export type LawfulNotaryRoleOutcome = {
3319
3077
  kind: "accepted";
3320
3078
  role: "notary";
@@ -3356,6 +3114,7 @@ async function settleLawfulNotaryTerminalResult(
3356
3114
  if (entries === undefined) return undefined;
3357
3115
  const extracted = extractNotaryRoleOutcome(entries);
3358
3116
  if (extracted === undefined) {
3117
+ // No usable Notary release → existing non-zero failure channel with candidate (#475).
3359
3118
  for (let index = entries.length - 1; index >= 0; index -= 1) {
3360
3119
  const message = entries[index]?.message;
3361
3120
  if (message?.role !== "toolResult") continue;
@@ -3366,16 +3125,11 @@ async function settleLawfulNotaryTerminalResult(
3366
3125
  NOTARY_OUTPUT_TOOL_NAME,
3367
3126
  );
3368
3127
  if (residual === undefined) continue;
3369
- return {
3370
- roleOutcome: buildResidualIncompleteTerminalOutcome({
3371
- role: "notary",
3372
- candidate: residual.candidate,
3373
- diagnostic: residual.diagnostic,
3374
- }),
3375
- navigator: extractNavigatorFact(entries),
3376
- artifacts: [],
3377
- runId: admitted.runId,
3378
- };
3128
+ return settleFailureTerminalResult(admitted, {
3129
+ cause: "output",
3130
+ diagnostic: residual.diagnostic,
3131
+ details: { candidate: residual.candidate, acceptedReceipt: false },
3132
+ });
3379
3133
  }
3380
3134
  return undefined;
3381
3135
  }