@akagilnc/pi-workflow-roles 0.1.4620 → 0.1.4641

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.
Files changed (41) hide show
  1. package/README.zh-CN.md +1 -1
  2. package/dist/acp-host/production-host.js +962 -547
  3. package/dist/activation-ledger-topology.js +0 -4
  4. package/dist/gatekeeper-role.js +76 -6
  5. package/dist/headless-host/production-host.js +923 -508
  6. package/dist/migrate-book-topology.js +9 -0
  7. package/dist/pi/role-turn-host.js +0 -3
  8. package/dist/public-cli/auto-resume.js +26 -0
  9. package/dist/public-cli/countersign-run.js +31 -10
  10. package/dist/public-cli/main.js +1456 -1068
  11. package/dist/public-cli/post-admission.js +101 -7
  12. package/dist/public-cli/process-cancel.js +57 -0
  13. package/dist/public-cli/settlement.js +95 -2
  14. package/dist/public-role-summons.js +19 -0
  15. package/dist/secretariat-contracts.js +4 -0
  16. package/package.json +1 -1
  17. package/scripts/build-package.mjs +0 -1
  18. package/src/activation-ledger-topology.ts +1 -6
  19. package/src/activation-ledger.ts +5 -236
  20. package/src/analyst-entry.ts +19 -2
  21. package/src/analyst-ledger.ts +111 -6
  22. package/src/archivist-record-entry.ts +3 -3
  23. package/src/audit-escalation.ts +1 -1
  24. package/src/gatekeeper-pass-envelope.ts +37 -5
  25. package/src/gatekeeper-role.ts +125 -20
  26. package/src/host-contracts.ts +22 -2
  27. package/src/pi/adapter.ts +5 -1
  28. package/src/pi/role-turn-host.ts +0 -3
  29. package/src/public-cli/auto-resume.ts +34 -0
  30. package/src/public-cli/cli.ts +30 -0
  31. package/src/public-cli/countersign-run.ts +50 -13
  32. package/src/public-cli/main.ts +25 -3
  33. package/src/public-cli/post-admission.ts +173 -6
  34. package/src/public-cli/process-cancel.ts +75 -0
  35. package/src/public-cli/settlement.ts +117 -1
  36. package/src/public-role-summons.ts +30 -4
  37. package/src/role-envelope.ts +27 -10
  38. package/src/role-runtime.ts +146 -55
  39. package/src/secretariat-contracts.ts +18 -0
  40. package/dist/activation-reconciliation.js +0 -61
  41. package/src/activation-reconciliation.ts +0 -163
@@ -17,20 +17,10 @@ import { createSubmissionLedgerHost, sealAcceptedSubmission } from "./submission
17
17
  import { createCollectorLedger } from "./collector-ledger.ts";
18
18
 
19
19
  import { activationTraceRecordSchema, namedActivationCause, type ActivationTraceRecord, type ActivationTraceWriter } from "./activation-trace.ts";
20
+ import { homeFromRunDirectory } from "./activation-ledger-topology.ts";
20
21
  import {
21
- homeFromRunDirectory,
22
- resolveActivationLedgerHomeForPath,
23
- } from "./activation-ledger-topology.ts";
24
- import {
25
- appendAcceptedActivationToBook,
26
- buildAcceptedActivationFact,
27
- correlationIdentityFromEnv,
28
22
  durableSessionPointer,
29
- resolveActivationLedgerHome,
30
23
  resolveBookKeyFromGit,
31
- type AcceptedActivationFact,
32
- type ActivationCorrelationIdentity,
33
- type ActivationSessionPointer,
34
24
  } from "./activation-ledger.ts";
35
25
  import { writeStderrJsonlRecord } from "./stderr-jsonl.ts";
