@akagilnc/pi-workflow-roles 0.1.2026 → 0.1.2033

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.
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { executeAuditorChild, } from "./evidence-child-executor.js";
3
3
  import { createAuditorDossierTool } from "./auditor-dossier-tool.js";
4
+ import { withPackageOwnedToolIdleSuspended } from "./package-owned-tool-idle.js";
4
5
  /** Zero-projection kickoff — soul already carries dossier-fetch duty; no hand-delivered materials. */
5
6
  export const AUDITOR_DOSSIER_PROMPT = "Audit the current run dossier.";
6
7
  const nonblank = Type.String({ minLength: 1, pattern: "\\S" });
@@ -61,24 +62,28 @@ export function readComplianceCandidate(arguments_, usage) {
61
62
  return { status: "audit-incomplete", observation: { kind: "object-status-unreadable", status: status === undefined ? "missing" : "unknown" }, candidate: arguments_, ...(usage === undefined ? {} : { usage }) };
62
63
  }
63
64
  export async function runComplianceAudit(options) {
64
- const prompt = options.serializedInput ?? AUDITOR_DOSSIER_PROMPT;
65
- const receipt = await executeAuditorChild({
66
- tool: options.tool,
67
- dossierTool: createAuditorDossierTool(options.runDirectory),
68
- systemPrompt: options.systemPrompt,
69
- prompt,
70
- roleLabel: options.roleLabel,
71
- context: options.context,
72
- retainResponse: (response) => retainComplianceResponse(options.context, response),
73
- ...(options.runCompletion === undefined ? {} : { runCompletion: options.runCompletion }),
74
- ...(options.signal === undefined ? {} : { signal: options.signal }),
65
+ // #339: only the real compliance-audit await leaves the outer package-owned
66
+ // idle owner. Pre/post-audit work stays under the single outer backstop.
67
+ return withPackageOwnedToolIdleSuspended(async () => {
68
+ const prompt = options.serializedInput ?? AUDITOR_DOSSIER_PROMPT;
69
+ const receipt = await executeAuditorChild({
70
+ tool: options.tool,
71
+ dossierTool: createAuditorDossierTool(options.runDirectory),
72
+ systemPrompt: options.systemPrompt,
73
+ prompt,
74
+ roleLabel: options.roleLabel,
75
+ context: options.context,
76
+ retainResponse: (response) => retainComplianceResponse(options.context, response),
77
+ ...(options.runCompletion === undefined ? {} : { runCompletion: options.runCompletion }),
78
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
79
+ });
80
+ if (receipt.noReceiptLifecycle !== undefined) {
81
+ return {
82
+ status: "no-receipt",
83
+ ...receipt.noReceiptLifecycle,
84
+ ...(receipt.response.usage === undefined ? {} : { usage: receipt.response.usage }),
85
+ };
86
+ }
87
+ return readComplianceCandidate(receipt.decision, receipt.response.usage);
75
88
  });
76
- if (receipt.noReceiptLifecycle !== undefined) {
77
- return {
78
- status: "no-receipt",
79
- ...receipt.noReceiptLifecycle,
80
- ...(receipt.response.usage === undefined ? {} : { usage: receipt.response.usage }),
81
- };
82
- }
83
- return readComplianceCandidate(receipt.decision, receipt.response.usage);
84
89
  }
@@ -296,6 +296,10 @@ export async function createInheritedRuntime(options) {
296
296
  },
297
297
  };
298
298
  runtime.registerNativeProvider(provider);
299
+ // registerNativeProvider fires a background refresh; await it so child
300
+ // AgentSession.prompt sees configured auth without racing void refresh
301
+ // (mock timers / slow CI otherwise stall before the compliance stream).
302
+ await runtime.refresh({ allowNetwork: false });
299
303
  return state;
300
304
  }
