@akagilnc/pi-workflow-roles 0.1.2135 → 0.1.2139

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.
package/README.md CHANGED
@@ -40,7 +40,7 @@ ak-role config unset-engine judge
40
40
 
41
41
  Receipts are typed, so callers compose roles without parsing prose; ordering and stopping stay caller-owned. Programmatic consumers derive contracts from the exported schemas in `src/package-contracts/`, not from this guide.
42
42
 
43
- When a labor-engine detour fails and the seat continues the labor on the main road, the typed receipt may carry a mechanical `engineLaborFallback` field: `{ engine, failure, laborBy: "seat" }`. It appears only after a real detour failure that fell back to seat labor (including package-owned idle timeout on the detour tool)—not on detour success or caller cancel. First failure wins for the activation; model-forged `engineLaborFallback` keys are stripped unless the package latch recorded one. Sole producer: `src/engine-labor-fallback.ts`; decision record: [ADR 0071](docs/adr/0071-engine-detour-failure-seat-fallback-declaration.md). This README only projects that contract.
43
+ When a labor-engine detour fails and the seat continues the labor on the main road, the typed receipt may carry a mechanical `engineLaborFallback` field: `{ engine, failure, laborBy: "seat" }`. It appears only after a real detour failure that fell back to seat labor—not on detour success or caller cancel. First failure wins for the activation; model-forged `engineLaborFallback` keys are stripped unless the package latch recorded one. Sole producer: `src/engine-labor-fallback.ts`; decision record: [ADR 0071](docs/adr/0071-engine-detour-failure-seat-fallback-declaration.md). This README only projects that contract.
44
44
 
45
45
  ## Call the roles
46
46
 
package/README.zh-CN.md CHANGED
@@ -40,7 +40,7 @@ ak-role config unset-engine judge
40
40
 
41
41
  回执是 typed 的,调用者不必解析散文即可组合角色;顺序与停止归调用者。编程消费者从 `src/package-contracts/` 导出推导契约,不从本文。
42
42
 
43
- 当劳务引擎绕行失败、座席回到主路继续劳务时,typed 回执可带机械字段 `engineLaborFallback`:`{ engine, failure, laborBy: "seat" }`。仅在真实绕行失败并座席顶班后出现(含 detour 工具命中 package-owned idle 超时)——成功绕行或调用方 cancel 不出现。同一次 activation 内先到先得;无包内 latch 时剥离模型伪造的 `engineLaborFallback` 键。唯一构造点:`src/engine-labor-fallback.ts`;决策记录:[ADR 0071](docs/adr/0071-engine-detour-failure-seat-fallback-declaration.md)。本文只投影该契约。
43
+ 当劳务引擎绕行失败、座席回到主路继续劳务时,typed 回执可带机械字段 `engineLaborFallback`:`{ engine, failure, laborBy: "seat" }`。仅在真实绕行失败并座席顶班后出现——成功绕行或调用方 cancel 不出现。同一次 activation 内先到先得;无包内 latch 时剥离模型伪造的 `engineLaborFallback` 键。唯一构造点:`src/engine-labor-fallback.ts`;决策记录:[ADR 0071](docs/adr/0071-engine-detour-failure-seat-fallback-declaration.md)。本文只投影该契约。
44
44
 
45
45
  ## 调用百官
46
46
 
@@ -1,7 +1,6 @@
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";
5
4
  /** Zero-projection kickoff — soul already carries dossier-fetch duty; no hand-delivered materials. */
6
5
  export const AUDITOR_DOSSIER_PROMPT = "Audit the current run dossier.";
7
6
  const nonblank = Type.String({ minLength: 1, pattern: "\\S" });
@@ -62,28 +61,24 @@ export function readComplianceCandidate(arguments_, usage) {
62
61
  return { status: "audit-incomplete", observation: { kind: "object-status-unreadable", status: status === undefined ? "missing" : "unknown" }, candidate: arguments_, ...(usage === undefined ? {} : { usage }) };
63
62
  }
64
63
  export async function runComplianceAudit(options) {
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);
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 }),
88
75
  });
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);
89
84
  }
@@ -1,7 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { ENGINE_DETOUR_ALREADY_USED_DIAGNOSTIC, ENGINE_DETOUR_TOOL_NAME, engineDetourFailureDiagnostic, engineNameFromEnv, isEngineDetourFailure, runEngineDetourOnce, } from "./engine-detour.js";
3
3
  import { activationEngineLaborFallbackLatch, recordEngineLaborFallback, } from "./engine-labor-fallback.js";