36
26
  import {
@@ -301,43 +291,18 @@ export {
301
291
  export { activationTraceRecordSchema, namedActivationCause } from "./activation-trace.ts";
302
292
  export type { ActivationTraceRecord, ActivationTraceWriter } from "./activation-trace.ts";
303
293
  export {
304
- ACCEPTED_ACTIVATION_EVENT,
305
- ACCEPTED_ACTIVATION_FACT_KEYS,
306
294
  ActivationGitRepositoryRequiredError,
307
295
  ActivationLedgerError,
308
296
  ActivationSessionFileMissingError,
309
297
  activationBookDirectory,
310
- activationWaitingLedgerPath,
311
- appendAcceptedActivationFact,
312
- appendAcceptedActivationToBook,
313
- buildAcceptedActivationFact,
314
- correlationIdentityFromEnv,
315
298
  durableSessionPointer,
316
299
  resolveActivationLedgerHome,
317
300
  resolveBookKeyFromGit,
318
- serializeAcceptedActivationFact,
319
301
  } from "./activation-ledger.ts";
320
302
  export type {
321
- AcceptedActivationFact,
322
- AcceptedActivationFactInput,
323
- AcceptedActivationFactKey,
324
- ActivationCorrelationIdentity,
325
303
  ActivationSessionManager,
326
304
  ActivationSessionPointer,
327
305
  } from "./activation-ledger.ts";
328
- export {
329
- DISPATCH_STUB_EVENT,
330
- buildDispatchStubFact,
331
- reconcileInvocation,
332
- } from "./activation-reconciliation.ts";
333
- export type {
334
- DispatchPointer,
335
- DispatchStubFact,
336
- DispatchStubFactInput,
337
- ProcessLivenessFact,
338
- ReconciliationOutcome,
339
- ReconciliationSubject,
340
- } from "./activation-reconciliation.ts";
341
306
  export {
342
307
  TOOL_EXECUTION_UPDATE_HEARTBEAT,
343
308
  TOOL_EXECUTION_UPDATE_THROTTLE_MS,
@@ -349,15 +314,23 @@ export {
349
314
  writeToolExecutionObservationRecord,
350
315
  } from "./tool-execution-observation.ts";
351
316
  export type { ToolExecutionObservationRecord, ToolExecutionObservationWriter } from "./tool-execution-observation.ts";
352
- export {
317
+ import {
353
318
  NOTARY_OUTPUT_TOOL,
354
319
  INSPECTOR_OUTPUT_TOOL,
355
320
  GatekeeperDecisionError,
356
321
  createGatekeeperOutputTool,
357
322
  runGatekeeper,
323
+ gateOfficerForSubject,
358
324
  } from "./gatekeeper-role.ts";
325
+ export {
326
+ NOTARY_OUTPUT_TOOL,
327
+ INSPECTOR_OUTPUT_TOOL,
328
+ GatekeeperDecisionError,
329
+ createGatekeeperOutputTool,
330
+ runGatekeeper,
331
+ gateOfficerForSubject,
332
+ };
359
333
  export type { GatekeeperResult, GatekeeperSubject, GatekeeperNonPassResult, GateOfficer, RunGatekeeperOptions } from "./gatekeeper-role.ts";
360
- export { gateOfficerForSubject } from "./gatekeeper-role.ts";
361
334
  import { ParentQueueReaskError } from "./submission-errors.ts";
362
335
 
363
336
  export {
@@ -581,6 +554,13 @@ type NavigatorAttendanceDependency = Omit<NavigatorAttendance, "knownRoutePlaybo
581
554
  export type RoleRuntimeDependencies = {
582
555
  /** Package root for packaged engine-note resolution (#879). */
583
556
  packageRoot?: string;
557
+ /**
558
+ * Composition-root adapter table for nested gate officer summons (#969).
559
+ * Production leaves unset (default pi + packaged externals). Tests inject
560
+ * faux nested hosts so prepareRoleEnvelope.requireGatekeeperPass is bitten
561
+ * without reimplementing the envelope summon closure.
562
+ */
563
+ hostAdapters?: readonly import("./public-cli/role-turn-host-resolution.ts").NamedRoleTurnHostAdapter[];
584
564
  /** Non-identity opening materials delivered in the ordinary role brief. */
585
565
  loadRoleReferenceMaterials?(role: PackagedRole): Promise<string>;
586
566
  loadJudgeSoul(): Promise<string>;
@@ -1034,18 +1014,137 @@ const COUNTERSIGN_QUEUE_STATUSES = new Set(["converged", "continue", "escalate"]
1034
1014
  const COUNTERSIGN_STATUS_REASK =
1035
1015
  "countersignStatus 不是 converged、continue、escalate 三态之一。请重新交卷,status 写明其一。" as const;
1036
1016
 
1017
+ /**
1018
+ * #969 authorized non-pi hosts: Secretariat converged is a candidate ticket —
1019
+ * AK-side submission gate summons 给事中. pi keeps mid-turn summon tool only.
1020
+ */
1021
+ const SECRETARIAT_SUBMISSION_GATE_HOSTS = new Set(["codex", "claude", "grok-build"]);
1022
+
1023
+ /** Secretariat status words the non-pi submission gate reads (#969). */
1024
+ const SECRETARIAT_QUEUE_STATUSES = new Set(["converged", "escalate"]);
1025
+
1026
+ /**
1027
+ * Plain-language re-ask when non-pi secretariatStatus is not a known queue word.
1028
+ * Back to secretariat itself — 给事中 is not summoned (#969 / ADR 0055).
1029
+ */
1030
+ const SECRETARIAT_STATUS_REASK =
1031
+ "secretariatStatus 不是 converged、escalate 之一。请重新交卷,status 写明其一。" as const;
1032
+
1037
1033
  /**
1038
1034
  * #924 Secretariat on the shared filed-officer envelope + non-terminating
1039
1035
  * summon-countersign tool (auditor dossier extension pattern).
1040
1036
  * Nested countersign lifecycle stays on summonPublicRole (ADR 0018).
1041
- * #924 / 第 0 条: output tool records the receipt as submitted — no status
1037
+ * #924 / 第 0 条 (pi): output tool records the receipt as submitted — no status
1042
1038
  * shape/value reject or reask; legality is content-layer / downstream.
1039
+ * #969 (codex/claude/grok-build): converged arms 既有交卷闸 → 给事中; escalate
1040
+ * skips the gate; other values reask secretariat (ADR 0055).
1043
1041
  */
1044
1042
  export function createSecretariatRoleRuntime(
1045
1043
  roleHost: RoleHost,
1046
1044
  dependencies: SecretariatRuntimeDependencies,
1045
+ hostActions?: import("./host-contracts.ts").HostGatekeeperActions,
1047
1046
  ) {
1048
1047
  let parentInstruction = "";
1048
+ // #969: non-pi submission gate only — pi mid-turn summon path stays untouched.
1049
+ const beforeAccept: FiledOfficerBeforeAccept | undefined =
1050
+ hostActions !== undefined && roleHost.requireGatekeeperPass !== undefined
1051
+ ? async ({ toolCallId, parameters, signal, ctx }) => {
1052
+ const host =
1053
+ typeof ctx.host === "string" && ctx.host.trim() !== ""
1054
+ ? ctx.host.trim()
1055
+ : undefined;
1056
+ if (host === undefined || !SECRETARIAT_SUBMISSION_GATE_HOSTS.has(host)) {
1057
+ return undefined;
1058
+ }
1059
+ const record =
1060
+ parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
1061
+ ? (parameters as Record<string, unknown>)
1062
+ : undefined;
1063
+ const status =
1064
+ record !== undefined && typeof record.secretariatStatus === "string"
1065
+ ? record.secretariatStatus
1066
+ : undefined;
1067
+ if (status === undefined || !SECRETARIAT_QUEUE_STATUSES.has(status)) {
1068
+ throw new ParentQueueReaskError(SECRETARIAT_STATUS_REASK);
1069
+ }
1070
+ if (status === "escalate") {
1071
+ // Parent escalate → throw to caller as-is; 给事中 does not attend (#969).
1072
+ return undefined;
1073
+ }
1074
+ try {
1075
+ const pass = await roleHost.requireGatekeeperPass!({
1076
+ context: ctx,
1077
+ subject: { kind: "secretariat_verdict" },
1078
+ ...(signal === undefined ? {} : { signal }),
1079
+ hostActions,
1080
+ toolCallId,
1081
+ // #879: this-turn typed payload — identity-bound at submit site.
1082
+ submission: parameters,
1083
+ });
1084
+ // #969: book 给事中 署 snapshot for seat settlement projection (receipt + runId).
1085
+ // Ledger accepted stays LLM params (#836); public terminal reads this entry.
1086
+ if (
1087
+ pass !== undefined
1088
+ && pass !== null
1089
+ && typeof pass === "object"
1090
+ && "receipt" in pass
1091
+ ) {
1092
+ const {
1093
+ SECRETARIAT_GATE_OFFICER_ENTRY_TYPE,
1094
+ } = await import("./secretariat-contracts.ts");
1095
+ const runId =
1096
+ typeof pass.runId === "string" && pass.runId.trim() !== ""
1097
+ ? pass.runId
1098
+ : undefined;
1099
+ ctx.sessionManager.appendCustomEntry?.(SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, {
1100
+ officer: "countersign",
1101
+ receipt: pass.receipt,
1102
+ ...(runId === undefined ? {} : { runId }),
1103
+ });
1104
+ }
1105
+ return undefined;
1106
+ } catch (error) {
1107
+ // #969: 给事中上呈 ends parent with officer receipt (no retry / 不擅改).
1108
+ if (
1109
+ error instanceof GatekeeperDecisionError
1110
+ && error.result.status === "escalate"
1111
+ ) {
1112
+ const receipt = error.result.receipt;
1113
+ const receiptRecord =
1114
+ receipt !== null && typeof receipt === "object" && !Array.isArray(receipt)
1115
+ ? (receipt as Record<string, unknown>)
1116
+ : undefined;
1117
+ const runId =
1118
+ typeof error.result.runId === "string" && error.result.runId.trim() !== ""
1119
+ ? error.result.runId
1120
+ : undefined;
1121
+ // Durable on every host: envelope persists custom entries only
1122
+ // (toolResult rows are memory-only on headless/ACP — #617/#959).
1123
+ const {
1124
+ SECRETARIAT_GATE_OFFICER_ENTRY_TYPE,
1125
+ } = await import("./secretariat-contracts.ts");
1126
+ ctx.sessionManager.appendCustomEntry?.(SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, {
1127
+ officer: "countersign",
1128
+ receipt,
1129
+ ...(runId === undefined ? {} : { runId }),
1130
+ });
1131
+ const { buildAuditEscalationResult } = await import("./audit-escalation.ts");
1132
+ return buildAuditEscalationResult(
1133
+ {
1134
+ status: "escalate",
1135
+ officer: "countersign",
1136
+ ...(receiptRecord !== undefined
1137
+ && Object.prototype.hasOwnProperty.call(receiptRecord, "decisionGate")
1138
+ ? { decisionGate: receiptRecord.decisionGate }
1139
+ : {}),
1140
+ },
1141
+ receipt,
1142
+ );
1143
+ }
1144
+ throw error;
1145
+ }
1146
+ }
1147
+ : undefined;
1049
1148
  const base = createFiledOfficerRuntime(
1050
1149
  roleHost,
1051
1150
  {
@@ -1053,6 +1152,7 @@ export function createSecretariatRoleRuntime(
1053
1152
  tool: SECRETARIAT_OUTPUT_TOOL_SPEC,
1054
1153
  acceptedText: SECRETARIAT_ACCEPTED_TEXT,
1055
1154
  soulTag: "secretariat",
1155
+ ...(beforeAccept === undefined ? {} : { beforeAccept }),
1056
1156
  },
1057
1157
  dependencies,
1058
1158
  );
@@ -1893,7 +1993,10 @@ export function createRoleRuntimeExtension(
1893
1993
  ...(dependencies.packageRoot === undefined
1894
1994
  ? {}
1895
1995
  : { packageRoot: dependencies.packageRoot }),
1896
- });
1996
+ ...(dependencies.hostAdapters === undefined
1997
+ ? {}
1998
+ : { hostAdapters: dependencies.hostAdapters }),
1999
+ }, hostActions);
1897
2000
  const merger = createMergerRoleRuntime(roleHost, {
1898
2001
  async loadSoul() { if (!dependencies.loadMergerSoul) throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerSoul(); },
1899
2002
  async loadInput(path) { if (!dependencies.loadMergerInput) throw new Error("Merger runtime dependencies are not configured"); return dependencies.loadMergerInput(path); },
@@ -2195,11 +2298,9 @@ export function createRoleRuntimeExtension(
2195
2298
  // Production topology only (ADR 0048/0049): no test-only ledger hooks.
2196
2299
  // Admit durable session file first so lifecycle getEntries/appendEntry are truthful:
2197
2300
  // deferred SM materialization must not wipe an in-memory principal marker.
2198
- const bookKey = resolveBookKeyFromGit(ctx.cwd);
2199
- const correlation = correlationIdentityFromEnv();
2200
- const sessionFile = ctx.sessionManager.getSessionFile?.() || ctx.sessionManager.getSessionDir?.();
2201
- const ledgerHome = resolveActivationLedgerHomeForPath(sessionFile);
2202
- const session = durableSessionPointer(ctx.sessionManager);
2301
+ // #855: two-face waiting.jsonl write removed — fail-closed book-key + session only.
2302
+ resolveBookKeyFromGit(ctx.cwd);
2303
+ durableSessionPointer(ctx.sessionManager);
2203
2304
 
2204
2305
  // Station children (court diarist, inner-gate summons) omit navigator sidecar (#840).
2205
2306
  // Top-level public entry legs still attend automatically.
@@ -2314,16 +2415,6 @@ export function createRoleRuntimeExtension(
2314
2415
  if (entry.role === "coder") coder.armSubmissionGate(ctx.cwd, ctx.sessionManager);
2315
2416
  else fixer.armSubmissionGate(ctx.cwd, ctx.sessionManager);
2316
2417
  }
2317
- appendAcceptedActivationToBook({
2318
- ledgerHome,
2319
- fact: buildAcceptedActivationFact({
2320
- role: entry.role,
2321
- observedAt: clock(),
2322
- bookKey,
2323
- session,
2324
- correlation,
2325
- }),
2326
- });
2327
2418
  admitted = true;
2328
2419
  } catch (error) {
2329
2420
  failInfrastructure(error, ctx);
@@ -11,6 +11,24 @@ export const SECRETARIAT_OUTPUT_TOOL_NAME = "ak_secretariat_output";
11
11
  export const SECRETARIAT_SUMMON_COUNTERSIGN_TOOL_NAME =
12
12
  "ak_secretariat_summon_countersign";
13
13
  export const SECRETARIAT_ACCEPTED_TEXT = "中书省回执已接受";
14
+ /**
15
+ * #969 durable custom entry: 给事中 terminal (署|上呈) for seat settlement projection.
16
+ * Carries original receipt bytes + nested runId. Envelope persists custom entries
17
+ * (not toolResult rows) — headless/ACP safe. Single authority for public terminal
18
+ * officer projection (pass and escalate share this entry).
19
+ */
20
+ export const SECRETARIAT_GATE_OFFICER_ENTRY_TYPE =
21
+ "ak-secretariat-gate-officer" as const;
22
+
23
+ /** Decisive-facts key for nested 给事中 terminal on public Secretariat settlement. */
24
+ export const SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY = "countersignTerminal" as const;
25
+
26
+ export type SecretariatCountersignTerminalFact = {
27
+ /** Officer receipt original bytes — never rewritten. */
28
+ readonly receipt: unknown;
29
+ /** Nested 给事中 runId when known. */
30
+ readonly runId?: string;
31
+ };
14
32
 
15
33
  export type SecretariatVerdict =
16
34
  | {
@@ -1,61 +0,0 @@
1
- const DISPATCH_STUB_EVENT = "dispatch-stub";
2
- function buildDispatchStubFact(input) {
3
- return {
4
- event: DISPATCH_STUB_EVENT,
5
- observedAt: input.observedAt,
6
- bookKey: input.bookKey,
7
- dispatch: input.dispatch.kind === "process" ? { kind: "process", pid: input.dispatch.pid } : { kind: "opaque", ref: input.dispatch.ref },
8
- correlation: { kind: "caller", id: input.correlation.id }
9
- };
10
- }
11
- function callerCorrelationId(correlation) {
12
- return correlation.kind === "caller" ? correlation.id : void 0;
13
- }
14
- function dispatchMatchesActivation(dispatch, activation) {
15
- const activationId = callerCorrelationId(activation.correlation);
16
- return activationId !== void 0 && activationId === dispatch.correlation.id && activation.bookKey === dispatch.bookKey;
17
- }
18
- function reconcileInvocation(subject) {
19
- const { dispatch, activation, process } = subject;
20
- if (activation !== void 0) {
21
- if (dispatch !== void 0 && dispatchMatchesActivation(dispatch, activation)) {
22
- return {
23
- kind: "matched",
24
- correlationId: dispatch.correlation.id,
25
- bookKey: dispatch.bookKey
26
- };
27
- }
28
- return {
29
- kind: "activation-without-dispatch",
30
- correlationId: callerCorrelationId(activation.correlation),
31
- bookKey: activation.bookKey
32
- };
33
- }
34
- if (dispatch !== void 0) {
35
- if (process === void 0) {
36
- throw new TypeError(
37
- "reconcileInvocation requires process liveness when activation is absent"
38
- );
39
- }
40
- if (process.state === "alive") {
41
- return {
42
- kind: "pending",
43
- correlationId: dispatch.correlation.id,
44
- bookKey: dispatch.bookKey
45
- };
46
- }
47
- return {
48
- kind: "ghost",
49
- correlationId: dispatch.correlation.id,
50
- bookKey: dispatch.bookKey
51
- };
52
- }
53
- throw new TypeError(
54
- "reconcileInvocation requires a dispatch stub and/or an accepted-activation fact"
55
- );
56
- }
57
- export {
58
- DISPATCH_STUB_EVENT,
59
- buildDispatchStubFact,
60
- reconcileInvocation
61
- };
@@ -1,163 +0,0 @@
1
- import type { AcceptedActivationFact } from "./activation-ledger.ts";
2
-
3
- /** Event category for pre-dispatch stub facts (#11 producer contract). */
4
- export const DISPATCH_STUB_EVENT = "dispatch-stub" as const;
5
-
6
- /**
7
- * Non-content dispatch pointer. Carries process identity or an opaque ref —
8
- * never prompt/argv/content bytes (ADR 0049).
9
- */
10
- export type DispatchPointer =
11
- | { readonly kind: "process"; readonly pid: number }
12
- | { readonly kind: "opaque"; readonly ref: string };
13
-
14
- /**
15
- * Minimum typed dispatch stub the #11 producer supplies before ignition.
16
- * Index-only: correlation, book, time, event category, non-content pointer.
17
- */
18
- export type DispatchStubFact = {
19
- readonly event: typeof DISPATCH_STUB_EVENT;
20
- readonly observedAt: string;
21
- readonly bookKey: string;
22
- readonly dispatch: DispatchPointer;
23
- readonly correlation: { readonly kind: "caller"; readonly id: string };
24
- };
25
-
26
- /** Trusted typed inputs for building the closed dispatch stub (fact minus event discriminant). */
27
- export type DispatchStubFactInput = Omit<DispatchStubFact, "event">;
28
-
29
- /**
30
- * Construct the closed dispatch stub from trusted typed inputs only.
31
- * Nested dispatch/correlation are rebuilt closed (ADR 0049: first-phase ledger holds zero content bytes by construction).
32
- */
33
- export function buildDispatchStubFact(input: DispatchStubFactInput): DispatchStubFact {
34
- return {
35
- event: DISPATCH_STUB_EVENT,
36
- observedAt: input.observedAt,
37
- bookKey: input.bookKey,
38
- dispatch: input.dispatch.kind === "process"
39
- ? { kind: "process", pid: input.dispatch.pid }
40
- : { kind: "opaque", ref: input.dispatch.ref },
41
- correlation: { kind: "caller", id: input.correlation.id },
42
- };
43
- }
44
-
45
- /**
46
- * Truthful process liveness supplied by the consumer.
47
- * Reconciliation never probes, kills, or retries processes (D1a / #78).
48
- */
49
- export type ProcessLivenessFact =
50
- | { readonly state: "alive" }
51
- | { readonly state: "terminated" };
52
-
53
- /**
54
- * One reconciliation subject as an exclusive complete union (four outcomes only):
55
- * - activation present → activation required; dispatch/process optional
56
- * - activation absent → dispatch + truthful process required; activation prohibited
57
- * No session-file existence oracle (ADR 0047). Untyped JS still hits runtime TypeError.
58
- */
59
- export type ReconciliationSubject =
60
- | {
61
- readonly activation: AcceptedActivationFact;
62
- readonly dispatch?: DispatchStubFact;
63
- readonly process?: ProcessLivenessFact;
64
- }
65
- | {
66
- readonly activation?: undefined;
67
- readonly dispatch: DispatchStubFact;
68
- readonly process: ProcessLivenessFact;
69
- };
70
-
71
- /**
72
- * Exactly four typed outcomes (D1a):
73
- * - matched — dispatch stub and matching accepted-activation fact
74
- * - pending — dispatch present, no activation, process still alive
75
- * - ghost — dispatch present, no activation, process terminated
76
- * - activation-without-dispatch — activation fact with no matching stub
77
- */
78
- export type ReconciliationOutcome =
79
- | {
80
- readonly kind: "matched";
81
- readonly correlationId: string;
82
- readonly bookKey: string;
83
- }
84
- | {
85
- readonly kind: "pending";
86
- readonly correlationId: string;
87
- readonly bookKey: string;
88
- }
89
- | {
90
- readonly kind: "ghost";
91
- readonly correlationId: string;
92
- readonly bookKey: string;
93
- }
94
- | {
95
- readonly kind: "activation-without-dispatch";
96
- readonly correlationId: string | undefined;
97
- readonly bookKey: string;
98
- };
99
-
100
- function callerCorrelationId(
101
- correlation: AcceptedActivationFact["correlation"] | DispatchStubFact["correlation"],
102
- ): string | undefined {
103
- return correlation.kind === "caller" ? correlation.id : undefined;
104
- }
105
-
106
- function dispatchMatchesActivation(
107
- dispatch: DispatchStubFact,
108
- activation: AcceptedActivationFact,
109
- ): boolean {
110
- const activationId = callerCorrelationId(activation.correlation);
111
- return (
112
- activationId !== undefined
113
- && activationId === dispatch.correlation.id
114
- && activation.bookKey === dispatch.bookKey
115
- );
116
- }
117
-
118
- /**
119
- * Reconcile one invocation's typed dispatch/activation facts against truthful
120
- * process liveness. Pure: never kills, never retries, never reads session files.
121
- */
122
- export function reconcileInvocation(subject: ReconciliationSubject): ReconciliationOutcome {
123
- const { dispatch, activation, process } = subject;
124
-
125
- if (activation !== undefined) {
126
- if (dispatch !== undefined && dispatchMatchesActivation(dispatch, activation)) {
127
- return {
128
- kind: "matched",
129
- correlationId: dispatch.correlation.id,
130
- bookKey: dispatch.bookKey,
131
- };
132
- }
133
- return {
134
- kind: "activation-without-dispatch",
135
- correlationId: callerCorrelationId(activation.correlation),
136
- bookKey: activation.bookKey,
137
- };
138
- }
139
-
140
- if (dispatch !== undefined) {
141
- if (process === undefined) {
142
- throw new TypeError(
143
- "reconcileInvocation requires process liveness when activation is absent",
144
- );
145
- }
146
- if (process.state === "alive") {
147
- return {
148
- kind: "pending",
149
- correlationId: dispatch.correlation.id,
150
- bookKey: dispatch.bookKey,
151
- };
152
- }
153
- return {
154
- kind: "ghost",
155
- correlationId: dispatch.correlation.id,
156
- bookKey: dispatch.bookKey,
157
- };
158
- }
159
-
160
- throw new TypeError(
161
- "reconcileInvocation requires a dispatch stub and/or an accepted-activation fact",
162
- );
163
- }