pi-messenger-swarm 0.20.2 → 0.20.3

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
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [0.20.3](https://github.com/monotykamary/pi-messenger-swarm/compare/v0.20.2...v0.20.3) (2026-04-04)
6
+
7
+
8
+ ### Features
9
+
10
+ * **swarm:** filter running agents by default, add spawn.history and messaging warnings ([0fb1a9e](https://github.com/monotykamary/pi-messenger-swarm/commit/0fb1a9e90a0472456677dbc349c5ebabe70a191c))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **swarm:** use listSpawnedHistory in session shutdown to unclaim all spawned agent tasks ([212cd05](https://github.com/monotykamary/pi-messenger-swarm/commit/212cd052e2f558f9f1f0b39c9d04b35c204e2c3c))
16
+
5
17
  ### [0.20.2](https://github.com/monotykamary/pi-messenger-swarm/compare/v0.20.1...v0.20.2) (2026-04-04)
6
18
 
7
19
  ### [0.20.1](https://github.com/monotykamary/pi-messenger-swarm/compare/v0.20.0...v0.20.1) (2026-04-04)
@@ -21,6 +21,8 @@ import {
21
21
  import { displayChannelLabel, normalizeChannelId } from '../channel.js';
22
22
  import * as store from '../store.js';
23
23
  import * as taskStore from '../swarm/task-store.js';
24
+ import { findSpawnedAgentByName } from '../swarm/spawn.js';
25
+ import { getContextSessionId } from '../store/shared.js';
24
26
  import {
25
27
  formatFeedLine,
26
28
  isSwarmEvent,
@@ -620,6 +622,23 @@ export function executeSend(
620
622
  const isChannelTarget = typeof to === 'string' && to.startsWith('#');
621
623
  const targetChannel = isChannelTarget ? normalizeChannelId(to) : channel || state.currentChannel;
622
624
 
625
+ // Check if targeting a completed/failed/stopped spawned agent
626
+ let spawnWarning = '';
627
+ if (typeof to === 'string' && !isChannelTarget) {
628
+ const sessionId = getContextSessionId({ cwd } as ExtensionContext);
629
+ const spawnedAgent = findSpawnedAgentByName(cwd, sessionId, to);
630
+ if (spawnedAgent && spawnedAgent.status !== 'running') {
631
+ const statusEmoji =
632
+ spawnedAgent.status === 'completed' ? '✅' : spawnedAgent.status === 'failed' ? '❌' : '🛑';
633
+ spawnWarning = `\n\n⚠️ Warning: ${to} is a spawned agent that has already ${spawnedAgent.status} ${statusEmoji}. The message will be logged to the feed, but the agent process is no longer active.`;
634
+ if (spawnedAgent.status === 'completed') {
635
+ spawnWarning += `\n If you need to continue the work, consider spawning a new agent.`;
636
+ } else if (spawnedAgent.status === 'failed') {
637
+ spawnWarning += `\n The agent failed with errors. Review the task and consider respawning.`;
638
+ }
639
+ }
640
+ }
641
+
623
642
  // All messaging is now feed-based
624
643
  logFeedEvent(
625
644
  cwd,
@@ -633,12 +652,17 @@ export function executeSend(
633
652
  const targetLabel = typeof to === 'string' ? to : 'multiple recipients';
634
653
  const channelLabel = displayChannelLabel(targetChannel);
635
654
  // If the target is already a channel reference, just say "posted to #channel"
636
- const text = isChannelTarget
655
+ let text = isChannelTarget
637
656
  ? `Message posted to ${targetLabel}.`
638
657
  : `Message posted to ${targetLabel} on ${channelLabel}.`;
658
+
659
+ // Append warning if targeting a completed/failed/stopped agent
660
+ text += spawnWarning;
661
+
639
662
  return result(text, {
640
663
  mode: 'send',
641
664
  channel: targetChannel,
642
665
  to: typeof to === 'string' ? to : undefined,
666
+ warning: spawnWarning ? 'target_agent_completed' : undefined,
643
667
  });
644
668
  }
package/index.ts CHANGED
@@ -580,8 +580,8 @@ Usage (swarm-first API):
580
580
  overlayTui = null;
581
581
  if (state.registered) {
582
582
  const sessionId = state.contextSessionId ?? '';
583
- const { listSpawned } = await import('./swarm/spawn.js');
584
- const spawnedAgents = listSpawned(cwd, sessionId);
583
+ const { listSpawnedHistory } = await import('./swarm/spawn.js');
584
+ const spawnedAgents = listSpawnedHistory(cwd, sessionId);
585
585
  const spawnedNames = new Set(spawnedAgents.map((s) => s.name));
586
586
 
587
587
  // Get all tasks for this session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-messenger-swarm",
3
- "version": "0.20.2",
3
+ "version": "0.20.3",
4
4
  "description": "Swarm-first multi-agent messaging and task orchestration extension for Pi",
5
5
  "type": "module",
6
6
  "author": "Tom X Nguyen",
@@ -112,7 +112,8 @@ The body after `---` becomes the system prompt.
112
112
  Manage spawned agents:
113
113
 
114
114
  ```typescript
115
- pi_messenger({ action: 'spawn.list' });
115
+ pi_messenger({ action: 'spawn.list' }); // Only shows running agents
116
+ pi_messenger({ action: 'spawn.history' }); // Shows all agents including completed/failed/stopped
116
117
  pi_messenger({ action: 'spawn.stop', id: '<spawn-id>' });
117
118
  ```
118
119
 
@@ -127,6 +128,8 @@ pi_messenger({
127
128
  });
128
129
  ```
129
130
 
131
+ ⚠️ **Warning when messaging completed agents**: If you try to send a message to a spawned subagent that has already completed, failed, or stopped, you'll receive a warning. The message is still logged to the feed, but the agent process is no longer active. Use `pi_messenger({ action: "spawn.history" })` to see which agents are still running.
132
+
130
133
  ## Swarm Philosophy
131
134
 
132
135
  The swarm is self-organizing. Your role is participant, not manager.
package/swarm/handlers.ts CHANGED
@@ -4,7 +4,13 @@ import { displayChannelLabel, normalizeChannelId } from '../channel.js';
4
4
  import { result } from './result.js';
5
5
  import { logFeedEvent } from '../feed.js';
6
6
  import * as taskStore from './task-store.js';
7
- import { cleanupExitedSpawned, listSpawned, spawnSubagent, stopSpawn } from './spawn.js';
7
+ import {
8
+ cleanupExitedSpawned,
9
+ listSpawned,
10
+ listSpawnedHistory,
11
+ spawnSubagent,
12
+ stopSpawn,
13
+ } from './spawn.js';
8
14
  import type { SpawnRequest, SwarmTaskEvidence } from './types.js';
9
15
  import { formatRoleLabel } from './labels.js';
10
16
  import { executeTaskAction } from './task-actions.js';
@@ -18,20 +24,28 @@ export function executeSwarmStatus(cwd: string, channelId: string, sessionId: st
18
24
  cleanupExitedSpawned(cwd, sessionId);
19
25
  const tasks = taskStore.getTasks(cwd, sessionId);
20
26
  const summary = taskStore.getSummary(cwd, sessionId);
21
- const spawned = listSpawned(cwd, sessionId);
27
+ const runningAgents = listSpawned(cwd, sessionId, false); // Only running
28
+ const allAgents = listSpawned(cwd, sessionId, true); // All including completed
29
+ const completedCount = allAgents.filter((a) => a.status === 'completed').length;
30
+ const failedCount = allAgents.filter((a) => a.status === 'failed').length;
22
31
  const channelLabel = displayChannelLabel(channelId);
23
32
 
24
- if (tasks.length === 0) {
25
- return result(
26
- `# Agent Swarm ${channelLabel}\n\nNo tasks yet.\n\nCreate one:\n pi_messenger({ action: "task.create", title: "...", content: "..." })\n\nSpawn a subagent:\n pi_messenger({ action: "spawn", role: "Researcher", message: "Investigate ..." })`,
27
- {
28
- mode: 'swarm',
29
- channel: normalizeChannelId(channelId),
30
- summary,
31
- tasks: [],
32
- spawned,
33
- }
34
- );
33
+ if (tasks.length === 0 && runningAgents.length === 0) {
34
+ let text = `# Agent Swarm ${channelLabel}\n\nNo tasks yet.`;
35
+ if (completedCount > 0 || failedCount > 0) {
36
+ text += `\n\n${completedCount} completed, ${failedCount} failed agents in history.`;
37
+ }
38
+ text += `\n\nCreate one:\n pi_messenger({ action: "task.create", title: "...", content: "..." })\n\nSpawn a subagent:\n pi_messenger({ action: "spawn", role: "Researcher", message: "Investigate ..." })`;
39
+ if (completedCount > 0 || failedCount > 0) {
40
+ text += `\n\nView history:\n pi_messenger({ action: "spawn.history" })`;
41
+ }
42
+ return result(text, {
43
+ mode: 'swarm',
44
+ channel: normalizeChannelId(channelId),
45
+ summary,
46
+ tasks: [],
47
+ spawned: runningAgents,
48
+ });
35
49
  }
36
50
 
37
51
  const lines: string[] = [
@@ -41,9 +55,9 @@ export function executeSwarmStatus(cwd: string, channelId: string, sessionId: st
41
55
  '',
42
56
  ];
43
57
 
44
- if (spawned.length > 0) {
45
- lines.push('## Spawned Agents');
46
- for (const agent of spawned.slice(0, 8)) {
58
+ if (runningAgents.length > 0) {
59
+ lines.push('## Running Agents');
60
+ for (const agent of runningAgents.slice(0, 8)) {
47
61
  const suffix = agent.taskId ? ` → ${agent.taskId}` : '';
48
62
  lines.push(
49
63
  `- ${agent.id} · ${agent.name} (${formatRoleLabel(agent.role)}) · ${agent.status}${suffix}`
@@ -52,6 +66,13 @@ export function executeSwarmStatus(cwd: string, channelId: string, sessionId: st
52
66
  lines.push('');
53
67
  }
54
68
 
69
+ if (completedCount > 0 || failedCount > 0) {
70
+ lines.push(`## Agent History`);
71
+ lines.push(`- ${completedCount} completed · ${failedCount} failed`);
72
+ lines.push(`- View: pi_messenger({ action: "spawn.history" })`);
73
+ lines.push('');
74
+ }
75
+
55
76
  lines.push('## Tasks');
56
77
  for (const task of tasks) {
57
78
  const icon =
@@ -72,7 +93,7 @@ export function executeSwarmStatus(cwd: string, channelId: string, sessionId: st
72
93
  channel: normalizeChannelId(channelId),
73
94
  summary,
74
95
  tasks,
75
- spawned,
96
+ spawned: runningAgents,
76
97
  });
77
98
  }
78
99
 
@@ -642,18 +663,23 @@ export function executeSpawn(
642
663
  }
643
664
 
644
665
  if (op === 'list') {
645
- const items = listSpawned(cwd, sessionId);
666
+ const items = listSpawned(cwd, sessionId, false); // Only running agents
646
667
  if (items.length === 0) {
647
- return result('No spawned agents for this project.', { mode: 'spawn.list', agents: [] });
668
+ return result('No running spawned agents for this project.', {
669
+ mode: 'spawn.list',
670
+ agents: [],
671
+ });
648
672
  }
649
673
 
650
674
  const lines = [
651
- '# Spawned Agents',
675
+ '# Running Spawned Agents',
652
676
  '',
653
677
  ...items.map((agent) => {
654
678
  const tail = agent.taskId ? ` → ${agent.taskId}` : '';
655
679
  return `- ${agent.id}: ${agent.name} (${formatRoleLabel(agent.role)}) · ${agent.status}${tail}`;
656
680
  }),
681
+ '',
682
+ `Use pi_messenger({ action: "spawn.history" }) to see all agents including completed.`,
657
683
  ];
658
684
 
659
685
  return result(lines.join('\n'), {
@@ -662,6 +688,79 @@ export function executeSpawn(
662
688
  });
663
689
  }
664
690
 
691
+ if (op === 'history') {
692
+ const items = listSpawnedHistory(cwd, sessionId); // All agents including completed
693
+ const running = items.filter((a) => a.status === 'running');
694
+ const completed = items.filter((a) => a.status === 'completed');
695
+ const failed = items.filter((a) => a.status === 'failed');
696
+ const stopped = items.filter((a) => a.status === 'stopped');
697
+
698
+ if (items.length === 0) {
699
+ return result('No spawned agents for this project.', { mode: 'spawn.history', agents: [] });
700
+ }
701
+
702
+ const lines: string[] = ['# Spawned Agent History', ''];
703
+
704
+ if (running.length > 0) {
705
+ lines.push('## Running');
706
+ for (const agent of running.slice(0, 8)) {
707
+ const tail = agent.taskId ? ` → ${agent.taskId}` : '';
708
+ lines.push(`- ${agent.id}: ${agent.name} (${formatRoleLabel(agent.role)})${tail}`);
709
+ }
710
+ lines.push('');
711
+ }
712
+
713
+ if (completed.length > 0) {
714
+ lines.push(`## Completed (${completed.length})`);
715
+ for (const agent of completed.slice(0, 10)) {
716
+ const ended = agent.endedAt
717
+ ? ` · ended ${new Date(agent.endedAt).toLocaleTimeString()}`
718
+ : '';
719
+ const tail = agent.taskId ? ` → ${agent.taskId}` : '';
720
+ lines.push(`- ${agent.id}: ${agent.name} (${formatRoleLabel(agent.role)})${tail}${ended}`);
721
+ }
722
+ if (completed.length > 10) {
723
+ lines.push(`... and ${completed.length - 10} more`);
724
+ }
725
+ lines.push('');
726
+ }
727
+
728
+ if (failed.length > 0) {
729
+ lines.push(`## Failed (${failed.length})`);
730
+ for (const agent of failed.slice(0, 5)) {
731
+ const ended = agent.endedAt
732
+ ? ` · ended ${new Date(agent.endedAt).toLocaleTimeString()}`
733
+ : '';
734
+ const tail = agent.taskId ? ` → ${agent.taskId}` : '';
735
+ lines.push(`- ${agent.id}: ${agent.name} (${formatRoleLabel(agent.role)})${tail}${ended}`);
736
+ }
737
+ lines.push('');
738
+ }
739
+
740
+ if (stopped.length > 0) {
741
+ lines.push(`## Stopped (${stopped.length})`);
742
+ for (const agent of stopped.slice(0, 5)) {
743
+ const ended = agent.endedAt
744
+ ? ` · ended ${new Date(agent.endedAt).toLocaleTimeString()}`
745
+ : '';
746
+ const tail = agent.taskId ? ` → ${agent.taskId}` : '';
747
+ lines.push(`- ${agent.id}: ${agent.name} (${formatRoleLabel(agent.role)})${tail}${ended}`);
748
+ }
749
+ lines.push('');
750
+ }
751
+
752
+ return result(lines.join('\n'), {
753
+ mode: 'spawn.history',
754
+ agents: items,
755
+ counts: {
756
+ running: running.length,
757
+ completed: completed.length,
758
+ failed: failed.length,
759
+ stopped: stopped.length,
760
+ },
761
+ });
762
+ }
763
+
665
764
  if (op === 'stop') {
666
765
  const id = params.id;
667
766
  if (!id) {
package/swarm/spawn.ts CHANGED
@@ -491,7 +491,11 @@ export function spawnSubagent(
491
491
  return record;
492
492
  }
493
493
 
494
- export function listSpawned(cwd: string, sessionId: string): SpawnedAgent[] {
494
+ export function listSpawned(
495
+ cwd: string,
496
+ sessionId: string,
497
+ includeAll: boolean = false
498
+ ): SpawnedAgent[] {
495
499
  // First, get persisted agents for this session from event log
496
500
  const persisted = loadSpawnedAgents(cwd, sessionId);
497
501
  const persistedById = new Map(persisted.map((a) => [a.id, a]));
@@ -503,9 +507,33 @@ export function listSpawned(cwd: string, sessionId: string): SpawnedAgent[] {
503
507
  persistedById.set(id, runtime.record);
504
508
  }
505
509
 
506
- return Array.from(persistedById.values()).sort(
507
- (a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt)
508
- );
510
+ let agents = Array.from(persistedById.values());
511
+
512
+ // Filter to only running agents by default
513
+ if (!includeAll) {
514
+ agents = agents.filter((a) => a.status === 'running');
515
+ }
516
+
517
+ return agents.sort((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt));
518
+ }
519
+
520
+ /**
521
+ * Get all spawned agents including completed/failed/stopped (history view).
522
+ */
523
+ export function listSpawnedHistory(cwd: string, sessionId: string): SpawnedAgent[] {
524
+ return listSpawned(cwd, sessionId, true);
525
+ }
526
+
527
+ /**
528
+ * Find a spawned agent by name (including non-running agents).
529
+ */
530
+ export function findSpawnedAgentByName(
531
+ cwd: string,
532
+ sessionId: string,
533
+ name: string
534
+ ): SpawnedAgent | null {
535
+ const allAgents = listSpawnedHistory(cwd, sessionId);
536
+ return allAgents.find((a) => a.name === name) ?? null;
509
537
  }
510
538
 
511
539
  export function stopSpawn(cwd: string, id: string): boolean {