pi-orchestrator-config 4.24.1 → 4.25.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.25.0] - 2026-11-06
4
+
5
+ - `.pi/tmp` is cleaned at session start: above `project_tmp_max_size` (default `2G`), entries older than
6
+ `project_tmp_retention_days` (default 7 days) are deleted oldest-first until the tree fits, and `0`
7
+ disables it entirely. The sweep lock lives in `.pi/run` rather than in the directory being swept, a run
8
+ that cannot take the lock deletes nothing, an entry replaced mid-scan is skipped, and oneshot sessions
9
+ skip the whole thing. A bad size value deletes nothing rather than falling back to a deleting default.
10
+ - Daemon control files moved to `.pi/run` — `pidiff.pid`, `pidiff.port`, and the `reload.flag` /
11
+ `reload.done` / `reload.pending` handshake. Readers fall back to `.pi/tmp` and `reload.done` is mirrored
12
+ back, so a daemon started before the upgrade is still found instead of a second one spawning on its
13
+ port. **The migration release needs exactly one manual `/reload`**: a session whose watcher was loaded
14
+ before the move cannot observe the new flag path. The old temp-dir reload files stay protected until no
15
+ pre-upgrade daemon remains.
16
+ - The pending-task reminder keys on how long a task has actually been pending rather than on the first
17
+ 60-second tick, so it no longer fires about a minute after creation. It measures the oldest active task,
18
+ treats an undated task as old so a fresh sibling cannot mask it, and covers `agentBusy` in tests.
19
+ - `reviews fetch` approves on a clean findings catalog at the PR head instead of requiring a `completed`
20
+ Qodo session, so a legitimately skipped auto-run can approve. A run that is still in progress never does.
21
+ `reason` reports `no_findings`, `open_findings`, `not_at_head`, or `all_resolved` instead of always
22
+ claiming `all_resolved`, and the review-loop exit names which reviewer actually approved — a CodeRabbit
23
+ approval is no longer reported as Qodo's.
24
+
25
+ ## [4.24.2] - 2026-11-04
26
+
27
+ - Async work is tracked even when an agent starts it: the task trigger now covers work set in
28
+ motion by an async result, and `taskId: "-1"` is restricted to a single read-only check in
29
+ every place that taught the old habit.
30
+ - Task-id validation actually runs. `pitasks` exports the store as a live binding that the
31
+ subagent tool used to snapshot before it was assigned, so an unknown id was accepted; the
32
+ store is now published on a symbol bridge and read at access time. Verified live: unknown
33
+ ids rejected, a real id and the read-only sentinel accepted.
34
+ - Machine-generated notices declare their provenance: follow-ups are prefixed
35
+ "[system message — not written by the user]", so a guard rail cannot be read as user input.
36
+ - Board updates are success-gated — a failed agent keeps its task open — and reconciling the
37
+ board before new dispatch is mandatory. Status reports derive from `TaskList`.
38
+
3
39
  ## [4.24.1] - 2026-11-03
4
40
 
5
41
  - Dreaming no longer stores re-derivable changelog: `[done]` takes non-derivable outcomes only (conclusions, measurements, root causes) and reorganizing prunes PR/version/ticket
@@ -22,6 +22,7 @@ import { registerEnforcement } from "./enforcement.js";
22
22
  import { registerProjectSettings } from "./project-settings.js";
23
23
  import { registerRules } from "./rules.js";
24
24
  import { registerSessionValidation } from "./session-validation.js";
25
+ import { registerTmpCleanup } from "./tmp-cleanup.js";
25
26
  import { registerStatusLine } from "./status-line.js";
26
27
  import { registerSubagentTool } from "./subagent-tool.js";
27
28
  import { registerGithubAutocomplete } from "./github-autocomplete.js";
