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 +12 -0
- package/handlers/coordination.ts +25 -1
- package/index.ts +2 -2
- package/package.json +1 -1
- package/skills/pi-messenger-swarm/SKILL.md +4 -1
- package/swarm/handlers.ts +119 -20
- package/swarm/spawn.ts +32 -4
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)
|
package/handlers/coordination.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
584
|
-
const spawnedAgents =
|
|
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
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 (
|
|
45
|
-
lines.push('##
|
|
46
|
-
for (const agent of
|
|
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.', {
|
|
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(
|
|
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
|
-
|
|
507
|
-
|
|
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 {
|