@ferris1225/pi-subagents 1.0.0 → 1.0.1

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/src/monitor.ts CHANGED
@@ -11,8 +11,7 @@
11
11
  import { stripVTControlCharacters } from "node:util";
12
12
  import type { Theme } from "@earendil-works/pi-coding-agent";
13
13
  import { visibleWidth } from "@earendil-works/pi-tui";
14
- import type { UsageStats } from "./spawn.ts";
15
- import { redactSensitiveText } from "./trajectory.ts";
14
+ import { emptyUsage, type UsageStats } from "./rpc-run.ts";
16
15
  import type { IsolationMode, WorktreeFinalizationStatus } from "./worktree.ts";
17
16
 
18
17
  // ---------------------------------------------------------------------------
@@ -46,14 +45,6 @@ export interface RunView {
46
45
  usage: UsageStats;
47
46
  /** Concise current activity ("thinking", "read src/index.ts"); last writer wins. */
48
47
  activity?: string;
49
- /** Total tool calls started by the run so far (a progress signal). */
50
- toolCount?: number;
51
- /** Tool currently executing (set on tool_start, cleared on tool_end). When set,
52
- * the run is NOT idle for needs-attention purposes. */
53
- currentTool?: string;
54
- /** Epoch ms of the last live activity (tool, usage, status). Used to derive
55
- * the needs-attention state: no tool running AND now - lastActivityAt > threshold. */
56
- lastActivityAt?: number;
57
48
  /** Epoch ms when the run started executing (set on first "running" status). */
58
49
  startedAt?: number;
59
50
  /** Epoch ms when the run finished (set on "done"/"failed"). */
@@ -62,16 +53,6 @@ export interface RunView {
62
53
  groupId?: string;
63
54
  /** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
64
55
  relationLabel?: string;
65
- /** Free-form orchestration note (e.g. "auto-fix chain running"). */
66
- annotation?: string;
67
- /** One-line outcome summary of a finished chain run: a reviewer reports its verdict
68
- * plus key fragments of what it found ("fail · src/index.ts · render()"), a
69
- * worker the fragments of what it changed. Unset for non-chain runs. */
70
- summary?: string;
71
- /** True when a finished run remains in monitor state (e.g. an auto-fix
72
- * parent whose chain is still running). beginTurn preserves
73
- * retained runs so they are not swept between turns. */
74
- retained?: boolean;
75
56
  }
76
57
 
77
58
  /** Optional chain metadata for runs spawned by an auto-fix loop. */
@@ -280,18 +261,40 @@ export function formatDuration(ms: number): string {
280
261
  /** Elapsed wall time of a run: live while running, final once finished. */
281
262
  export function formatElapsed(run: RunView, now: number = Date.now()): string {
282
263
  if (run.startedAt === undefined) return "";
283
- // A retained row (e.g. an auto-fix chain parent whose chain is still running)
284
- // must keep ticking: its `endedAt` was stamped when the review itself
285
- // finished, but the work is ongoing, so show live elapsed until the chain
286
- // resolves and the row is removed. Without this, subagent_status would show a
287
- // frozen elapsed for a run the UI otherwise presents as still active.
288
- const end = run.retained ? now : (run.endedAt ?? now);
289
- return formatDuration(end - run.startedAt);
264
+ return formatDuration((run.endedAt ?? now) - run.startedAt);
290
265
  }
291
266
 
292
267
  /** Max length of the argument target inside a formatted activity line. */
293
268
  export const ACTIVITY_TARGET_MAX = 60;
294
269
 
270
+ const REDACTED = "<redacted>";
271
+
272
+ /** Remove credentials embedded in otherwise ordinary activity strings such as
273
+ * shell commands and HTTP headers. */
274
+ function redactSensitiveText(value: string): string {
275
+ let text = stripVTControlCharacters(value);
276
+ text = text.replace(
277
+ /(\bauthorization\s*:\s*(?:bearer|basic)\s+)([^\s'"`;,]+)/giu,
278
+ `$1${REDACTED}`,
279
+ );
280
+ text = text.replace(
281
+ /(\bbearer\s+)([A-Za-z0-9._~+/=-]{6,})/giu,
282
+ `$1${REDACTED}`,
283
+ );
284
+ text = text.replace(
285
+ /(\b(?:api[-_]?key|apikey|access[-_]?token|refresh[-_]?token|token|password|passwd|secret|credential|cookie)\b\s*(?:=|:)\s*)(?:"[^"]*"|'[^']*'|[^\s;&,]+)/giu,
286
+ `$1${REDACTED}`,
287
+ );
288
+ text = text.replace(
289
+ /((?:--?(?:api[-_]?key|access[-_]?token|token|password|secret|credential))\s+)(?:"[^"]*"|'[^']*'|\S+)/giu,
290
+ `$1${REDACTED}`,
291
+ );
292
+ return text.replace(
293
+ /\b(?:sk-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9]{8,}|AKIA[A-Z0-9]{16})\b/gu,
294
+ REDACTED,
295
+ );
296
+ }
297
+
295
298
  /** Monitor activity is returned to the parent model and rendered in the terminal,
296
299
  * so treat every live string as untrusted before it reaches store state. */
297
300
  function sanitizeActivityText(value: string): string {
@@ -363,19 +366,22 @@ export class MonitorStore {
363
366
  private subscribers = new Set<() => void>();
364
367
 
365
368
  beginTurn(): void {
366
- // Clear finished runs from a previous turn, but keep any still-active
367
- // (queued/running) ones so a concurrent sub-agent call is not wiped.
368
- // Retained runs (e.g. an auto-fix chain parent whose chain is still
369
- // running) are also preserved — their status is "done" but they must
370
- // stay visible until the chain resolves.
369
+ // Clear finished runs from a previous turn, but keep active and parked
370
+ // threads so concurrent work is not wiped between parent turns.
371
371
  this.runs = this.runs.filter(
372
- (r) => isRunActiveStatus(r.status) || r.status === "parked" || r.retained,
372
+ (r) => isRunActiveStatus(r.status) || r.status === "parked",
373
373
  );
374
374
  this.notify();
375
375
  }
376
376
 
377
+ /** Reserve a stable id for a durable result that must remain independently
378
+ * addressable without appearing as a live monitor row. */
379
+ reserveRunId(): number {
380
+ return this.nextId++;
381
+ }
382
+
377
383
  addRun(agent: string, task: string, model?: string, thinking?: string, meta?: RunChainMeta): number {
378
- const id = this.nextId++;
384
+ const id = this.reserveRunId();
379
385
  this.runs.push({
380
386
  id,
381
387
  agent,
@@ -384,7 +390,7 @@ export class MonitorStore {
384
390
  model,
385
391
  thinking,
386
392
  status: "queued",
387
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
393
+ usage: emptyUsage(),
388
394
  ...(meta?.groupId ? { groupId: meta.groupId } : {}),
389
395
  ...(meta?.relationLabel ? { relationLabel: meta.relationLabel } : {}),
390
396
  ...(meta?.isolation ? { isolation: meta.isolation, integrationStatus: meta.isolation === "worktree" ? "pending" : undefined } : {}),
@@ -403,7 +409,6 @@ export class MonitorStore {
403
409
  // A model-fallback retry or resumed generation restarts the clock; a
404
410
  // stale endedAt would freeze the elapsed display at the first attempt.
405
411
  if (run.endedAt !== undefined) run.endedAt = undefined;
406
- run.lastActivityAt = Date.now();
407
412
  } else if ((status === "parked" || status === "done" || status === "failed") && run.endedAt === undefined) {
408
413
  run.endedAt = Date.now();
409
414
  }
@@ -414,7 +419,6 @@ export class MonitorStore {
414
419
  if (!run) return;
415
420
  run.usage = { ...usage };
416
421
  if (model) run.model = model;
417
- run.lastActivityAt = Date.now();
418
422
  this.notify();
419
423
  }
420
424
 
@@ -432,59 +436,27 @@ export class MonitorStore {
432
436
  const run = this.find(id);
433
437
  if (!run) return;
434
438
  run.activity = sanitizeActivityText(text) || undefined;
435
- run.lastActivityAt = Date.now();
436
439
  this.notify();
437
440
  }
438
441
 
439
- /** Record a tool starting: counts it, marks it current, and updates activity.
440
- * A running tool means the run is NOT idle, so needs-attention is suppressed
441
- * while it stays current. */
442
+ /** Record a tool starting and update the run's visible activity. */
442
443
  recordToolStart(id: number, toolName: string, activity: string): void {
443
444
  const run = this.find(id);
444
445
  if (!run) return;
445
446
  const safeToolName = sanitizeActivityText(toolName) || "tool";
446
- run.toolCount = (run.toolCount ?? 0) + 1;
447
- run.currentTool = safeToolName;
448
447
  run.activity = sanitizeActivityText(activity) || safeToolName;
449
- run.lastActivityAt = Date.now();
450
448
  this.notify();
451
449
  }
452
450
 
453
- /** Record a tool ending: clears the current-tool marker (so the run becomes
454
- * eligible for needs-attention again) and notes the failure in activity. */
451
+ /** Record a failed tool; successful completions keep their last activity
452
+ * until the next model event supplies a more useful description. */
455
453
  recordToolEnd(id: number, toolName: string, isError: boolean): void {
456
454
  const run = this.find(id);
457
455
  if (!run) return;
458
- run.currentTool = undefined;
459
- run.lastActivityAt = Date.now();
460
456
  if (isError) run.activity = `✗ ${sanitizeActivityText(toolName) || "tool"} failed`;
461
457
  this.notify();
462
458
  }
463
459
 
464
- /** Set an orchestration note on the run (e.g. auto-fix chain running). */
465
- setAnnotation(id: number, text: string): void {
466
- const run = this.find(id);
467
- if (!run) return;
468
- run.annotation = text;
469
- this.notify();
470
- }
471
-
472
- /** Set the run's one-line outcome summary (what a finished chain round did). */
473
- setSummary(id: number, text: string | undefined): void {
474
- const run = this.find(id);
475
- if (!run) return;
476
- run.summary = text;
477
- this.notify();
478
- }
479
-
480
- /** Keep a finished chain step in status state until its group settles. */
481
- setRetained(id: number, retained: boolean): void {
482
- const run = this.find(id);
483
- if (!run) return;
484
- run.retained = retained;
485
- this.notify();
486
- }
487
-
488
460
  setIsolation(id: number, isolation: IsolationMode, integrationStatus?: "pending" | WorktreeFinalizationStatus): void {
489
461
  const run = this.find(id);
490
462
  if (!run) return;
@@ -526,7 +498,7 @@ export class MonitorStore {
526
498
  thinking,
527
499
  ...(isolation ? { isolation, integrationStatus: isolation === "worktree" ? "pending" as const : undefined } : {}),
528
500
  status: "queued",
529
- usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
501
+ usage: emptyUsage(),
530
502
  });
531
503
  this.notify();
532
504
  return;
@@ -539,16 +511,10 @@ export class MonitorStore {
539
511
  if (isolation) run.isolation = isolation;
540
512
  run.integrationStatus = isolation === "worktree" ? "pending" : undefined;
541
513
  run.status = "queued";
542
- run.usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
514
+ run.usage = emptyUsage();
543
515
  run.activity = undefined;
544
- run.toolCount = undefined;
545
- run.currentTool = undefined;
546
- run.lastActivityAt = undefined;
547
516
  run.startedAt = undefined;
548
517
  run.endedAt = undefined;
549
- run.annotation = undefined;
550
- run.summary = undefined;
551
- run.retained = undefined;
552
518
  this.notify();
553
519
  }
554
520
 
@@ -589,7 +555,6 @@ export class MonitorStore {
589
555
  const usage = formatUsageCompact(run.usage);
590
556
  const parts = [run.agent];
591
557
  if (run.relationLabel) parts.push(run.relationLabel);
592
- if (run.summary) parts.push(run.summary);
593
558
  if (run.model) parts.push(run.model);
594
559
  if (run.thinking) parts.push(`thinking ${run.thinking}`);
595
560
  if (run.isolation === "worktree") parts.push(`worktree ${run.integrationStatus ?? "active"}`);
@@ -658,20 +623,3 @@ export function statusLabel(status: RunStatus): string {
658
623
  return "stopped";
659
624
  }
660
625
  }
661
-
662
- /** Theme color matching the status label. */
663
- export function statusColor(status: RunStatus): "accent" | "success" | "error" | "warning" | "dim" {
664
- switch (status) {
665
- case "running":
666
- case "steering":
667
- return "accent";
668
- case "interrupting":
669
- return "warning";
670
- case "done":
671
- return "success";
672
- case "failed":
673
- return "error";
674
- default:
675
- return "dim";
676
- }
677
- }
package/src/rpc-run.ts CHANGED
@@ -14,7 +14,7 @@ import { tmpdir } from "node:os";
14
14
  import { basename, join } from "node:path";
15
15
  import { StringDecoder } from "node:string_decoder";
16
16
  import type { Message } from "@earendil-works/pi-ai";
17
- import type { AgentConfig, AgentSource } from "./agents.ts";
17
+ import type { AgentConfig } from "./agents.ts";
18
18
  import type { ThinkingLevel } from "./config.ts";
19
19
  import type { IsolationMode, WorktreeFinalizationStatus } from "./worktree.ts";
20
20
 
@@ -43,7 +43,6 @@ export interface UsageStats {
43
43
 
44
44
  export interface RpcSingleResult {
45
45
  agent: string;
46
- agentSource: AgentSource | "unknown";
47
46
  task: string;
48
47
  exitCode: number;
49
48
  messages: Message[];
@@ -65,16 +64,12 @@ export interface RpcSingleResult {
65
64
  failedTools?: Array<{ toolName: string; error: string }>;
66
65
  sessionId?: string;
67
66
  sessionDir?: string;
68
- resumed?: boolean;
69
67
  /** Internal disposition: dispatch suppresses completion delivery for parks. */
70
68
  parked?: boolean;
71
69
  /** Stable logical run id assigned by dispatch (also present on queued results). */
72
70
  runId?: number;
73
71
  /** Filesystem isolation selected for this logical thread. */
74
72
  isolation?: IsolationMode;
75
- /** Original parent cwd; isolated children execute at isolationCwd instead. */
76
- originalCwd?: string;
77
- isolationCwd?: string;
78
73
  /** Final integration state for a worktree-isolated settlement. */
79
74
  integrationStatus?: "pending" | WorktreeFinalizationStatus;
80
75
  integrationApplied?: boolean;
@@ -168,10 +163,6 @@ export class RpcRunControl {
168
163
  this.setPhase("parked");
169
164
  }
170
165
 
171
- markQueued(): void {
172
- this.setPhase("queued");
173
- }
174
-
175
166
  markStarting(): void {
176
167
  this.setPhase("starting");
177
168
  }
@@ -431,12 +422,8 @@ export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise
431
422
  args.push("--append-system-prompt", tmpPromptPath);
432
423
  }
433
424
 
434
- const resumed = Boolean(
435
- options.sessionDir && options.sessionId && sessionExists(options.sessionDir, options.sessionId),
436
- );
437
425
  const result: RpcSingleResult = {
438
426
  agent: agentName,
439
- agentSource: agent.source,
440
427
  task,
441
428
  exitCode: 0,
442
429
  messages: [],
@@ -446,7 +433,6 @@ export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise
446
433
  thinking: thinkingLevel,
447
434
  sessionId: options.sessionId,
448
435
  sessionDir: options.sessionDir,
449
- resumed,
450
436
  };
451
437
 
452
438
  const childDepth = currentSubagentDepth(options.env) + 1;
package/src/runtime.ts CHANGED
@@ -27,7 +27,6 @@ import {
27
27
  type RecoveryRecord,
28
28
  } from "./recovery.ts";
29
29
  import type { RpcRunControl } from "./rpc-run.ts";
30
- import { trajectoryStore } from "./trajectory.ts";
31
30
  import type { SingleResult } from "./spawn.ts";
32
31
  import type { IsolationMode, WorktreeFinalization, WorktreeIsolation } from "./worktree.ts";
33
32
 
@@ -118,9 +117,6 @@ export interface SubagentRuntime {
118
117
  sessionDirs: Set<string>;
119
118
  retainSession: (result: Pick<SingleResult, "sessionDir">) => void;
120
119
  retireThreadSession: (thread: SubagentThread) => void;
121
- /** Worktree/patch paths intentionally retained after a failed integration. */
122
- retainedArtifactPaths: Set<string>;
123
- retainWorktreeArtifacts: (finalization: WorktreeFinalization) => void;
124
120
  /** Flip sessionActive off and release all session-scoped resources. */
125
121
  shutdown: () => Promise<void>;
126
122
  }
@@ -141,9 +137,11 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
141
137
  // Computing this at delivery (emit) time — not when the item was
142
138
  // pushed — reflects the current monitor state, since finishing runs
143
139
  // are removed from the monitor before their completion is pushed.
140
+ // Auto-fix parents are flipped back to "running" while their chain
141
+ // owns the logical run, so they are included without a special case.
144
142
  const active = monitor
145
143
  .getRuns()
146
- .filter((run) => isRunActiveStatus(run.status) || run.retained)
144
+ .filter((run) => isRunActiveStatus(run.status))
147
145
  .map((run) => ({ id: run.id, agent: run.agent, label: run.label }));
148
146
  const message = {
149
147
  customType: "subagent-result",
@@ -171,11 +169,6 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
171
169
  threads: new Map<number, SubagentThread>(),
172
170
  preflightOperations: new Set<Promise<void>>(),
173
171
  sessionDirs: new Set<string>(),
174
- retainedArtifactPaths: new Set<string>(),
175
- retainWorktreeArtifacts: (finalization) => {
176
- if (finalization.worktreePath) runtime.retainedArtifactPaths.add(finalization.worktreePath);
177
- if (finalization.patchPath) runtime.retainedArtifactPaths.add(finalization.patchPath);
178
- },
179
172
  retainSession: (result) => {
180
173
  if (result.sessionDir) runtime.sessionDirs.add(result.sessionDir);
181
174
  },
@@ -241,7 +234,6 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
241
234
  for (const thread of runtime.threads.values()) {
242
235
  const finalization = await thread.finalizeIsolation(thread.generation).catch(() => undefined);
243
236
  if (finalization?.status === "retained") {
244
- runtime.retainWorktreeArtifacts(finalization);
245
237
  recoveryRecords.push(recoveryRecordFromFinalization(thread.id, finalization));
246
238
  if (!thread.isolationFailureNotified) {
247
239
  thread.isolationFailureNotified = true;
@@ -268,12 +260,8 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
268
260
  }
269
261
  runtime.sessionDirs.clear();
270
262
  runtime.preflightOperations.clear();
271
- // Deliberately do not remove retainedArtifactPaths: they are the recovery
272
- // path after a failed patch apply/cleanup.
273
263
  runtime.threads.clear();
274
264
  monitor.clear();
275
- // Lifecycle trajectories are parent-session scoped.
276
- trajectoryStore.clearAll();
277
265
  },
278
266
  };
279
267
 
@@ -7,7 +7,6 @@ import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
8
 
9
9
  export interface ForkedSession {
10
- sourceSessionFile: string;
11
10
  sessionDir: string;
12
11
  sessionId: string;
13
12
  sessionFile: string;
@@ -15,7 +14,6 @@ export interface ForkedSession {
15
14
 
16
15
  /** Locate one retained session by its authoritative header id. */
17
16
  export async function findRetainedSessionFile(
18
- cwd: string,
19
17
  sessionDir: string,
20
18
  sessionId: string,
21
19
  ): Promise<string> {
@@ -47,7 +45,6 @@ export async function forkRetainedSession(options: {
47
45
  sessionId: string;
48
46
  }): Promise<ForkedSession> {
49
47
  const sourceSessionFile = await findRetainedSessionFile(
50
- options.cwd,
51
48
  options.sessionDir,
52
49
  options.sessionId,
53
50
  );
@@ -72,7 +69,6 @@ export async function forkRetainedSession(options: {
72
69
  throw new Error(`Forked session branch has no persisted assistant checkpoint at ${sessionFile}.`);
73
70
  }
74
71
  return {
75
- sourceSessionFile,
76
72
  sessionDir,
77
73
  sessionId: manager.getSessionId(),
78
74
  sessionFile,