@@ -144,6 +145,7 @@ export default function (pi: ExtensionAPI) {
144
145
  registerPreferenceExtractor(pi);
145
146
  registerMemoryTools(pi);
146
147
  registerReviewLearning(pi);
148
+ registerTmpCleanup(pi);
147
149
  registerSessionSearch(pi);
148
150
  registerSettingsTui(pi);
149
151
  registerMarkdownTransformer(pi);
@@ -16,6 +16,8 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
16
16
  import type { Message } from "@earendil-works/pi-ai";
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
18
  import { createLogger } from "../shared/logger.js";
19
+ import { sendSystemNote } from "../shared/system-note.js";
20
+ import { publishedTaskStore } from "../shared/task-store-bridge.js";
19
21
  import { createBridgedUi } from "../shared/ui-bridge.js";
20
22
  import {
21
23
  getMarkdownTheme,
@@ -30,12 +32,44 @@ import {
30
32
  import { Type } from "typebox";
31
33
  import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
32
34
 
33
- // Task store access via shared instance from extensions/pitasks
34
- let _taskStore: any = null;
35
+ // Task store access via the pitasks module.
36
+ // `taskStore` is `export let` — a live binding the pitasks extension assigns when it
37
+ // initializes. Copying the value at import time captured `undefined` and disabled
38
+ // task-id validation for the whole session (issue #1016), so the binding is read fresh.
39
+ let _pitasksModule: any = null;
40
+ let _pitasksLoad: Promise<any> | null = null;
41
+ async function pitasksModule(): Promise<any> {
42
+ if (_pitasksModule) return _pitasksModule;
43
+ if (!_pitasksLoad) {
44
+ _pitasksLoad = (async () => {
45
+ try {
46
+ _pitasksModule = await import("../pitasks/index.js");
47
+ return _pitasksModule;
48
+ } catch (err) {
49
+ log.warn(`[subagent] pitasks module unavailable: ${String((err as Error)?.message ?? err).slice(0, 120)}`);
50
+ _pitasksLoad = null; // retry on the next call
51
+ return null;
52
+ }
53
+ })();
54
+ }
55
+ return _pitasksLoad;
56
+ }
57
+
58
+ /** The live task store, or null while pitasks has not initialized it yet.
59
+ * Read from the global symbol registry: this tool's module instance cannot see the
60
+ * pitasks extension's `export let taskStore` binding (it snapshots `undefined`), which
61
+ * is why task-id validation silently failed open (issue #1016). */
35
62
  async function getTaskStore(): Promise<any> {
36
- if (_taskStore) return _taskStore;
37
- try { const mod = await import("../pitasks/index.js"); _taskStore = mod.taskStore; } catch { /* ignore */ }
38
- return _taskStore;
63
+ const published = publishedTaskStore();
64
+ if (published) return published;
65
+ // Fall back to the module binding for in-process loads that share this instance.
66
+ const mod = await pitasksModule();
67
+ const store = mod?.taskStore ?? null;
68
+ if (!store) {
69
+ // Validation is about to be skipped: that is a degraded state, not routine flow.
70
+ log.warn("task_store_unavailable", { note: "pitasks store not initialized — taskId validation skipped" });
71
+ }
72
+ return store;
39
73
  }
40
74
  import {
41
75
  decideAsyncLlmDispatch,
@@ -153,7 +187,7 @@ const SubagentParams = Type.Object({
153
187
  Type.String({ description: "Display name for async agents in status line and notifications (e.g., 'Dream', 'Code Review'). Defaults to agent name." }),
154
188
  ),
155
189
  taskId: Type.Optional(
156
- Type.String({ description: "Task ID to auto-complete when this async agent finishes. Required for async agents — pass \"-1\" if not linked to any task." }),
190
+ Type.String({ description: "Task ID to auto-complete when this async agent finishes. Required for async agents — pass \"-1\" only for a single read-only check; an agent that edits, pushes, or drives a PR needs a real task id." }),
157
191
  ),
158
192
  asyncKill: Type.Optional(
159
193
  Type.String({ description: "Kill async agent(s) by name, id prefix, or 'all'. Returns which agents were killed." }),
@@ -577,22 +611,27 @@ export async function runSingleAgent(
577
611
  }
578
612
  }
579
613
 
580
- /** Validate that a taskId references an existing task in the pitasks store. */
581
- function validateTaskId(taskId: string, _cwd: string, _sessionId?: string): string | null {
614
+ /** The message for a task id the store does not know; null when it is valid or exempt. */
615
+ export function taskIdError(taskId: string, lookup: (id: string) => boolean): string | null {
616
+ if (taskId === "-1") return null;
617
+ return lookup(taskId)
618
+ ? null
619
+ : `Task #${taskId} not found. Verify the task ID exists (use TaskList to check), or pass taskId: "-1" for a read-only check (editing or PR work needs a real task id).`;
620
+ }
621
+
622
+ /** Validate that a taskId references an existing task. Awaits the store load, so the
623
+ * first async call of a session validates too instead of failing open (issue #1016). */
624
+ async function validateTaskId(taskId: string, _cwd: string, _sessionId?: string): Promise<string | null> {
582
625
  if (taskId === "-1") return null;
626
+ const store = await getTaskStore();
627
+ if (!store) return null; // pitasks genuinely unavailable — warned in getTaskStore
583
628
  try {
584
- // Ensure store is loaded (async init may not have completed)
585
- if (!_taskStore) {
586
- getTaskStore().catch(() => {});
587
- }
588
- const store = _taskStore;
589
- if (!store) {
590
- // Store not loaded yet (async init) — fail open, skip validation
591
- return null;
592
- }
593
- if (store.get(taskId)) return null;
594
- } catch { return null; }
595
- return `Task #${taskId} not found. Verify the task ID exists (use TaskList to check), or pass taskId: "-1" if not linked to a task.`;
629
+ return taskIdError(taskId, (id) => Boolean(store.get(id)));
630
+ } catch (err) {
631
+ // Same fail-open as before, but a broken store must not look like a valid id.
632
+ log.warn("task_id_validation_failed", { reason: String((err as Error)?.message ?? err).slice(0, 120) });
633
+ return null;
634
+ }
596
635
  }
597
636
 
598
637
  // ── Registration ─────────────────────────────────────────────────────────
@@ -629,7 +668,7 @@ export function registerSubagentTool(
629
668
  };
630
669
  }
631
670
 
632
- function executeAsync(
671
+ async function executeAsync(
633
672
  params: any,
634
673
  agents: AgentConfig[],
635
674
  mkd: (mode: "single" | "parallel" | "chain") => (results: SingleResult[]) => SubagentDetails,
@@ -659,13 +698,13 @@ export function registerSubagentTool(
659
698
  }
660
699
  const noTaskId = params.tasks.filter((t: any) => (t as any).taskId == null);
661
700
  if (noTaskId.length > 0) {
662
- const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nMissing taskId for: ${noTaskId.map((t: any) => t.agent).join(", ")}\n\nIf an agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf an agent is NOT linked to any task: pass taskId: "-1"`;
701
+ const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nMissing taskId for: ${noTaskId.map((t: any) => t.agent).join(", ")}\n\nIf an agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf an agent is only doing a single read-only check: pass taskId: "-1" (editing or PR work needs a real task id)`;
663
702
  // Force a follow-up turn so the AI MUST deal with this error
664
- pi.sendMessage({
665
- customType: "subagent-taskid-error",
666
- content: `🚨 CRITICAL: Async subagent call was REJECTED — missing taskId for: ${noTaskId.map((t: any) => t.agent).join(", ")}. NO agents were spawned. You MUST retry with taskId for each agent. Use TaskList to find task IDs, or pass taskId: "-1" if not linked.`,
667
- display: true,
668
- }, { triggerTurn: true, deliverAs: "followUp" });
703
+ await sendSystemNote(
704
+ pi,
705
+ `🚨 CRITICAL: Async subagent call was REJECTED — missing taskId for: ${noTaskId.map((t: any) => t.agent).join(", ")}. NO agents were spawned. You MUST retry with taskId for each agent. Use TaskList to find task IDs; "-1" is only for a single read-only check.`,
706
+ { customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
707
+ );
669
708
  return {
670
709
  content: [{ type: "text" as const, text: errorMsg }],
671
710
  details: mkd("single")([]),
@@ -674,13 +713,13 @@ export function registerSubagentTool(
674
713
  }
675
714
  for (const t of params.tasks) {
676
715
  const tid = (t as any).taskId as string;
677
- const taskErr = validateTaskId(tid, ctx.cwd, ctx.sessionManager?.getSessionId?.());
716
+ const taskErr = await validateTaskId(tid, ctx.cwd, ctx.sessionManager?.getSessionId?.());
678
717
  if (taskErr) {
679
- pi.sendMessage({
680
- customType: "subagent-taskid-error",
681
- content: `🚨 CRITICAL: ${taskErr} (agent: ${t.agent}). NO agents were spawned. Fix the taskId and retry.`,
682
- display: true,
683
- }, { triggerTurn: true, deliverAs: "followUp" });
718
+ await sendSystemNote(
719
+ pi,
720
+ `🚨 CRITICAL: ${taskErr} (agent: ${t.agent}). NO agents were spawned. Fix the taskId and retry.`,
721
+ { customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
722
+ );
684
723
  return {
685
724
  content: [{ type: "text" as const, text: `${taskErr} (agent: ${t.agent})` }],
686
725
  details: mkd("single")([]),
@@ -747,13 +786,13 @@ export function registerSubagentTool(
747
786
  };
748
787
  }
749
788
  if (params.taskId == null) {
750
- const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nIf this agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf this agent is NOT linked to any task: pass taskId: "-1"\n\nExample: subagent(agent="${params.agent}", task="...", async=true, name="${params.name}", taskId="-1")`;
789
+ const errorMsg = `Missing required parameter: taskId. Every async agent MUST have a taskId.\n\nIf this agent is working on a task: pass the task ID (e.g., taskId: "5")\nIf this agent is only doing a single read-only check: pass taskId: "-1" (editing or PR work needs a real task id)\n\nExample: subagent(agent="${params.agent}", task="...", async=true, name="${params.name}", taskId="-1")`;
751
790
  // Force a follow-up turn so the AI MUST deal with this error
752
- pi.sendMessage({
753
- customType: "subagent-taskid-error",
754
- content: `🚨 CRITICAL: Async subagent call for "${params.agent}" was REJECTED — missing taskId. The agent was NOT spawned. You MUST retry the subagent call with taskId. Use TaskList to find the task ID, or pass taskId: "-1" if not linked to any task. Do NOT proceed without fixing this.`,
755
- display: true,
756
- }, { triggerTurn: true, deliverAs: "followUp" });
791
+ await sendSystemNote(
792
+ pi,
793
+ `🚨 CRITICAL: Async subagent call for "${params.agent}" was REJECTED — missing taskId. The agent was NOT spawned. You MUST retry the subagent call with taskId. Use TaskList to find the task ID; "-1" is only for a single read-only check. Do NOT proceed without fixing this.`,
794
+ { customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
795
+ );
757
796
  return {
758
797
  content: [{ type: "text" as const, text: errorMsg }],
759
798
  details: mkd("single")([]),
@@ -761,13 +800,13 @@ export function registerSubagentTool(
761
800
  };
762
801
  }
763
802
  {
764
- const taskErr = validateTaskId(params.taskId, ctx.cwd, ctx.sessionManager?.getSessionId?.());
803
+ const taskErr = await validateTaskId(params.taskId, ctx.cwd, ctx.sessionManager?.getSessionId?.());
765
804
  if (taskErr) {
766
- pi.sendMessage({
767
- customType: "subagent-taskid-error",
768
- content: `🚨 CRITICAL: ${taskErr} The agent was NOT spawned. Fix the taskId and retry.`,
769
- display: true,
770
- }, { triggerTurn: true, deliverAs: "followUp" });
805
+ await sendSystemNote(
806
+ pi,
807
+ `🚨 CRITICAL: ${taskErr} The agent was NOT spawned. Fix the taskId and retry.`,
808
+ { customType: "subagent-taskid-error", triggerTurn: true, deliverAs: "followUp" },
809
+ );
771
810
  return {
772
811
  content: [{ type: "text" as const, text: taskErr }],
773
812
  details: mkd("single")([]),
@@ -1689,7 +1728,7 @@ export function registerSubagentTool(
1689
1728
 
1690
1729
  // Async mode — spawn in background and return immediately
1691
1730
  if (params.async === true) {
1692
- const result = executeAsync(params, agents, mkd, parentModelId, parentProvider, ctx, explicit);
1731
+ const result = await executeAsync(params, agents, mkd, parentModelId, parentProvider, ctx, explicit);
1693
1732
  if (capabilityNote && result.content?.[0]?.type === "text") {
1694
1733
  result.content[0].text = `${capabilityNote}\n\n${result.content[0].text}`;
1695
1734
  }