@akagilnc/pi-workflow-roles 0.1.2089 → 0.1.2096

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,11 +4,16 @@
4
4
  * Fixed 183000ms silence clock on package-owned tool execute only.
5
5
  * Real producing onUpdate resets; final resolve/reject clears; timeout throws so
6
6
  * Pi settles the current call as an LLM-visible isError tool result. No retry,
7
- * role failure, process termination, signal abort, config, or Pi built-in coverage.
7
+ * role failure, process termination, config, or Pi built-in coverage.
8
8
  *
9
9
  * #339: do not name-exempt whole terminating tools. Outer idle stays armed for
10
10
  * pre/post-audit work. Only the real compliance-audit await suspends this single
11
11
  * layer (ADR 0059 owns that interval); resume re-arms the same outer backstop.
12
+ *
13
+ * #380: on idle, abort a derived tool signal (not the caller signal) so detour
14
+ * spawn cleanup and softFail can run through the existing AbortSignal path.
15
+ * Cooperative soft-settle may win; non-listeners still hard-reject after a
16
+ * microtask drain (no second timer).
12
17
  */
13
18
  import { AsyncLocalStorage } from "node:async_hooks";
14
19
  import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
@@ -28,6 +33,8 @@ const WRAPPED = Symbol.for("ak.packageOwnedToolIdleWrapped");
28
33
  type PackageOwnedToolIdleSuspension = {
29
34
  suspend(): void;
30
35
  resume(): void;
36
+ /** Reset the silence window (byte/activity signal from the active execute). */
37
+ poke(): void;
31
38
  };
32
39
 
33
40
  /**
@@ -46,6 +53,20 @@ export class PackageOwnedToolIdleTimeoutError extends Error {
46
53
  }
47
54
  }
48
55
 
56
+ export function isPackageOwnedToolIdleTimeoutError(
57
+ value: unknown,
58
+ ): value is PackageOwnedToolIdleTimeoutError {
59
+ return (
60
+ value instanceof PackageOwnedToolIdleTimeoutError ||
61
+ (
62
+ typeof value === "object" &&
63
+ value !== null &&
64
+ (value as { name?: unknown }).name === "PackageOwnedToolIdleTimeoutError" &&
65
+ (value as { code?: unknown }).code === PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE
66
+ )
67
+ );
68
+ }
69
+
49
70
  /** Minimal executable tool shape accepted by the shared idle wrapper. */
50
71
  export type PackageOwnedToolLike = {
51
72
  readonly name: string;
@@ -87,6 +108,15 @@ export async function withPackageOwnedToolIdleSuspended<T>(
87
108
  }
88
109
  }
89
110
 
