@akagilnc/pi-workflow-roles 0.1.4620 → 0.1.4631

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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * #855: catchable process signals for ak-role public entry.
3
+ * SIGTERM / SIGINT / SIGHUP abort the shared controller so hosts can gracefully
4
+ * terminate children (SIGTERM only, never SIGKILL). Uncatchable death (SIGKILL,
5
+ * crash, power loss) is reported via run-state + writer lease on analyst.
6
+ */
7
+
8
+ const CATCHABLE_PROCESS_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"] as const;
9
+
10
+ export type CatchableProcessSignal = (typeof CATCHABLE_PROCESS_SIGNALS)[number];
11
+
12
+ export type ProcessCancelHandle = {
13
+ readonly signal: AbortSignal;
14
+ /** Signal name once a catchable signal has been received; else undefined. */
15
+ readonly receivedSignal: () => CatchableProcessSignal | undefined;
16
+ dispose(): void;
17
+ };
18
+
19
+ /**
20
+ * Install once per process entry. First catchable signal wins; later signals
21
+ * are ignored so settlement can finish without re-entry storms.
22
+ */
23
+ export function installProcessCancelHandlers(
24
+ processRef: NodeJS.Process = process,
25
+ ): ProcessCancelHandle {
26
+ const controller = new AbortController();
27
+ let received: CatchableProcessSignal | undefined;
28
+ const listeners = new Map<CatchableProcessSignal, () => void>();
29
+
30
+ for (const sig of CATCHABLE_PROCESS_SIGNALS) {
31
+ const onSignal = (): void => {
32
+ if (received !== undefined) return;
33
+ received = sig;
34
+ try {
35
+ controller.abort(sig);
36
+ } catch {
37
+ // already aborted
38
+ }
39
+ };
40
+ listeners.set(sig, onSignal);
41
+ processRef.on(sig, onSignal);
42
+ }
43
+
44
+ return {
45
+ signal: controller.signal,
46
+ receivedSignal: () => received,
47
+ dispose(): void {
48
+ for (const [sig, onSignal] of listeners) {
49
+ processRef.off(sig, onSignal);
50
+ }
51
+ listeners.clear();
52
+ },
53
+ };
54
+ }
55
+
56
+ /** Signal name when this AbortSignal was aborted by installProcessCancelHandlers. */
57
+ export function processCancelSignalName(
58
+ signal: AbortSignal | undefined,
59
+ ): CatchableProcessSignal | undefined {
60
+ if (signal === undefined || signal.aborted !== true) return undefined;
61
+ const reason = signal.reason;
62
+ if (
63
+ reason === "SIGTERM" ||
64
+ reason === "SIGINT" ||
65
+ reason === "SIGHUP"
66
+ ) {
67
+ return reason;
68
+ }
69
+ return undefined;
70
+ }
71
+
72
+ /** Diagnostic line for a catchable process-signal termination (failure honesty). */
73
+ export function processCancelDiagnostic(signalName: CatchableProcessSignal): string {
74
+ return `ak-role terminated by ${signalName}`;
75
+ }
@@ -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,
@@ -2195,11 +2160,9 @@ export function createRoleRuntimeExtension(
2195
2160
  // Production topology only (ADR 0048/0049): no test-only ledger hooks.
2196
2161
  // Admit durable session file first so lifecycle getEntries/appendEntry are truthful:
2197
2162
  // 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);
2163
+ // #855: two-face waiting.jsonl write removed — fail-closed book-key + session only.
2164
+ resolveBookKeyFromGit(ctx.cwd);
2165
+ durableSessionPointer(ctx.sessionManager);
2203
2166
 
2204
2167
  // Station children (court diarist, inner-gate summons) omit navigator sidecar (#840).
2205
2168
  // Top-level public entry legs still attend automatically.
@@ -2314,16 +2277,6 @@ export function createRoleRuntimeExtension(
2314
2277
  if (entry.role === "coder") coder.armSubmissionGate(ctx.cwd, ctx.sessionManager);
2315
2278
  else fixer.armSubmissionGate(ctx.cwd, ctx.sessionManager);
2316
2279
  }
2317
- appendAcceptedActivationToBook({
2318
- ledgerHome,
2319
- fact: buildAcceptedActivationFact({
2320
- role: entry.role,
2321
- observedAt: clock(),
2322
- bookKey,
2323
- session,
2324
- correlation,
2325
- }),
2326
- });
2327
2280
  admitted = true;
2328
2281
  } catch (error) {
2329
2282
  failInfrastructure(error, ctx);
@@ -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
- }