4
- import { isPackageOwnedToolIdleTimeoutError, pokePackageOwnedToolIdle, wrapPackageOwnedToolDefinition, } from "./package-owned-tool-idle.js";
5
4
  const engineDetourArgsSchema = Type.Object({
6
5
  argv: Type.Array(Type.String({ minLength: 1 }), {
7
6
  minItems: 1,
@@ -23,14 +22,8 @@ function seatFallbackToolResult(field, failure) {
23
22
  },
24
23
  };
25
24
  }
26
- /** Caller/upper-layer cancel must propagate; idle backstop is seat-fallback, not cancel. */
25
+ /** Caller/upper-layer cancel must propagate; process failure is seat-fallback, not cancel. */
27
26
  function isCallerCancellation(error, signal) {
28
- if (isPackageOwnedToolIdleTimeoutError(error))
29
- return false;
30
- if (signal !== undefined &&
31
- isPackageOwnedToolIdleTimeoutError(signal.reason)) {
32
- return false;
33
- }
34
27
  if (signal?.aborted === true)
35
28
  return true;
36
29
  if (typeof error === "object" &&
@@ -44,13 +37,13 @@ function isCallerCancellation(error, signal) {
44
37
  * Build one once-latch detour tool definition for a configured engine name.
45
38
  * `latch` is shared so parent registration can reset between activations.
46
39
  * `fail` owns host abort (parent) vs throw (evidence child) for tool misuse only.
47
- * Engine process failure (nonzero/empty/spawn/idle-timeout) soft-returns seat fallback (#380).
40
+ * Engine process failure (nonzero/empty/spawn) soft-returns seat fallback (#380).
48
41
  * Caller AbortSignal cancel propagates without writing fallback.
49
42
  */
50
43
  export function createEngineDetourToolDefinition(input) {
51
44
  const latch = input.latch ?? { used: false };
52
45
  const engineName = input.engineName;
53
- return wrapPackageOwnedToolDefinition({
46
+ return {
54
47
  name: ENGINE_DETOUR_TOOL_NAME,
55
48
  label: "Engine Detour",
56
49
  description: `Run one labor-engine subprocess (engine=${engineName}) and return its stdout to this session. Call at most once per activation. Build argv from the host CLI actual interface for this engine name; when optional packaged notes are present in the session prompt, follow those bytes too.`,
@@ -83,17 +76,14 @@ export function createEngineDetourToolDefinition(input) {
83
76
  };
84
77
  let result;
85
78
  try {
86
- // Byte activity on stdout/stderr touches the outer package-owned idle clock
87
- // (183s silence law unchanged). True hangs still die; slow streaming engines live.
88
79
  result = await runEngineDetourOnce({
89
80
  argv,
90
81
  cwd: ctx.cwd,
91
82
  ...(signal === undefined ? {} : { signal }),
92
- onOutputActivity: pokePackageOwnedToolIdle,
93
83
  });
94
84
  }
95
85
  catch (error) {
96
- // Caller cancel: propagate. Idle backstop + spawn/engine failure: seat fallback.
86
+ // Caller cancel: propagate. Spawn/engine failure: seat fallback.
97
87
  if (isCallerCancellation(error, signal)) {
98
88
  throw error;
99
89
  }
@@ -111,7 +101,7 @@ export function createEngineDetourToolDefinition(input) {
111
101
  },
112
102
  };
113
103
  },
114
- });
104
+ };
115
105
  }
116
106
  /**
117
107
  * Register the engine-generic detour tool once for this process when any role has
@@ -48,8 +48,8 @@ export async function runEngineDetourOnce(input) {
48
48
  return await new Promise((resolve, reject) => {
49
49
  let settled = false;
50
50
  const signal = input.signal;
51
- // Own abort→kill explicitly so rejection preserves signal.reason (caller cancel
52
- // vs package-owned idle). Do not pass `signal` to spawn (Node replaces reason).
51
+ // Own abort→kill explicitly so rejection preserves signal.reason (caller cancel).
52
+ // Do not pass `signal` to spawn (Node replaces reason).
53
53
  const child = spawn(command, args, {
54
54
  cwd: input.cwd,
55
55
  env: input.env ?? process.env,
@@ -57,18 +57,11 @@ export async function runEngineDetourOnce(input) {
57
57
  });
58
58
  let stdout = "";
59
59
  let stderr = "";
60
- const noteActivity = (chunk) => {
61
- if (chunk.length === 0)
62
- return;
63
- input.onOutputActivity?.();
64
- };
65
60
  child.stdout.setEncoding("utf8").on("data", (chunk) => {
66
61
  stdout += chunk;
67
- noteActivity(chunk);
68
62
  });
69
63
  child.stderr.setEncoding("utf8").on("data", (chunk) => {
70
64
  stderr += chunk;
71
- noteActivity(chunk);
72
65
  });
73
66
  const fail = (error) => {
74
67
  if (settled)
@@ -89,8 +82,7 @@ export async function runEngineDetourOnce(input) {
89
82
  resolve(result);
90
83
  };
91
84
  const onAbort = () => {
92
- // Fail synchronously so cooperative idle/cancel paths can soft-settle
93
- // before the outer package-owned idle hard-reject drain.
85
+ // Fail synchronously so caller-cancel soft-settle preserves signal.reason.
94
86
  fail(signal !== undefined ? abortReasonError(signal) : new Error("aborted"));
95
87
  try {
96
88
  child.kill("SIGTERM");
@@ -11,7 +11,6 @@ import { AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, AUDITOR_PARENT_ATTEMPT_BINDING_E
11
11
  import { createEngineDetourToolDefinition } from "./engine-detour-tool.js";
12
12
  import { engineNameFromEnv } from "./engine-detour.js";
13
13
  import { appendEngineSessionMaterial, engineSessionMaterialFromOptions, } from "./package-resources/engine-material.js";
14
- import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.js";
15
14
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.js";
16
15
  import { REVIEWER_VERIFICATION_BOUNDARY } from "./reviewer-construction.js";
17
16
  import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.js";
@@ -599,7 +598,7 @@ export async function executeAuditorChild(options) {
599
598
  let decisionToolFailure;
600
599
  const decisionToolFailures = new Map();
601
600
  const delivery = createReceiptDeliveryPolicy();
602
- const tool = wrapPackageOwnedToolDefinition({
601
+ const tool = {
603
602
  ...options.tool,
604
603
  label: options.roleLabel,
605
604
  async execute(...args) {
@@ -625,7 +624,7 @@ export async function executeAuditorChild(options) {
625
624
  throw error;
626
625
  }
627
626
  },
628
- });
627
+ };
629
628
  const parentSessionManager = options.context.sessionManager;
630
629
  const parentHeader = parentSessionManager?.getHeader?.();
631
630
  const parentSessionFile = parentSessionManager?.getSessionFile?.();
@@ -644,7 +643,7 @@ export async function executeAuditorChild(options) {
644
643
  thinkingLevel: options.context.thinkingLevel ?? "off",
645
644
  modelRuntime: inherited.runtime,
646
645
  systemPrompt: options.systemPrompt,
647
- customTools: [wrapPackageOwnedToolDefinition({ ...options.dossierTool, label: options.roleLabel }), tool],
646
+ customTools: [{ ...options.dossierTool, label: options.roleLabel }, tool],
648
647
  sessionManager: auditorSessionManager,
649
648
  });
650
649
  const binding = {
@@ -14,7 +14,6 @@ import { PACKAGED_ROLE_REGISTRY, packagedRoleMetadata } from "./packaged-role-re
14
14
  import { openInProcessAgentSession } from "./in-process-session.js";
15
15
  import { renderPublicAkRoleCommand } from "./public-command-renderer.js";
16
16
  import { issueRoot, subjectPath } from "./work-subject-identity.js";
17
- import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.js";
18
17
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.js";
19
18
  import { recordTypedProviderHttpStatus } from "./typed-provider-http.js";
20
19
  import {
@@ -247,7 +246,7 @@ function parseNavigatorModelSetting(value) {
247
246
  return { provider, model, thinkingLevel: suffix === "max" ? "max" : "off" };
248
247
  }
249
248
  function createNavigatorPrepareTool(onOutput) {
250
- return wrapPackageOwnedToolDefinition({
249
+ return {
251
250
  name: NAVIGATOR_PREPARE_TOOL_NAME,
252
251
  label: "Navigator preparation",
253
252
  description: "Submit Navigator direction advice. Provide candidates with next.role (phase when meaningful). route/matches/reason/command are optional context, not acceptance gates.",
@@ -256,7 +255,7 @@ function createNavigatorPrepareTool(onOutput) {
256
255
  onOutput(value);
257
256
  return { content: [{ type: "text", text: "Navigator preparation accepted" }], details: value, terminate: true };
258
257
  }
259
- });
258
+ };
260
259
  }
261
260
  function selectNavigatorCandidate(candidates, settlement) {
262
261
  if (settlement.kind !== "accepted") return void 0;
@@ -19302,64 +19302,6 @@ var init_engine_labor_fallback = __esm({
19302
19302
  }
19303
19303
  });
19304
19304
 
19305
- // src/stream-idle-guard.ts
19306
- var init_stream_idle_guard = __esm({
19307
- "src/stream-idle-guard.ts"() {
19308
- "use strict";
19309
- }
19310
- });
19311
-
19312
- // src/stderr-jsonl.ts
19313
- var init_stderr_jsonl = __esm({
19314
- "src/stderr-jsonl.ts"() {
19315
- "use strict";
19316
- }
19317
- });
19318
-
19319
- // src/tool-execution-observation.ts
19320
- var observationBase, toolExecutionObservationRecordSchema;
19321
- var init_tool_execution_observation = __esm({
19322
- "src/tool-execution-observation.ts"() {
19323
- "use strict";
19324
- init_build();
19325
- init_value2();
19326
- init_stderr_jsonl();
19327
- observationBase = {
19328
- role: typebox_exports.String({ minLength: 1 }),
19329
- toolCallId: typebox_exports.String({ minLength: 1 }),
19330
- toolName: typebox_exports.String({ minLength: 1 }),
19331
- timestamp: typebox_exports.String({ format: "date-time" })
19332
- };
19333
- toolExecutionObservationRecordSchema = typebox_exports.Union([
19334
- typebox_exports.Object({
19335
- ...observationBase,
19336
- event: typebox_exports.Literal("tool_execution_start")
19337
- }, { additionalProperties: true }),
19338
- typebox_exports.Object({
19339
- ...observationBase,
19340
- event: typebox_exports.Literal("tool_execution_update")
19341
- }, { additionalProperties: true }),
19342
- typebox_exports.Object({
19343
- ...observationBase,
19344
- event: typebox_exports.Literal("tool_execution_end"),
19345
- isError: typebox_exports.Boolean()
19346
- }, { additionalProperties: true })
19347
- ]);
19348
- }
19349
- });
19350
-
19351
- // src/package-owned-tool-idle.ts
19352
- import { AsyncLocalStorage } from "node:async_hooks";
19353
- var packageOwnedToolIdleScope;
19354
- var init_package_owned_tool_idle = __esm({
19355
- "src/package-owned-tool-idle.ts"() {
19356
- "use strict";
19357
- init_stream_idle_guard();
19358
- init_tool_execution_observation();
19359
- packageOwnedToolIdleScope = new AsyncLocalStorage();
19360
- }
19361
- });
19362
-
19363
19305
  // src/engine-detour-tool.ts
19364
19306
  var engineDetourArgsSchema;
19365
19307
  var init_engine_detour_tool = __esm({
@@ -19368,7 +19310,6 @@ var init_engine_detour_tool = __esm({
19368
19310
  init_build();
19369
19311
  init_engine_detour();
19370
19312
  init_engine_labor_fallback();
19371
- init_package_owned_tool_idle();
19372
19313
  engineDetourArgsSchema = typebox_exports.Object(
19373
19314
  {
19374
19315
  argv: typebox_exports.Array(typebox_exports.String({ minLength: 1 }), {
@@ -19411,6 +19352,13 @@ var init_receipt_delivery_policy = __esm({
19411
19352
  }
19412
19353
  });
19413
19354
 
19355
+ // src/stream-idle-guard.ts
19356
+ var init_stream_idle_guard = __esm({
19357
+ "src/stream-idle-guard.ts"() {
19358
+ "use strict";
19359
+ }
19360
+ });
19361
+
19414
19362
  // src/evidence-child-executor.ts
19415
19363
  var init_evidence_child_executor = __esm({
19416
19364
  "src/evidence-child-executor.ts"() {
@@ -19419,7 +19367,6 @@ var init_evidence_child_executor = __esm({
19419
19367
  init_engine_detour_tool();
19420
19368
  init_engine_detour();
19421
19369
  init_engine_material();
19422
- init_package_owned_tool_idle();
19423
19370
  init_receipt_delivery_policy();
19424
19371
  init_reviewer_construction();
19425
19372
  init_stream_idle_guard();
@@ -19452,7 +19399,6 @@ var init_compliance_transport = __esm({
19452
19399
  init_build();
19453
19400
  init_evidence_child_executor();
19454
19401
  init_auditor_dossier_tool();
19455
- init_package_owned_tool_idle();
19456
19402
  nonblank2 = typebox_exports.String({ minLength: 1, pattern: "\\S" });
19457
19403
  decisionGateSchema = typebox_exports.Object({ question: nonblank2, options: typebox_exports.Array(nonblank2, { minItems: 1 }) }, { additionalProperties: false });
19458
19404
  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.2135",
3
+ "version": "0.1.2139",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -33,10 +33,9 @@ codex exec --skip-git-repo-check --ephemeral -C "$PROJECT_ROOT" "YOUR_LABOR_PROM
33
33
 
34
34
  Default (non-`--json`) mode prints the session banner and progress on stderr
35
35
  (measured on this host). Collect the labor body from stdout so that stderr log
36
- noise is not mixed into the returned body; the package idle clock can still see
37
- activity on both streams. Use `--json` only when the seat itself needs
38
- machine-readable event rows (those rows land on stdout as JSONL; measured event
39
- types include `thread.started`, `turn.started`, `item.completed`, `error`,
36
+ noise is not mixed into the returned body. Use `--json` only when the seat itself
37
+ needs machine-readable event rows (those rows land on stdout as JSONL; measured
38
+ event types include `thread.started`, `turn.started`, `item.completed`, `error`,
40
39
  `turn.failed`):
41
40
 
42
41
  ```bash
@@ -27,12 +27,11 @@ grok --prompt-file /path/to/labor-prompt.md -m grok-4.6 --always-approve --outpu
27
27
  - `--output-format plain` keeps stdout clean for capture — but it stays
28
28
  silent until the run finishes. **For labor longer than ~2 minutes use
29
29
  `--output-format streaming-json` instead**: it emits NDJSON events
30
- (thought/text deltas) continuously from the first second, which keeps the
31
- package idle watchdog fed (verified live 2026-08-21; a plain-format judge
32
- run was killed by the 183s idle timeout). Reconstruct the final answer by concatenating each NDJSON
33
- object's `data` where `type == "text"`, in stream order; `type == "end"`
34
- (stopReason end_turn) marks completion. Do not treat `thought` events as
35
- the answer (live-verified stream shape 2026-08-21).
30
+ (thought/text deltas) continuously from the first second, so long runs stay
31
+ observable instead of appearing hung. Reconstruct the final answer by
32
+ concatenating each NDJSON object's `data` where `type == "text"`, in stream
33
+ order; `type == "end"` (stopReason end_turn) marks completion. Do not treat
34
+ `thought` events as the answer (live-verified stream shape 2026-08-21).
36
35
  - **Always pass `--reasoning-effort <low|medium|high>`** matching the effort
37
36
  tier ordered in the labor mandate (verified live 2026-08-21: flag exists,
38
37
  alias `--effort`; a low-tier run completed correctly). If the mandate names
@@ -31,8 +31,8 @@ kimi -m <model-alias> -p "YOUR_LABOR_PROMPT"
31
31
  ```
32
32
 
33
33
  Use `--output-format stream-json` (choices measured on this host: `text`,
34
- `stream-json`; default is `text`) so the package idle clock can see subprocess
35
- activity while the engine works. Take the labor body from
34
+ `stream-json`; default is `text`) when long labor needs progressive observability
35
+ while the engine works. Take the labor body from
36
36
  `{"role":"assistant","content":...}` rows, not from `role:meta` rows:
37
37
 
38
38
  ```bash
@@ -33,8 +33,8 @@ claude -p --model opus --verbose --output-format=stream-json "YOUR_LABOR_PROMPT"
33
33
  ```
34
34
 
35
35
  Use `--output-format=stream-json` (choices measured on this host: `text`, `json`,
36
- `stream-json`) so the package idle clock can see subprocess activity while the
37
- engine works; take the labor body from the final `result` event's `result` field,
36
+ `stream-json`) when long labor needs progressive observability while the engine
37
+ works; take the labor body from the final `result` event's `result` field,
38
38
  not from intermediate stream rows.
39
39
 
40
40
  Prefer `claude --help` on the host over any remembered flag set. Do not wrap this
@@ -61,10 +61,6 @@ const requestSchema = collectorRequestArgsSchema;
61
61
  const waitSchema = collectorWaitArgsSchema;
62
62
  const outputSchema = collectorOutputArgsSchema;
63
63
 
64
- // Well below the fixed 183s package-tool silence clock while avoiding noisy UI
65
- // churn during Collector's legal five-minute wait.
66
- const COLLECTOR_WAIT_PROGRESS_INTERVAL_MS = 60_000;
67
-
68
64
  type RequestParams = Static<typeof requestSchema>;
69
65
  type WaitParams = Static<typeof waitSchema>;
70
66
  type OutputParams = Static<typeof outputSchema>;
@@ -377,40 +373,18 @@ export function createCollectorRoleRuntime(
377
373
  "Call ak_collector_wait with a positive durationMs; runtime caps each wait to five minutes and to remaining eligibility.",
378
374
  ],
379
375
  parameters: waitSchema,
380
- async execute(toolCallId, params: WaitParams, signal, onUpdate, ctx) {
376
+ async execute(toolCallId, params: WaitParams, signal, _onUpdate, ctx) {
381
377
  if (activation === undefined) {
382
378
  throw new Error("Collector is not activated");
383
379
  }
384
- const currentActivation = activation;
385
- let progressTimer: ReturnType<typeof setTimeout> | undefined;
386
380
  try {
387
- currentActivation.ledger.beginOperational(COLLECTOR_WAIT_TOOL, toolCallId);
388
- const waitStartedMono = currentActivation.clock.monoNow();
389
- let lastElapsedMs = 0;
390
- const scheduleProgress = (): void => {
391
- if (onUpdate === undefined) return;
392
- progressTimer = setTimeout(() => {
393
- const elapsedMs = Math.max(
394
- 0,
395
- Math.floor(currentActivation.clock.monoNow() - waitStartedMono),
396
- );
397
- if (elapsedMs > lastElapsedMs) {
398
- lastElapsedMs = elapsedMs;
399
- onUpdate({
400
- content: [],
401
- details: { kind: "collector_wait_progress", elapsedMs },
402
- });
403
- }
404
- scheduleProgress();
405
- }, COLLECTOR_WAIT_PROGRESS_INTERVAL_MS);
406
- };
407
- scheduleProgress();
408
- const details = await currentActivation.ledger.wait(
381
+ activation.ledger.beginOperational(COLLECTOR_WAIT_TOOL, toolCallId);
382
+ const details = await activation.ledger.wait(
409
383
  params,
410
- currentActivation.clock,
384
+ activation.clock,
411
385
  signal,
412
386
  );
413
- currentActivation.ledger.completeOperational(toolCallId);
387
+ activation.ledger.completeOperational(toolCallId);
414
388
  return {
415
389
  content: [{
416
390
  type: "text" as const,
@@ -420,8 +394,6 @@ export function createCollectorRoleRuntime(
420
394
  };
421
395
  } catch (error) {
422
396
  hostActions.failInfrastructure(error, ctx);
423
- } finally {
424
- if (progressTimer !== undefined) clearTimeout(progressTimer);
425
397
  }
426
398
  },
427
399
  });
@@ -7,7 +7,6 @@ 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";
11
10
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
12
11
 
13
12
  export type ComplianceCompletion = AuditorCompletion;
@@ -117,28 +116,24 @@ export type RunComplianceAuditOptions = {
117
116
  };
118
117
 
119
118
  export async function runComplianceAudit(options: RunComplianceAuditOptions): Promise<ComplianceDecision> {
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);
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 }),
143
130
  });
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);
144
139
  }
@@ -4,7 +4,7 @@
4
4
  * Evidence-child legs install the same definition via customTools (no spawn in role modules).
5
5
  * #380: engine process failure soft-returns so the seat rejoins the main road; declaration
6
6
  * is recorded once via engine-labor-fallback (no fail-closed reject-leg).
7
- * Caller AbortSignal cancel propagates without fallback; package-owned idle abort soft-fails.
7
+ * Caller AbortSignal cancel propagates without fallback.
8
8
  */
9
9
  import type {
10
10
  AgentToolResult,
@@ -27,11 +27,6 @@ import {
27
27
  recordEngineLaborFallback,
28
28
  type EngineLaborFallbackField,
29
29
  } from "./engine-labor-fallback.ts";
30
- import {
31
- isPackageOwnedToolIdleTimeoutError,
32
- pokePackageOwnedToolIdle,
33
- wrapPackageOwnedToolDefinition,
34
- } from "./package-owned-tool-idle.ts";
35
30
 
36
31
  const engineDetourArgsSchema = Type.Object(
37
32
  {
@@ -81,18 +76,11 @@ function seatFallbackToolResult(
81
76
  };
82
77
  }
83
78
 
84
- /** Caller/upper-layer cancel must propagate; idle backstop is seat-fallback, not cancel. */
79
+ /** Caller/upper-layer cancel must propagate; process failure is seat-fallback, not cancel. */
85
80
  function isCallerCancellation(
86
81
  error: unknown,
87
82
  signal: AbortSignal | undefined,
88
83
  ): boolean {
89
- if (isPackageOwnedToolIdleTimeoutError(error)) return false;
90
- if (
91
- signal !== undefined &&
92
- isPackageOwnedToolIdleTimeoutError(signal.reason)
93
- ) {
94
- return false;
95
- }
96
84
  if (signal?.aborted === true) return true;
97
85
  if (
98
86
  typeof error === "object" &&
@@ -108,7 +96,7 @@ function isCallerCancellation(
108
96
  * Build one once-latch detour tool definition for a configured engine name.
109
97
  * `latch` is shared so parent registration can reset between activations.
110
98
  * `fail` owns host abort (parent) vs throw (evidence child) for tool misuse only.
111
- * Engine process failure (nonzero/empty/spawn/idle-timeout) soft-returns seat fallback (#380).
99
+ * Engine process failure (nonzero/empty/spawn) soft-returns seat fallback (#380).
112
100
  * Caller AbortSignal cancel propagates without writing fallback.
113
101
  */
114
102
  export function createEngineDetourToolDefinition(input: {
@@ -118,7 +106,7 @@ export function createEngineDetourToolDefinition(input: {
118
106
  }): ToolDefinition {
119
107
  const latch = input.latch ?? { used: false };
120
108
  const engineName = input.engineName;
121
- return wrapPackageOwnedToolDefinition({
109
+ return {
122
110
  name: ENGINE_DETOUR_TOOL_NAME,
123
111
  label: "Engine Detour",
124
112
  description:
@@ -170,16 +158,13 @@ export function createEngineDetourToolDefinition(input: {
170
158
 
171
159
  let result: Awaited<ReturnType<typeof runEngineDetourOnce>>;
172
160
  try {
173
- // Byte activity on stdout/stderr touches the outer package-owned idle clock
174
- // (183s silence law unchanged). True hangs still die; slow streaming engines live.
175
161
  result = await runEngineDetourOnce({
176
162
  argv,
177
163
  cwd: ctx.cwd,
178
164
  ...(signal === undefined ? {} : { signal }),
179
- onOutputActivity: pokePackageOwnedToolIdle,
180
165
  });
181
166
  } catch (error) {
182
- // Caller cancel: propagate. Idle backstop + spawn/engine failure: seat fallback.
167
+ // Caller cancel: propagate. Spawn/engine failure: seat fallback.
183
168
  if (isCallerCancellation(error, signal)) {
184
169
  throw error;
185
170
  }
@@ -200,7 +185,7 @@ export function createEngineDetourToolDefinition(input: {
200
185
  },
201
186
  };
202
187
  },
203
- }) as ToolDefinition;
188
+ } as ToolDefinition;
204
189
  }
205
190
 
206
191
  /**
@@ -44,11 +44,6 @@ export type EngineDetourRunInput = Readonly<{
44
44
  cwd: string;
45
45
  env?: NodeJS.ProcessEnv;
46
46
  signal?: AbortSignal;
47
- /**
48
- * Called when the child emits at least one byte on stdout or stderr.
49
- * Host wires this to the package-owned idle clock (activity = touch); no timer here.
50
- */
51
- onOutputActivity?: () => void;
52
47
  }>;
53
48
 
54
49
  function abortReasonError(signal: AbortSignal): Error {
@@ -78,8 +73,8 @@ export async function runEngineDetourOnce(
78
73
  return await new Promise<EngineDetourResult>((resolve, reject) => {
79
74
  let settled = false;
80
75
  const signal = input.signal;
81
- // Own abort→kill explicitly so rejection preserves signal.reason (caller cancel
82
- // vs package-owned idle). Do not pass `signal` to spawn (Node replaces reason).
76
+ // Own abort→kill explicitly so rejection preserves signal.reason (caller cancel).
77
+ // Do not pass `signal` to spawn (Node replaces reason).
83
78
  const child = spawn(command, args, {
84
79
  cwd: input.cwd,
85
80
  env: input.env ?? process.env,
@@ -87,17 +82,11 @@ export async function runEngineDetourOnce(
87
82
  });
88
83
  let stdout = "";
89
84
  let stderr = "";
90
- const noteActivity = (chunk: string): void => {
91
- if (chunk.length === 0) return;
92
- input.onOutputActivity?.();
93
- };
94
85
  child.stdout.setEncoding("utf8").on("data", (chunk: string) => {
95
86
  stdout += chunk;
96
- noteActivity(chunk);
97
87
  });
98
88
  child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
99
89
  stderr += chunk;
100
- noteActivity(chunk);
101
90
  });
102
91
  const fail = (error: unknown): void => {
103
92
  if (settled) return;
@@ -116,8 +105,7 @@ export async function runEngineDetourOnce(
116
105
  resolve(result);
117
106
  };
118
107
  const onAbort = (): void => {
119
- // Fail synchronously so cooperative idle/cancel paths can soft-settle
120
- // before the outer package-owned idle hard-reject drain.
108
+ // Fail synchronously so caller-cancel soft-settle preserves signal.reason.
121
109
  fail(signal !== undefined ? abortReasonError(signal) : new Error("aborted"));
122
110
  try {
123
111
  child.kill("SIGTERM");
@@ -36,7 +36,6 @@ import {
36
36
  engineSessionMaterialFromOptions,
37
37
  type EngineSessionMaterial,
38
38
  } from "./package-resources/engine-material.ts";
39
- import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
40
39
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
41
40
  import { REVIEWER_VERIFICATION_BOUNDARY } from "./reviewer-construction.ts";
42
41
  import type { ReviewerPromptText } from "./reviewer-prompt-identity.ts";
@@ -750,7 +749,7 @@ export async function executeAuditorChild(
750
749
  let decisionToolFailure: unknown;
751
750
  const decisionToolFailures = new Map<string, unknown>();
752
751
  const delivery = createReceiptDeliveryPolicy();
753
- const tool = wrapPackageOwnedToolDefinition({
752
+ const tool = {
754
753
  ...options.tool,
755
754
  label: options.roleLabel,
756
755
  async execute(...args: any[]) {
@@ -775,7 +774,7 @@ export async function executeAuditorChild(
775
774
  throw error;
776
775
  }
777
776
  },
778
- });
777
+ };
779
778
 
780
779
  const parentSessionManager = options.context.sessionManager;
781
780
  const parentHeader = parentSessionManager?.getHeader?.();
@@ -796,7 +795,7 @@ export async function executeAuditorChild(
796
795
  thinkingLevel: options.context.thinkingLevel ?? "off",
797
796
  modelRuntime: inherited.runtime,
798
797
  systemPrompt: options.systemPrompt,
799
- customTools: [wrapPackageOwnedToolDefinition({ ...options.dossierTool, label: options.roleLabel }), tool],
798
+ customTools: [{ ...options.dossierTool, label: options.roleLabel }, tool],
800
799
  sessionManager: auditorSessionManager,
801
800
  });
802
801
 
@@ -16,7 +16,6 @@ import { PACKAGED_ROLE_REGISTRY, type PackagedRole, packagedRoleMetadata } from
16
16
  import { openInProcessAgentSession } from "./in-process-session.ts";
17
17
  import { renderPublicAkRoleCommand } from "./public-command-renderer.ts";
18
18
  import { issueRoot, subjectPath } from "./work-subject-identity.ts";
19
- import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
20
19
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
21
20
  import { recordTypedProviderHttpStatus } from "./typed-provider-http.ts";
22
21
  import {
@@ -437,7 +436,7 @@ export function parseNavigatorModelSetting(value: string): { provider: string; m
437
436
  }
438
437
 
439
438
  export function createNavigatorPrepareTool(onOutput: (value: PrepareOutput) => void): ToolDefinition {
440
- return wrapPackageOwnedToolDefinition({
439
+ return {
441
440
  name: NAVIGATOR_PREPARE_TOOL_NAME,
442
441
  label: "Navigator preparation",
443
442
  description: "Submit Navigator direction advice. Provide candidates with next.role (phase when meaningful). route/matches/reason/command are optional context, not acceptance gates.",
@@ -448,7 +447,7 @@ export function createNavigatorPrepareTool(onOutput: (value: PrepareOutput) => v
448
447
  onOutput(value as PrepareOutput);
449
448
  return { content: [{ type: "text" as const, text: "Navigator preparation accepted" }], details: value, terminate: true as const };
450
449
  },
451
- });
450
+ };
452
451
  }
453
452
 
454
453
  /**
@@ -28,7 +28,6 @@ import {
28
28
  installActivationEngineLaborFallbackLatch,
29
29
  restoreEngineLaborFallbackFromSessionEntries,
30
30
  } from "./engine-labor-fallback.ts";
31
- import { installPackageOwnedToolRegistration } from "./package-owned-tool-idle.ts";
32
31
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
33
32
  import { createOAuthKeepalive, type OAuthKeepaliveOptions } from "./oauth-keepalive.ts";
34
33
 
@@ -575,8 +574,6 @@ export function createRoleRuntimeExtension(
575
574
  dependencies: RoleRuntimeDependencies,
576
575
  ): (pi: ExtensionAPI) => void {
577
576
  return (pi) => {
578
- // #102: one shared package-owned tool registration surface for all role runtimes.
579
- installPackageOwnedToolRegistration(pi);
580
577
  pi.registerFlag(ROLE_FLAG.name, ROLE_FLAG.definition);
581
578
  // Reviewer transport flags: shared envelope owns registration (ADR 0018).
582
579
  for (const flag of REVIEWER_TRANSPORT_FLAGS) {
@@ -1,223 +0,0 @@
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, 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
- * #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).
17
- */
18
- import { AsyncLocalStorage } from "node:async_hooks";
19
- import { DEFAULT_STREAM_IDLE_TIMEOUT_MS, createStreamIdleGuard, } from "./stream-idle-guard.js";
20
- import { isProducingToolUpdate } from "./tool-execution-observation.js";
21
- export const PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS = DEFAULT_STREAM_IDLE_TIMEOUT_MS;
22
- export const PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE = "AK_PACKAGE_OWNED_TOOL_IDLE_TIMEOUT";
23
- const WRAPPED = Symbol.for("ak.packageOwnedToolIdleWrapped");
24
- /**
25
- * Active outer package-owned execute idle, if any. Nested tool executes install
26
- * their own store; compliance audit only suspends the store visible at await time.
27
- */
28
- const packageOwnedToolIdleScope = new AsyncLocalStorage();
29
- export class PackageOwnedToolIdleTimeoutError extends Error {
30
- code = PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE;
31
- idleTimeoutMs = PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS;
32
- constructor() {
33
- super(`package-owned tool idle timeout after ${PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS}ms`);
34
- this.name = "PackageOwnedToolIdleTimeoutError";
35
- }
36
- }
37
- export function isPackageOwnedToolIdleTimeoutError(value) {
38
- return (value instanceof PackageOwnedToolIdleTimeoutError ||
39
- (typeof value === "object" &&
40
- value !== null &&
41
- value.name === "PackageOwnedToolIdleTimeoutError" &&
42
- value.code === PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE));
43
- }
44
- /**
45
- * Package-tool activity includes content production and host-only details
46
- * progress. Keep the observation-plane oracle separate: its stderr heartbeat
47
- * contract remains content-driven. Pi's known execute-entry placeholder
48
- * (`content: [], details: undefined`) is not activity.
49
- */
50
- function isPackageOwnedToolActivityUpdate(partialResult) {
51
- if (isProducingToolUpdate(partialResult))
52
- return true;
53
- if (typeof partialResult !== "object" || partialResult === null)
54
- return false;
55
- const details = partialResult.details;
56
- if (details === undefined || details === null)
57
- return false;
58
- if (typeof details === "string")
59
- return details.length > 0;
60
- if (Array.isArray(details))
61
- return details.length > 0;
62
- if (typeof details === "object")
63
- return Reflect.ownKeys(details).length > 0;
64
- return true;
65
- }
66
- /**
67
- * #339: suspend the active package-owned execute idle for one real compliance
68
- * audit await. Single-layer only — production has one runComplianceAudit seam.
69
- * No-op outside a wrapped package-owned execute. No second timeout or retry.
70
- */
71
- export async function withPackageOwnedToolIdleSuspended(run) {
72
- const scope = packageOwnedToolIdleScope.getStore();
73
- if (scope === undefined)
74
- return run();
75
- scope.suspend();
76
- try {
77
- return await run();
78
- }
79
- finally {
80
- scope.resume();
81
- }
82
- }
83
- /**
84
- * Touch the active package-owned execute idle clock (reuse stream-idle-guard poke).
85
- * No-op outside a wrapped package-owned execute or while that layer is suspended.
86
- * Used by long-running tools (engine detour) that see real subprocess output bytes.
87
- */
88
- export function pokePackageOwnedToolIdle() {
89
- packageOwnedToolIdleScope.getStore()?.poke();
90
- }
91
- /**
92
- * Single shared execute wrapper for package-owned tool definitions.
93
- * Idempotent: wrapping twice returns the same protected definition.
94
- */
95
- export function wrapPackageOwnedToolDefinition(tool) {
96
- // Mark the execute function, not the tool object — callers may spread tool fields
97
- // onto a new definition with a different execute (e.g. auditor customTools).
98
- if (tool.execute[WRAPPED] === true)
99
- return tool;
100
- const originalExecute = tool.execute.bind(tool);
101
- const wrappedExecute = function packageOwnedToolIdleExecute(...args) {
102
- const parentSignal = args[2];
103
- const onUpdate = args[3];
104
- return new Promise((resolve, reject) => {
105
- let settled = false;
106
- let suspended = false;
107
- let idle = createStreamIdleGuard({
108
- idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
109
- });
110
- // Derived tool signal: caller cancel forwards; idle aborts this only (not parent).
111
- const toolController = new AbortController();
112
- const onParentAbort = () => {
113
- if (toolController.signal.aborted)
114
- return;
115
- toolController.abort(parentSignal?.reason);
116
- };
117
- if (parentSignal !== undefined) {
118
- if (parentSignal.aborted) {
119
- toolController.abort(parentSignal.reason);
120
- }
121
- else {
122
- parentSignal.addEventListener("abort", onParentAbort);
123
- }
124
- }
125
- const cleanup = () => {
126
- idle.signal.removeEventListener("abort", onIdle);
127
- idle.dispose();
128
- parentSignal?.removeEventListener("abort", onParentAbort);
129
- };
130
- const settle = (deliver) => {
131
- if (settled)
132
- return;
133
- settled = true;
134
- cleanup();
135
- deliver();
136
- };
137
- const onIdle = () => {
138
- const idleError = new PackageOwnedToolIdleTimeoutError();
139
- if (!toolController.signal.aborted) {
140
- toolController.abort(idleError);
141
- }
142
- // Cooperative tools (detour) soft-settle via the aborted signal in the
143
- // next microtasks. Non-listeners still hard-reject after a short drain
144
- // of those microtasks — no second wall-clock timer.
145
- void Promise.resolve()
146
- .then(() => { })
147
- .then(() => { })
148
- .then(() => {
149
- settle(() => reject(idleError));
150
- });
151
- };
152
- idle.signal.addEventListener("abort", onIdle, { once: true });
153
- const suspension = {
154
- suspend() {
155
- if (settled || suspended)
156
- return;
157
- suspended = true;
158
- // ADR 0059 owns the audit interval — drop this layer until audit returns.
159
- idle.signal.removeEventListener("abort", onIdle);
160
- idle.dispose();
161
- },
162
- resume() {
163
- if (settled || !suspended)
164
- return;
165
- suspended = false;
166
- // Fresh single-layer silence window for post-audit work (e.g. cleanup).
167
- idle = createStreamIdleGuard({
168
- idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
169
- });
170
- idle.signal.addEventListener("abort", onIdle, { once: true });
171
- },
172
- poke() {
173
- if (settled || suspended)
174
- return;
175
- idle.poke();
176
- },
177
- };
178
- const guardedOnUpdate = onUpdate === undefined
179
- ? undefined
180
- : (partialResult) => {
181
- if (settled)
182
- return;
183
- if (isPackageOwnedToolActivityUpdate(partialResult))
184
- idle.poke();
185
- onUpdate(partialResult);
186
- };
187
- const callArgs = args.slice();
188
- // Pass derived signal so idle/cancel can clean up subprocesses without
189
- // aborting the caller/parent agent signal identity.
190
- callArgs[2] = toolController.signal;
191
- callArgs[3] = guardedOnUpdate;
192
- void packageOwnedToolIdleScope.run(suspension, async () => {
193
- try {
194
- const result = await originalExecute(...callArgs);
195
- settle(() => resolve(result));
196
- }
197
- catch (error) {
198
- settle(() => reject(error));
199
- }
200
- });
201
- });
202
- };
203
- wrappedExecute[WRAPPED] = true;
204
- return {
205
- ...tool,
206
- execute: wrappedExecute,
207
- };
208
- }
209
- /**
210
- * Install the shared registration surface on an ExtensionAPI once.
211
- * All subsequent pi.registerTool calls for package-owned tools are wrapped.
212
- */
213
- export function installPackageOwnedToolRegistration(pi) {
214
- const current = pi.registerTool;
215
- if (current[WRAPPED] === true)
216
- return;
217
- const original = current.bind(pi);
218
- const installed = ((tool) => {
219
- original(wrapPackageOwnedToolDefinition(tool));
220
- });
221
- installed[WRAPPED] = true;
222
- pi.registerTool = installed;
223
- }
@@ -1,257 +0,0 @@
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, 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
- * #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).
17
- */
18
- import { AsyncLocalStorage } from "node:async_hooks";
19
- import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
20
-
21
- import {
22
- DEFAULT_STREAM_IDLE_TIMEOUT_MS,
23
- createStreamIdleGuard,
24
- type StreamIdleGuard,
25
- } from "./stream-idle-guard.ts";
26
- import { isProducingToolUpdate } from "./tool-execution-observation.ts";
27
-
28
- export const PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS = DEFAULT_STREAM_IDLE_TIMEOUT_MS;
29
- export const PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE = "AK_PACKAGE_OWNED_TOOL_IDLE_TIMEOUT" as const;
30
-
31
- const WRAPPED = Symbol.for("ak.packageOwnedToolIdleWrapped");
32
-
33
- type PackageOwnedToolIdleSuspension = {
34
- suspend(): void;
35
- resume(): void;
36
- /** Reset the silence window (byte/activity signal from the active execute). */
37
- poke(): void;
38
- };
39
-
40
- /**
41
- * Active outer package-owned execute idle, if any. Nested tool executes install
42
- * their own store; compliance audit only suspends the store visible at await time.
43
- */
44
- const packageOwnedToolIdleScope = new AsyncLocalStorage<PackageOwnedToolIdleSuspension>();
45
-
46
- export class PackageOwnedToolIdleTimeoutError extends Error {
47
- readonly code = PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE;
48
- readonly idleTimeoutMs = PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS;
49
-
50
- constructor() {
51
- super(`package-owned tool idle timeout after ${PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS}ms`);
52
- this.name = "PackageOwnedToolIdleTimeoutError";
53
- }
54
- }
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
-
70
- /** Minimal executable tool shape accepted by the shared idle wrapper. */
71
- export type PackageOwnedToolLike = {
72
- readonly name: string;
73
- execute: (...args: never[]) => Promise<unknown>;
74
- };
75
-
76
- /**
77
- * Package-tool activity includes content production and host-only details
78
- * progress. Keep the observation-plane oracle separate: its stderr heartbeat
79
- * contract remains content-driven. Pi's known execute-entry placeholder
80
- * (`content: [], details: undefined`) is not activity.
81
- */
82
- function isPackageOwnedToolActivityUpdate(partialResult: unknown): boolean {
83
- if (isProducingToolUpdate(partialResult)) return true;
84
- if (typeof partialResult !== "object" || partialResult === null) return false;
85
- const details = (partialResult as { details?: unknown }).details;
86
- if (details === undefined || details === null) return false;
87
- if (typeof details === "string") return details.length > 0;
88
- if (Array.isArray(details)) return details.length > 0;
89
- if (typeof details === "object") return Reflect.ownKeys(details).length > 0;
90
- return true;
91
- }
92
-
93
- /**
94
- * #339: suspend the active package-owned execute idle for one real compliance
95
- * audit await. Single-layer only — production has one runComplianceAudit seam.
96
- * No-op outside a wrapped package-owned execute. No second timeout or retry.
97
- */
98
- export async function withPackageOwnedToolIdleSuspended<T>(
99
- run: () => Promise<T>,
100
- ): Promise<T> {
101
- const scope = packageOwnedToolIdleScope.getStore();
102
- if (scope === undefined) return run();
103
- scope.suspend();
104
- try {
105
- return await run();
106
- } finally {
107
- scope.resume();
108
- }
109
- }
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
-
120
- /**
121
- * Single shared execute wrapper for package-owned tool definitions.
122
- * Idempotent: wrapping twice returns the same protected definition.
123
- */
124
- export function wrapPackageOwnedToolDefinition<T extends PackageOwnedToolLike>(tool: T): T {
125
- // Mark the execute function, not the tool object — callers may spread tool fields
126
- // onto a new definition with a different execute (e.g. auditor customTools).
127
- if ((tool.execute as { [WRAPPED]?: boolean })[WRAPPED] === true) return tool;
128
-
129
- const originalExecute = tool.execute.bind(tool) as (
130
- ...args: unknown[]
131
- ) => Promise<unknown>;
132
-
133
- const wrappedExecute = function packageOwnedToolIdleExecute(
134
- ...args: unknown[]
135
- ): Promise<unknown> {
136
- const parentSignal = args[2] as AbortSignal | undefined;
137
- const onUpdate = args[3] as ((partialResult: unknown) => void) | undefined;
138
- return new Promise((resolve, reject) => {
139
- let settled = false;
140
- let suspended = false;
141
- let idle: StreamIdleGuard = createStreamIdleGuard({
142
- idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
143
- });
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
-
165
- const settle = (deliver: () => void): void => {
166
- if (settled) return;
167
- settled = true;
168
- cleanup();
169
- deliver();
170
- };
171
- const onIdle = (): void => {
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
- });
185
- };
186
- idle.signal.addEventListener("abort", onIdle, { once: true });
187
-
188
- const suspension: PackageOwnedToolIdleSuspension = {
189
- suspend(): void {
190
- if (settled || suspended) return;
191
- suspended = true;
192
- // ADR 0059 owns the audit interval — drop this layer until audit returns.
193
- idle.signal.removeEventListener("abort", onIdle);
194
- idle.dispose();
195
- },
196
- resume(): void {
197
- if (settled || !suspended) return;
198
- suspended = false;
199
- // Fresh single-layer silence window for post-audit work (e.g. cleanup).
200
- idle = createStreamIdleGuard({
201
- idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
202
- });
203
- idle.signal.addEventListener("abort", onIdle, { once: true });
204
- },
205
- poke(): void {
206
- if (settled || suspended) return;
207
- idle.poke();
208
- },
209
- };
210
-
211
- const guardedOnUpdate = onUpdate === undefined
212
- ? undefined
213
- : (partialResult: unknown) => {
214
- if (settled) return;
215
- if (isPackageOwnedToolActivityUpdate(partialResult)) idle.poke();
216
- onUpdate(partialResult);
217
- };
218
-
219
- const callArgs = args.slice();
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;
223
- callArgs[3] = guardedOnUpdate;
224
-
225
- void packageOwnedToolIdleScope.run(suspension, async () => {
226
- try {
227
- const result = await originalExecute(...callArgs);
228
- settle(() => resolve(result));
229
- } catch (error) {
230
- settle(() => reject(error));
231
- }
232
- });
233
- });
234
- };
235
- (wrappedExecute as { [WRAPPED]?: boolean })[WRAPPED] = true;
236
-
237
- return {
238
- ...tool,
239
- execute: wrappedExecute as T["execute"],
240
- };
241
- }
242
-
243
- /**
244
- * Install the shared registration surface on an ExtensionAPI once.
245
- * All subsequent pi.registerTool calls for package-owned tools are wrapped.
246
- */
247
- export function installPackageOwnedToolRegistration(pi: ExtensionAPI): void {
248
- const current = pi.registerTool;
249
- if ((current as { [WRAPPED]?: boolean })[WRAPPED] === true) return;
250
-
251
- const original = current.bind(pi);
252
- const installed = ((tool: ToolDefinition<any, any, any>) => {
253
- original(wrapPackageOwnedToolDefinition(tool) as ToolDefinition<any, any, any>);
254
- }) as typeof pi.registerTool;
255
- (installed as { [WRAPPED]?: boolean })[WRAPPED] = true;
256
- pi.registerTool = installed;
257
- }