111
+ /**
112
+ * Touch the active package-owned execute idle clock (reuse stream-idle-guard poke).
113
+ * No-op outside a wrapped package-owned execute or while that layer is suspended.
114
+ * Used by long-running tools (engine detour) that see real subprocess output bytes.
115
+ */
116
+ export function pokePackageOwnedToolIdle(): void {
117
+ packageOwnedToolIdleScope.getStore()?.poke();
118
+ }
119
+
90
120
  /**
91
121
  * Single shared execute wrapper for package-owned tool definitions.
92
122
  * Idempotent: wrapping twice returns the same protected definition.
@@ -103,7 +133,7 @@ export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(t
103
133
  const wrappedExecute = function packageOwnedToolIdleExecute(
104
134
  ...args: unknown[]
105
135
  ): Promise<unknown> {
106
- const signal = args[2] as AbortSignal | undefined;
136
+ const parentSignal = args[2] as AbortSignal | undefined;
107
137
  const onUpdate = args[3] as ((partialResult: unknown) => void) | undefined;
108
138
  return new Promise((resolve, reject) => {
109
139
  let settled = false;
@@ -112,15 +142,46 @@ export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(t
112
142
  idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
113
143
  });
114
144
 
145
+ // Derived tool signal: caller cancel forwards; idle aborts this only (not parent).
146
+ const toolController = new AbortController();
147
+ const onParentAbort = (): void => {
148
+ if (toolController.signal.aborted) return;
149
+ toolController.abort(parentSignal?.reason);
150
+ };
151
+ if (parentSignal !== undefined) {
152
+ if (parentSignal.aborted) {
153
+ toolController.abort(parentSignal.reason);
154
+ } else {
155
+ parentSignal.addEventListener("abort", onParentAbort);
156
+ }
157
+ }
158
+
159
+ const cleanup = (): void => {
160
+ idle.signal.removeEventListener("abort", onIdle);
161
+ idle.dispose();
162
+ parentSignal?.removeEventListener("abort", onParentAbort);
163
+ };
164
+
115
165
  const settle = (deliver: () => void): void => {
116
166
  if (settled) return;
117
167
  settled = true;
118
- idle.signal.removeEventListener("abort", onIdle);
119
- idle.dispose();
168
+ cleanup();
120
169
  deliver();
121
170
  };
122
171
  const onIdle = (): void => {
123
- settle(() => reject(new PackageOwnedToolIdleTimeoutError()));
172
+ const idleError = new PackageOwnedToolIdleTimeoutError();
173
+ if (!toolController.signal.aborted) {
174
+ toolController.abort(idleError);
175
+ }
176
+ // Cooperative tools (detour) soft-settle via the aborted signal in the
177
+ // next microtasks. Non-listeners still hard-reject after a short drain
178
+ // of those microtasks — no second wall-clock timer.
179
+ void Promise.resolve()
180
+ .then(() => {})
181
+ .then(() => {})
182
+ .then(() => {
183
+ settle(() => reject(idleError));
184
+ });
124
185
  };
125
186
  idle.signal.addEventListener("abort", onIdle, { once: true });
126
187
 
@@ -141,6 +202,10 @@ export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(t
141
202
  });
142
203
  idle.signal.addEventListener("abort", onIdle, { once: true });
143
204
  },
205
+ poke(): void {
206
+ if (settled || suspended) return;
207
+ idle.poke();
208
+ },
144
209
  };
145
210
 
146
211
  const guardedOnUpdate = onUpdate === undefined
@@ -152,8 +217,9 @@ export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(t
152
217
  };
153
218
 
154
219
  const callArgs = args.slice();
155
- // Preserve the original signal at args[2]; timeout must not abort it.
156
- callArgs[2] = signal;
220
+ // Pass derived signal so idle/cancel can clean up subprocesses without
221
+ // aborting the caller/parent agent signal identity.
222
+ callArgs[2] = toolController.signal;
157
223
  callArgs[3] = guardedOnUpdate;
158
224
 
159
225
  void packageOwnedToolIdleScope.run(suspension, async () => {
@@ -34,6 +34,7 @@ import {
34
34
  COLLECTOR_WAIT_TOOL,
35
35
  } from "../collector-ledger.ts";
36
36
  import { ENGINE_DETOUR_TOOL_NAME } from "../engine-detour.ts";
37
+ import { readEngineLaborFallbackFieldFrom } from "../engine-labor-fallback.ts";
37
38
  import {
38
39
  JUDGE_OUTPUT_TOOL_NAME,
39
40
  type JudgeVerdict,
@@ -1060,7 +1061,12 @@ function judgeDecisiveFacts(
1060
1061
  verdict: object,
1061
1062
  judgeStatus: JudgeVerdict["judgeStatus"],
1062
1063
  ): Record<string, unknown> {
1063
- const facts: Record<string, unknown> = { judgeStatus, ...auditNoReceiptDecisiveFact(verdict) };
1064
+ const facts: Record<string, unknown> = {
1065
+ judgeStatus,
1066
+ ...auditNoReceiptDecisiveFact(verdict),
1067
+ // #380: spread sole-built field; do not re-key here (S1).
1068
+ ...readEngineLaborFallbackFieldFrom(verdict),
1069
+ };
1064
1070
  if (judgeStatus === "continue") {
1065
1071
  const fix = safelyRead(verdict, "fix");
1066
1072
  if (fix.readable && isRecord(fix.value)) {
@@ -1260,6 +1266,8 @@ function reviewerDecisiveFacts(
1260
1266
  reportAxes,
1261
1267
  acceptedBatchPresent: acceptedBatch.readable && acceptedBatch.value !== undefined,
1262
1268
  ...auditNoReceiptDecisiveFact(candidate),
1269
+ // #380: spread sole-built field; do not re-key here (S1).
1270
+ ...readEngineLaborFallbackFieldFrom(candidate),
1263
1271
  };
1264
1272
  if (status.readable && typeof status.value === "string") facts.reviewerStatus = status.value;
1265
1273
  if (
@@ -7,6 +7,10 @@ export type { CanonicalSkillBinding };
7
7
  import { disposeComplianceDecision } from "./audit-escalation.ts";
8
8
  import type { ComplianceDecision } from "./compliance-transport.ts";
9
9
  import { appendActiveSessionCustomEntry } from "./compliance-transport.ts";
10
+ import {
11
+ readActivationEngineLaborFallbackField,
12
+ withEngineLaborFallbackField,
13
+ } from "./engine-labor-fallback.ts";
10
14
  import { REVIEWER_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
11
15
  import { type ReviewerSpecDisposition } from "./reviewer-construction.ts";
12
16
  import { createReviewerDispatcher, type AcceptedReviewerDispatch, type AcceptedReviewerExecution, type ReviewerIssueFetcher, type ReviewerPinnedGitReader } from "./reviewer-dispatch.ts";
@@ -145,11 +149,15 @@ export function createReviewerRoleRuntime(
145
149
  const output = validateReviewerIntent(parameters);
146
150
  let record: ReviewerExecutionRecord;
147
151
  try { record = ledger.recordForAudit(output.status); } catch (error) { if ((error as any)?.fatalReviewerInfrastructure) hostActions.failInfrastructure(error, toolCtx, id); throw error; }
148
- const candidate = assembleRuntimeReviewerReceipt({
149
- intent: output,
150
- record,
151
- canonicalSkillText: binding.snapshot.raw,
152
- });
152
+ const candidate = withEngineLaborFallbackField(
153
+ assembleRuntimeReviewerReceipt({
154
+ intent: output,
155
+ record,
156
+ canonicalSkillText: binding.snapshot.raw,
157
+ }),
158
+ // #380: sole-built declaration when any leg detour fell back to seat labor.
159
+ readActivationEngineLaborFallbackField(),
160
+ );
153
161
  // First-record-then-audit: candidate lands on the parent session books
154
162
  // before the auditor is spawned (zero hand-delivery).
155
163
  try {
@@ -21,7 +21,13 @@ import {
21
21
  writeToolExecutionObservationRecord,
22
22
  type ToolExecutionObservationWriter,
23
23
  } from "./tool-execution-observation.ts";
24
+ import { ENGINE_DETOUR_TOOL_NAME } from "./engine-detour.ts";
24
25
  import { registerEngineDetourTool } from "./engine-detour-tool.ts";
26
+ import {
27
+ createEngineLaborFallbackLatch,
28
+ installActivationEngineLaborFallbackLatch,
29
+ restoreEngineLaborFallbackFromSessionEntries,
30
+ } from "./engine-labor-fallback.ts";
25
31
  import { installPackageOwnedToolRegistration } from "./package-owned-tool-idle.ts";
26
32
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
27
33
  import { createOAuthKeepalive, type OAuthKeepaliveOptions } from "./oauth-keepalive.ts";
@@ -591,8 +597,9 @@ export function createRoleRuntimeExtension(
591
597
  let pendingNavigatorSettlement: Promise<void> | undefined;
592
598
  let navigatorWorkContext: NavigatorWorkContext | undefined;
593
599
  const pendingInfrastructureToolCallIds = new Set<string>();
594
- // #357 T2 / #378: engine detour once-latch + registration (Judge|Reviewer+engine).
600
+ // #357 T2 / #378 / #380: engine detour once-latch + seat-fallback latch (Judge|Reviewer+engine).
595
601
  let engineDetourRegistration: ReturnType<typeof registerEngineDetourTool> | undefined;
602
+ let engineLaborFallbackLatch = createEngineLaborFallbackLatch();
596
603
  // #288 primary-session thin adapter. The policy is the sole budget owner;
597
604
  // terminating-tool rejections and mechanical delivery requests share two turns.
598
605
  let receiptDelivery = createReceiptDeliveryPolicy();
@@ -1056,6 +1063,18 @@ export function createRoleRuntimeExtension(
1056
1063
  pendingNavigatorPresentation = undefined;
1057
1064
  pendingNavigatorSettlement = undefined;
1058
1065
  pendingInfrastructureToolCallIds.clear();
1066
+ engineLaborFallbackLatch = createEngineLaborFallbackLatch();
1067
+ // #380 resume: restore sole-built fallback from durable same-session detour results.
1068
+ const sessionEntries =
1069
+ typeof ctx.sessionManager?.getEntries === "function"
1070
+ ? ctx.sessionManager.getEntries()
1071
+ : [];
1072
+ restoreEngineLaborFallbackFromSessionEntries(
1073
+ engineLaborFallbackLatch,
1074
+ sessionEntries,
1075
+ ENGINE_DETOUR_TOOL_NAME,
1076
+ );
1077
+ installActivationEngineLaborFallbackLatch(engineLaborFallbackLatch);
1059
1078
  engineDetourRegistration?.resetLatch();
1060
1079
  navigatorWorkContext = undefined;
1061
1080
  // #351: OAuth keepalive is orthogonal to --ak-role; start before role early-return
@@ -1168,8 +1187,9 @@ export function createRoleRuntimeExtension(
1168
1187
  }
1169
1188
 
1170
1189
  await executeActivationStage(entry.role, activationStage(entry.role, runtime), { clock, writeTrace });
1171
- // #357 T2 / #378: Judge|Reviewer+engine activation registers the package detour tool once.
1190
+ // #357 T2 / #378 / #380: Judge|Reviewer+engine activation registers the package detour tool once.
1172
1191
  // Gate is env presence only — no per-engine execute branch; no role-module spawn.
1192
+ // Seat-fallback latch is activation-scoped (installed at session_start) so legs share it.
1173
1193
  if (
1174
1194
  (entry.role === "judge" || entry.role === "reviewer") &&
1175
1195
  engineDetourRegistration === undefined