301
305
  function numericHttpStatus(value) {
@@ -1,8 +1,26 @@
1
+ /**
2
+ * #102 package-owned tool idle backstop.
3
+ *
4
+ * Fixed 183000ms silence clock on package-owned tool execute only.
5
+ * Real producing onUpdate resets; final resolve/reject clears; timeout throws so
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.
8
+ *
9
+ * #339: do not name-exempt whole terminating tools. Outer idle stays armed for
10
+ * pre/post-audit work. Only the real compliance-audit await suspends this single
11
+ * layer (ADR 0059 owns that interval); resume re-arms the same outer backstop.
12
+ */
13
+ import { AsyncLocalStorage } from "node:async_hooks";
1
14
  import { DEFAULT_STREAM_IDLE_TIMEOUT_MS, createStreamIdleGuard, } from "./stream-idle-guard.js";
2
15
  import { isProducingToolUpdate } from "./tool-execution-observation.js";
3
16
  export const PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS = DEFAULT_STREAM_IDLE_TIMEOUT_MS;
4
17
  export const PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE = "AK_PACKAGE_OWNED_TOOL_IDLE_TIMEOUT";
5
18
  const WRAPPED = Symbol.for("ak.packageOwnedToolIdleWrapped");
19
+ /**
20
+ * Active outer package-owned execute idle, if any. Nested tool executes install
21
+ * their own store; compliance audit only suspends the store visible at await time.
22
+ */
23
+ const packageOwnedToolIdleScope = new AsyncLocalStorage();
6
24
  export class PackageOwnedToolIdleTimeoutError extends Error {
7
25
  code = PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE;
8
26
  idleTimeoutMs = PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS;
@@ -33,6 +51,23 @@ function isPackageOwnedToolActivityUpdate(partialResult) {
33
51
  return Reflect.ownKeys(details).length > 0;
34
52
  return true;
35
53
  }
54
+ /**
55
+ * #339: suspend the active package-owned execute idle for one real compliance
56
+ * audit await. Single-layer only — production has one runComplianceAudit seam.
57
+ * No-op outside a wrapped package-owned execute. No second timeout or retry.
58
+ */
59
+ export async function withPackageOwnedToolIdleSuspended(run) {
60
+ const scope = packageOwnedToolIdleScope.getStore();
61
+ if (scope === undefined)
62
+ return run();
63
+ scope.suspend();
64
+ try {
65
+ return await run();
66
+ }
67
+ finally {
68
+ scope.resume();
69
+ }
70
+ }
36
71
  /**
37
72
  * Single shared execute wrapper for package-owned tool definitions.
38
73
  * Idempotent: wrapping twice returns the same protected definition.
@@ -48,7 +83,8 @@ export function wrapPackageOwnedToolDefinition(tool) {
48
83
  const onUpdate = args[3];
49
84
  return new Promise((resolve, reject) => {
50
85
  let settled = false;
51
- const idle = createStreamIdleGuard({
86
+ let suspended = false;
87
+ let idle = createStreamIdleGuard({
52
88
  idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
53
89
  });
54
90
  const settle = (deliver) => {
@@ -63,6 +99,26 @@ export function wrapPackageOwnedToolDefinition(tool) {
63
99
  settle(() => reject(new PackageOwnedToolIdleTimeoutError()));
64
100
  };
65
101
  idle.signal.addEventListener("abort", onIdle, { once: true });
102
+ const suspension = {
103
+ suspend() {
104
+ if (settled || suspended)
105
+ return;
106
+ suspended = true;
107
+ // ADR 0059 owns the audit interval — drop this layer until audit returns.
108
+ idle.signal.removeEventListener("abort", onIdle);
109
+ idle.dispose();
110
+ },
111
+ resume() {
112
+ if (settled || !suspended)
113
+ return;
114
+ suspended = false;
115
+ // Fresh single-layer silence window for post-audit work (e.g. cleanup).
116
+ idle = createStreamIdleGuard({
117
+ idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
118
+ });
119
+ idle.signal.addEventListener("abort", onIdle, { once: true });
120
+ },
121
+ };
66
122
  const guardedOnUpdate = onUpdate === undefined
67
123
  ? undefined
68
124
  : (partialResult) => {
@@ -76,9 +132,15 @@ export function wrapPackageOwnedToolDefinition(tool) {
76
132
  // Preserve the original signal at args[2]; timeout must not abort it.
77
133
  callArgs[2] = signal;
78
134
  callArgs[3] = guardedOnUpdate;
79
- void Promise.resolve()
80
- .then(() => originalExecute(...callArgs))
81
- .then((result) => settle(() => resolve(result)), (error) => settle(() => reject(error)));
135
+ void packageOwnedToolIdleScope.run(suspension, async () => {
136
+ try {
137
+ const result = await originalExecute(...callArgs);
138
+ settle(() => resolve(result));
139
+ }
140
+ catch (error) {
141
+ settle(() => reject(error));
142
+ }
143
+ });
82
144
  });
83
145
  };
84
146
  wrappedExecute[WRAPPED] = true;
@@ -18448,11 +18448,14 @@ var init_tool_execution_observation = __esm({
18448
18448
  });
18449
18449
 
18450
18450
  // src/package-owned-tool-idle.ts
18451
+ import { AsyncLocalStorage } from "node:async_hooks";
18452
+ var packageOwnedToolIdleScope;
18451
18453
  var init_package_owned_tool_idle = __esm({
18452
18454
  "src/package-owned-tool-idle.ts"() {
18453
18455
  "use strict";
18454
18456
  init_stream_idle_guard();
18455
18457
  init_tool_execution_observation();
18458
+ packageOwnedToolIdleScope = new AsyncLocalStorage();
18456
18459
  }
18457
18460
  });
18458
18461
 
@@ -18524,6 +18527,7 @@ var init_compliance_transport = __esm({
18524
18527
  init_build();
18525
18528
  init_evidence_child_executor();
18526
18529
  init_auditor_dossier_tool();
18530
+ init_package_owned_tool_idle();
18527
18531
  nonblank2 = typebox_exports.String({ minLength: 1, pattern: "\\S" });
18528
18532
  decisionGateSchema = typebox_exports.Object({ question: nonblank2, options: typebox_exports.Array(nonblank2, { minItems: 1 }) }, { additionalProperties: false });
18529
18533
  complianceDecisionSchema = typebox_exports.Object({ status: typebox_exports.Unknown({ description: "Auditor decision status." }), violations: typebox_exports.Array(nonblank2, { description: "Observed compliance violations." }), conflicts: typebox_exports.Array(nonblank2, { description: "Unresolved authority or execution conflicts." }), decisionGate: typebox_exports.Union([decisionGateSchema, typebox_exports.Null()], { description: "Escalation question and available options." }) }, { additionalProperties: true, required: [] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.2026",
3
+ "version": "0.1.2033",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -7,6 +7,7 @@ import {
7
7
  } from "./evidence-child-executor.ts";
8
8
  import { createAuditorDossierTool } from "./auditor-dossier-tool.ts";
9
9
  import type { DossierObservation } from "./dossier-resolution.ts";
10
+ import { withPackageOwnedToolIdleSuspended } from "./package-owned-tool-idle.ts";
10
11
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
11
12
 
12
13
  export type ComplianceCompletion = AuditorCompletion;
@@ -116,24 +117,28 @@ export type RunComplianceAuditOptions = {
116
117
  };
117
118
 
118
119
  export async function runComplianceAudit(options: RunComplianceAuditOptions): Promise<ComplianceDecision> {
119
- const prompt = options.serializedInput ?? AUDITOR_DOSSIER_PROMPT;
120
- const receipt = await executeAuditorChild({
121
- tool: options.tool,
122
- dossierTool: createAuditorDossierTool(options.runDirectory),
123
- systemPrompt: options.systemPrompt,
124
- prompt,
125
- roleLabel: options.roleLabel,
126
- context: options.context,
127
- retainResponse: (response) => retainComplianceResponse(options.context, response),
128
- ...(options.runCompletion === undefined ? {} : { runCompletion: options.runCompletion }),
129
- ...(options.signal === undefined ? {} : { signal: options.signal }),
120
+ // #339: only the real compliance-audit await leaves the outer package-owned
121
+ // idle owner. Pre/post-audit work stays under the single outer backstop.
122
+ return withPackageOwnedToolIdleSuspended(async () => {
123
+ const prompt = options.serializedInput ?? AUDITOR_DOSSIER_PROMPT;
124
+ const receipt = await executeAuditorChild({
125
+ tool: options.tool,
126
+ dossierTool: createAuditorDossierTool(options.runDirectory),
127
+ systemPrompt: options.systemPrompt,
128
+ prompt,
129
+ roleLabel: options.roleLabel,
130
+ context: options.context,
131
+ retainResponse: (response) => retainComplianceResponse(options.context, response),
132
+ ...(options.runCompletion === undefined ? {} : { runCompletion: options.runCompletion }),
133
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
134
+ });
135
+ if (receipt.noReceiptLifecycle !== undefined) {
136
+ return {
137
+ status: "no-receipt",
138
+ ...receipt.noReceiptLifecycle,
139
+ ...(receipt.response.usage === undefined ? {} : { usage: receipt.response.usage }),
140
+ };
141
+ }
142
+ return readComplianceCandidate(receipt.decision, receipt.response.usage);
130
143
  });
131
- if (receipt.noReceiptLifecycle !== undefined) {
132
- return {
133
- status: "no-receipt",
134
- ...receipt.noReceiptLifecycle,
135
- ...(receipt.response.usage === undefined ? {} : { usage: receipt.response.usage }),
136
- };
137
- }
138
- return readComplianceCandidate(receipt.decision, receipt.response.usage);
139
144
  }
@@ -381,6 +381,10 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
381
381
  };
382
382
 
383
383
  runtime.registerNativeProvider(provider);
384
+ // registerNativeProvider fires a background refresh; await it so child
385
+ // AgentSession.prompt sees configured auth without racing void refresh
386
+ // (mock timers / slow CI otherwise stall before the compliance stream).
387
+ await runtime.refresh({ allowNetwork: false });
384
388
  return state;
385
389
  }
386
390
 
@@ -5,12 +5,18 @@
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
7
  * role failure, process termination, signal abort, config, or Pi built-in coverage.
8
+ *
9
+ * #339: do not name-exempt whole terminating tools. Outer idle stays armed for
10
+ * pre/post-audit work. Only the real compliance-audit await suspends this single
11
+ * layer (ADR 0059 owns that interval); resume re-arms the same outer backstop.
8
12
  */
13
+ import { AsyncLocalStorage } from "node:async_hooks";
9
14
  import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
10
15
 
11
16
  import {
12
17
  DEFAULT_STREAM_IDLE_TIMEOUT_MS,
13
18
  createStreamIdleGuard,
19
+ type StreamIdleGuard,
14
20
  } from "./stream-idle-guard.ts";
15
21
  import { isProducingToolUpdate } from "./tool-execution-observation.ts";
16
22
 
@@ -19,6 +25,17 @@ export const PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE = "AK_PACKAGE_OWNED_TOOL_IDLE_
19
25
 
20
26
  const WRAPPED = Symbol.for("ak.packageOwnedToolIdleWrapped");
21
27
 
28
+ type PackageOwnedToolIdleSuspension = {
29
+ suspend(): void;
30
+ resume(): void;
31
+ };
32
+
33
+ /**
34
+ * Active outer package-owned execute idle, if any. Nested tool executes install
35
+ * their own store; compliance audit only suspends the store visible at await time.
36
+ */
37
+ const packageOwnedToolIdleScope = new AsyncLocalStorage<PackageOwnedToolIdleSuspension>();
38
+
22
39
  export class PackageOwnedToolIdleTimeoutError extends Error {
23
40
  readonly code = PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE;
24
41
  readonly idleTimeoutMs = PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS;
@@ -52,6 +69,24 @@ function isPackageOwnedToolActivityUpdate(partialResult: unknown): boolean {
52
69
  return true;
53
70
  }
54
71
 
72
+ /**
73
+ * #339: suspend the active package-owned execute idle for one real compliance
74
+ * audit await. Single-layer only — production has one runComplianceAudit seam.
75
+ * No-op outside a wrapped package-owned execute. No second timeout or retry.
76
+ */
77
+ export async function withPackageOwnedToolIdleSuspended<T>(
78
+ run: () => Promise<T>,
79
+ ): Promise<T> {
80
+ const scope = packageOwnedToolIdleScope.getStore();
81
+ if (scope === undefined) return run();
82
+ scope.suspend();
83
+ try {
84
+ return await run();
85
+ } finally {
86
+ scope.resume();
87
+ }
88
+ }
89
+
55
90
  /**
56
91
  * Single shared execute wrapper for package-owned tool definitions.
57
92
  * Idempotent: wrapping twice returns the same protected definition.
@@ -72,7 +107,8 @@ export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(t
72
107
  const onUpdate = args[3] as ((partialResult: unknown) => void) | undefined;
73
108
  return new Promise((resolve, reject) => {
74
109
  let settled = false;
75
- const idle = createStreamIdleGuard({
110
+ let suspended = false;
111
+ let idle: StreamIdleGuard = createStreamIdleGuard({
76
112
  idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
77
113
  });
78
114
 
@@ -88,6 +124,25 @@ export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(t
88
124
  };
89
125
  idle.signal.addEventListener("abort", onIdle, { once: true });
90
126
 
127
+ const suspension: PackageOwnedToolIdleSuspension = {
128
+ suspend(): void {
129
+ if (settled || suspended) return;
130
+ suspended = true;
131
+ // ADR 0059 owns the audit interval — drop this layer until audit returns.
132
+ idle.signal.removeEventListener("abort", onIdle);
133
+ idle.dispose();
134
+ },
135
+ resume(): void {
136
+ if (settled || !suspended) return;
137
+ suspended = false;
138
+ // Fresh single-layer silence window for post-audit work (e.g. cleanup).
139
+ idle = createStreamIdleGuard({
140
+ idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
141
+ });
142
+ idle.signal.addEventListener("abort", onIdle, { once: true });
143
+ },
144
+ };
145
+
91
146
  const guardedOnUpdate = onUpdate === undefined
92
147
  ? undefined
93
148
  : (partialResult: unknown) => {
@@ -101,12 +156,14 @@ export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(t
101
156
  callArgs[2] = signal;
102
157
  callArgs[3] = guardedOnUpdate;
103
158
 
104
- void Promise.resolve()
105
- .then(() => originalExecute(...callArgs))
106
- .then(
107
- (result) => settle(() => resolve(result)),
108
- (error: unknown) => settle(() => reject(error)),
109
- );
159
+ void packageOwnedToolIdleScope.run(suspension, async () => {
160
+ try {
161
+ const result = await originalExecute(...callArgs);
162
+ settle(() => resolve(result));
163
+ } catch (error) {
164
+ settle(() => reject(error));
165
+ }
166
+ });
110
167
  });
111
168
  };
112
169
  (wrappedExecute as { [WRAPPED]?: boolean })[WRAPPED] = true;