@hueyexe/opencode-ensemble 0.6.2 → 0.6.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/dist/index.js +59 -46
- package/package.json +3 -1
- package/src/client.ts +6 -6
- package/src/db.ts +1 -1
- package/src/hooks.ts +1 -4
- package/src/index.ts +10 -9
- package/src/notify.ts +0 -11
- package/src/schema.ts +2 -1
- package/src/state.ts +5 -0
- package/src/system-prompt.ts +6 -6
- package/src/tools/team-broadcast.ts +9 -16
- package/src/tools/team-claim.ts +1 -1
- package/src/tools/team-message.ts +7 -5
- package/src/tools/team-spawn.ts +42 -18
package/dist/index.js
CHANGED
|
@@ -15371,7 +15371,9 @@ var MIGRATIONS = [
|
|
|
15371
15371
|
function applyMigrations(db) {
|
|
15372
15372
|
const { user_version: current } = db.query("PRAGMA user_version").get();
|
|
15373
15373
|
for (let i = current;i < MIGRATIONS.length; i++) {
|
|
15374
|
-
|
|
15374
|
+
const migration = MIGRATIONS[i];
|
|
15375
|
+
if (migration)
|
|
15376
|
+
db.exec(migration);
|
|
15375
15377
|
db.exec(`PRAGMA user_version = ${i + 1}`);
|
|
15376
15378
|
}
|
|
15377
15379
|
}
|
|
@@ -15611,6 +15613,9 @@ class MemberRegistry {
|
|
|
15611
15613
|
isTeamSession(sessionId) {
|
|
15612
15614
|
return this.bySession.has(sessionId);
|
|
15613
15615
|
}
|
|
15616
|
+
allSessionIds() {
|
|
15617
|
+
return new Set(this.bySession.keys());
|
|
15618
|
+
}
|
|
15614
15619
|
}
|
|
15615
15620
|
var DEFAULT_MAX_DEPTH = 10;
|
|
15616
15621
|
|
|
@@ -15680,10 +15685,7 @@ function checkToolIsolation(registry2, tracker, toolName, sessionId) {
|
|
|
15680
15685
|
return;
|
|
15681
15686
|
if (registry2.isTeamSession(sessionId))
|
|
15682
15687
|
return;
|
|
15683
|
-
const allTeamSessions =
|
|
15684
|
-
for (const entry of registry2["bySession"].keys()) {
|
|
15685
|
-
allTeamSessions.add(entry);
|
|
15686
|
-
}
|
|
15688
|
+
const allTeamSessions = registry2.allSessionIds();
|
|
15687
15689
|
if (allTeamSessions.size > 0 && tracker.isDescendantOf(sessionId, allTeamSessions)) {
|
|
15688
15690
|
throw new Error("Team tools are not available to sub-agents. Report findings to your parent teammate via your normal output.");
|
|
15689
15691
|
}
|
|
@@ -15771,9 +15773,9 @@ function buildLeadSystemPrompt(db, teamId) {
|
|
|
15771
15773
|
for (const row of taskCounts) {
|
|
15772
15774
|
countMap[row.status] = row.count;
|
|
15773
15775
|
}
|
|
15774
|
-
const completed = countMap
|
|
15775
|
-
const inProgress = countMap
|
|
15776
|
-
const pending = countMap
|
|
15776
|
+
const completed = countMap.completed ?? 0;
|
|
15777
|
+
const inProgress = countMap.in_progress ?? 0;
|
|
15778
|
+
const pending = countMap.pending ?? 0;
|
|
15777
15779
|
const memberList = members.map((m) => `${m.name} [${STATUS_DISPLAY[m.status] ?? m.status}]`).join(", ");
|
|
15778
15780
|
const teammateLine = members.length > 0 ? `Teammates: ${memberList}` : "Teammates: none";
|
|
15779
15781
|
return [
|
|
@@ -15804,9 +15806,9 @@ function buildTeamCompactionContext(db, teamId, role, memberName) {
|
|
|
15804
15806
|
for (const row of taskCounts) {
|
|
15805
15807
|
countMap[row.status] = row.count;
|
|
15806
15808
|
}
|
|
15807
|
-
const completed = countMap
|
|
15808
|
-
const inProgress = countMap
|
|
15809
|
-
const pending = countMap
|
|
15809
|
+
const completed = countMap.completed ?? 0;
|
|
15810
|
+
const inProgress = countMap.in_progress ?? 0;
|
|
15811
|
+
const pending = countMap.pending ?? 0;
|
|
15810
15812
|
const roleLine = role === "lead" ? `[Team Context] You are the lead of team "${team.name}".` : `[Team Context] You are a teammate named "${memberName}" in team "${team.name}".`;
|
|
15811
15813
|
const memberList = members.map((m) => `${m.name} (${STATUS_DISPLAY[m.status] ?? m.status})`).join(", ");
|
|
15812
15814
|
const membersLine = members.length > 0 ? `Members: ${memberList}` : "Members: none";
|
|
@@ -15947,25 +15949,36 @@ async function executeTeamSpawn(deps, args, sessionId) {
|
|
|
15947
15949
|
}
|
|
15948
15950
|
const contextStr = context.join(`
|
|
15949
15951
|
`);
|
|
15950
|
-
|
|
15951
|
-
|
|
15952
|
-
|
|
15953
|
-
|
|
15954
|
-
|
|
15955
|
-
});
|
|
15956
|
-
} catch (err) {
|
|
15957
|
-
deps.db.run("DELETE FROM team_member WHERE team_id = ? AND name = ?", [teamInfo.teamId, args.name]);
|
|
15958
|
-
deps.registry.unregister(childSessionId);
|
|
15952
|
+
deps.client.session.promptAsync({
|
|
15953
|
+
sessionID: childSessionId,
|
|
15954
|
+
parts: [{ type: "text", text: contextStr }],
|
|
15955
|
+
agent: args.agent
|
|
15956
|
+
}).catch(() => {
|
|
15959
15957
|
try {
|
|
15960
|
-
|
|
15958
|
+
deps.db.run("DELETE FROM team_member WHERE team_id = ? AND session_id = ?", [teamInfo.teamId, childSessionId]);
|
|
15959
|
+
deps.registry.unregister(childSessionId);
|
|
15960
|
+
deps.client.session.abort({ sessionID: childSessionId }).catch(() => {});
|
|
15961
|
+
if (worktreeDir) {
|
|
15962
|
+
deps.client.worktree.remove({ worktreeRemoveInput: { directory: worktreeDir } }).catch(() => {});
|
|
15963
|
+
}
|
|
15964
|
+
deps.client.tui.showToast({
|
|
15965
|
+
title: "Team",
|
|
15966
|
+
message: `Teammate "${args.name}" failed to start and was removed`,
|
|
15967
|
+
variant: "error",
|
|
15968
|
+
duration: 5000
|
|
15969
|
+
}).catch(() => {});
|
|
15970
|
+
sendMessage(deps.db, {
|
|
15971
|
+
teamId: teamInfo.teamId,
|
|
15972
|
+
from: "system",
|
|
15973
|
+
to: "lead",
|
|
15974
|
+
content: `Teammate "${args.name}" failed to start and was removed. You may retry the spawn.`
|
|
15975
|
+
});
|
|
15976
|
+
const team = deps.db.query("SELECT lead_session_id FROM team WHERE id = ?").get(teamInfo.teamId);
|
|
15977
|
+
if (team) {
|
|
15978
|
+
handleLeadIdleFlush(deps.db, deps.client, team.lead_session_id).catch(() => {});
|
|
15979
|
+
}
|
|
15961
15980
|
} catch {}
|
|
15962
|
-
|
|
15963
|
-
try {
|
|
15964
|
-
await deps.client.worktree.remove({ worktreeRemoveInput: { directory: worktreeDir } });
|
|
15965
|
-
} catch {}
|
|
15966
|
-
}
|
|
15967
|
-
throw new Error(`Failed to send initial prompt to teammate "${args.name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
15968
|
-
}
|
|
15981
|
+
});
|
|
15969
15982
|
const branchInfo = worktreeBranch ? ` (branch: ${worktreeBranch})` : "";
|
|
15970
15983
|
const planInfo = usePlanApproval ? " [plan mode \u2014 will send plan for approval]" : "";
|
|
15971
15984
|
return `Teammate "${args.name}" spawned (agent: ${args.agent})${branchInfo}${planInfo}. They are working on: ${args.prompt.slice(0, 120)}${args.prompt.length > 120 ? "..." : ""}`;
|
|
@@ -15976,7 +15989,7 @@ async function executeTeamMessage(deps, args, sessionId) {
|
|
|
15976
15989
|
const teamInfo = findTeamBySession(deps.db, deps.registry, sessionId);
|
|
15977
15990
|
if (!teamInfo)
|
|
15978
15991
|
throw new Error("This session is not in a team. Use team_create first.");
|
|
15979
|
-
const senderName = teamInfo.role === "lead" ? "lead" : teamInfo.memberName;
|
|
15992
|
+
const senderName = teamInfo.role === "lead" ? "lead" : teamInfo.memberName ?? "unknown";
|
|
15980
15993
|
const recipientSessionId = resolveRecipientSession(deps.db, deps.registry, teamInfo.teamId, args.to);
|
|
15981
15994
|
if (!recipientSessionId)
|
|
15982
15995
|
throw new Error(`Recipient "${args.to}" not found in team "${teamInfo.teamName}"`);
|
|
@@ -16026,11 +16039,12 @@ async function executeTeamMessage(deps, args, sessionId) {
|
|
|
16026
16039
|
if (recipientBusy) {
|
|
16027
16040
|
return `Message sent to ${args.to}. (queued \u2014 recipient is busy)`;
|
|
16028
16041
|
}
|
|
16029
|
-
|
|
16042
|
+
deps.client.session.promptAsync({
|
|
16030
16043
|
sessionID: recipientSessionId,
|
|
16031
16044
|
parts: [{ type: "text", text: deliveryText }]
|
|
16032
|
-
})
|
|
16033
|
-
|
|
16045
|
+
}).then(() => {
|
|
16046
|
+
markDelivered(deps.db, msgId);
|
|
16047
|
+
}).catch(() => {});
|
|
16034
16048
|
return `Message sent to ${args.to}.`;
|
|
16035
16049
|
}
|
|
16036
16050
|
|
|
@@ -16039,7 +16053,7 @@ async function executeTeamBroadcast(deps, args, sessionId) {
|
|
|
16039
16053
|
const teamInfo = findTeamBySession(deps.db, deps.registry, sessionId);
|
|
16040
16054
|
if (!teamInfo)
|
|
16041
16055
|
throw new Error("This session is not in a team. Use team_create first.");
|
|
16042
|
-
const senderName = teamInfo.role === "lead" ? "lead" : teamInfo.memberName;
|
|
16056
|
+
const senderName = teamInfo.role === "lead" ? "lead" : teamInfo.memberName ?? "unknown";
|
|
16043
16057
|
const msgId = broadcastMessage(deps.db, {
|
|
16044
16058
|
teamId: teamInfo.teamId,
|
|
16045
16059
|
from: senderName,
|
|
@@ -16059,18 +16073,16 @@ async function executeTeamBroadcast(deps, args, sessionId) {
|
|
|
16059
16073
|
}
|
|
16060
16074
|
let delivered = 0;
|
|
16061
16075
|
for (const recipient of recipients) {
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
|
|
16066
|
-
});
|
|
16076
|
+
deps.client.session.promptAsync({
|
|
16077
|
+
sessionID: recipient.sessionId,
|
|
16078
|
+
parts: [{ type: "text", text: `[Team broadcast from ${senderName}]: ${args.text}` }]
|
|
16079
|
+
}).then(() => {
|
|
16067
16080
|
delivered++;
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
markDelivered(deps.db, msgId);
|
|
16081
|
+
if (delivered === 1)
|
|
16082
|
+
markDelivered(deps.db, msgId);
|
|
16083
|
+
}).catch(() => {});
|
|
16072
16084
|
}
|
|
16073
|
-
return `Broadcast sent to ${
|
|
16085
|
+
return `Broadcast sent to ${recipients.length} recipient${recipients.length !== 1 ? "s" : ""}.`;
|
|
16074
16086
|
}
|
|
16075
16087
|
|
|
16076
16088
|
// src/tools/team-shutdown.ts
|
|
@@ -16240,7 +16252,7 @@ async function executeTeamClaim(deps, args, sessionId) {
|
|
|
16240
16252
|
const teamInfo = findTeamBySession(deps.db, deps.registry, sessionId);
|
|
16241
16253
|
if (!teamInfo)
|
|
16242
16254
|
throw new Error("This session is not in a team.");
|
|
16243
|
-
const claimerName = teamInfo.role === "lead" ? "lead" : teamInfo.memberName;
|
|
16255
|
+
const claimerName = teamInfo.role === "lead" ? "lead" : teamInfo.memberName ?? "unknown";
|
|
16244
16256
|
const task = deps.db.query("SELECT * FROM team_task WHERE id = ? AND team_id = ?").get(args.task_id, teamInfo.teamId);
|
|
16245
16257
|
if (!task)
|
|
16246
16258
|
throw new Error(`Task "${args.task_id}" not found`);
|
|
@@ -16484,7 +16496,8 @@ var plugin = async (input) => {
|
|
|
16484
16496
|
const db = createDb(dbPath);
|
|
16485
16497
|
const registry2 = new MemberRegistry;
|
|
16486
16498
|
const tracker = new DescendantTracker;
|
|
16487
|
-
const
|
|
16499
|
+
const pluginTransport = input.client._client;
|
|
16500
|
+
const rawClient = new OpencodeClient({ client: pluginTransport });
|
|
16488
16501
|
const client3 = wrapThrowingClient(rawClient);
|
|
16489
16502
|
const deps = { db, registry: registry2, tracker, client: client3, directory: input.directory };
|
|
16490
16503
|
const recovery = await recoverStaleMembers(db, client3);
|
|
@@ -16570,7 +16583,7 @@ var plugin = async (input) => {
|
|
|
16570
16583
|
const teamInfo = findTeamBySession(db, registry2, input2.sessionID);
|
|
16571
16584
|
if (!teamInfo)
|
|
16572
16585
|
return;
|
|
16573
|
-
const prompt = teamInfo.role === "lead" ? buildLeadSystemPrompt(db, teamInfo.teamId) : buildTeammateSystemPrompt(db, teamInfo.teamId, teamInfo.memberName);
|
|
16586
|
+
const prompt = teamInfo.role === "lead" ? buildLeadSystemPrompt(db, teamInfo.teamId) : buildTeammateSystemPrompt(db, teamInfo.teamId, teamInfo.memberName ?? "unknown");
|
|
16574
16587
|
output.system.push(prompt);
|
|
16575
16588
|
},
|
|
16576
16589
|
"experimental.session.compacting": async (input2, output) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hueyexe/opencode-ensemble",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Agent teams for OpenCode — parallel agents with peer-to-peer communication, shared tasks, and coordinated execution",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,12 +26,14 @@
|
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
+
"lint": "bunx biome lint src/",
|
|
29
30
|
"typecheck": "tsc --noEmit",
|
|
30
31
|
"test": "bun test",
|
|
31
32
|
"build": "bun build src/index.ts --outdir dist --target bun",
|
|
32
33
|
"prepublishOnly": "bun run typecheck && bun test && bun run build"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
36
|
+
"@biomejs/biome": "^2.4.9",
|
|
35
37
|
"@types/bun": "latest"
|
|
36
38
|
},
|
|
37
39
|
"peerDependencies": {
|
package/src/client.ts
CHANGED
|
@@ -6,11 +6,11 @@ function extractError(err: unknown): string {
|
|
|
6
6
|
return String(err)
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
type
|
|
9
|
+
/** Generic async SDK method type. Actual type safety enforced by PluginClient interface. */
|
|
10
|
+
type SdkMethod = (...args: unknown[]) => Promise<unknown>
|
|
11
11
|
|
|
12
12
|
/** Wrap a single async SDK method to throw on { error } responses. */
|
|
13
|
-
function throwing(fn:
|
|
13
|
+
function throwing(fn: SdkMethod): SdkMethod {
|
|
14
14
|
return async (...args: unknown[]) => {
|
|
15
15
|
const result = await fn(...args)
|
|
16
16
|
if (result && typeof result === "object" && "error" in result && result.error !== undefined) {
|
|
@@ -22,9 +22,9 @@ function throwing(fn: AnyFn): AnyFn {
|
|
|
22
22
|
|
|
23
23
|
/** Shape of the raw v2 SDK client — just the methods we wrap. */
|
|
24
24
|
interface RawClient {
|
|
25
|
-
session: { create:
|
|
26
|
-
tui: { showToast:
|
|
27
|
-
worktree: { create:
|
|
25
|
+
session: { create: SdkMethod; promptAsync: SdkMethod; abort: SdkMethod; status: SdkMethod }
|
|
26
|
+
tui: { showToast: SdkMethod; selectSession: SdkMethod }
|
|
27
|
+
worktree: { create: SdkMethod; remove: SdkMethod; list: SdkMethod; reset: SdkMethod }
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
package/src/db.ts
CHANGED
package/src/hooks.ts
CHANGED
|
@@ -94,10 +94,7 @@ export function checkToolIsolation(
|
|
|
94
94
|
if (registry.isTeamSession(sessionId)) return
|
|
95
95
|
|
|
96
96
|
// Check if this session is a descendant of any team member
|
|
97
|
-
const allTeamSessions =
|
|
98
|
-
for (const entry of registry["bySession"].keys()) {
|
|
99
|
-
allTeamSessions.add(entry)
|
|
100
|
-
}
|
|
97
|
+
const allTeamSessions = registry.allSessionIds()
|
|
101
98
|
|
|
102
99
|
if (allTeamSessions.size > 0 && tracker.isDescendantOf(sessionId, allTeamSessions)) {
|
|
103
100
|
throw new Error("Team tools are not available to sub-agents. Report findings to your parent teammate via your normal output.")
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
2
|
import { tool } from "@opencode-ai/plugin"
|
|
3
|
-
import { OpencodeClient
|
|
4
|
-
import path from "path"
|
|
3
|
+
import { OpencodeClient } from "@opencode-ai/sdk/v2"
|
|
4
|
+
import path from "node:path"
|
|
5
5
|
import { mkdirSync } from "node:fs"
|
|
6
6
|
import { createDb, getDbPath } from "./db"
|
|
7
7
|
import { wrapThrowingClient } from "./client"
|
|
@@ -24,7 +24,7 @@ import { executeTeamClaim } from "./tools/team-claim"
|
|
|
24
24
|
import { executeTeamResults } from "./tools/team-results"
|
|
25
25
|
import { executeTeamStatus } from "./tools/team-status"
|
|
26
26
|
import { executeTeamView } from "./tools/team-view"
|
|
27
|
-
import type { ToolDeps,
|
|
27
|
+
import type { ToolDeps, } from "./types"
|
|
28
28
|
import { TokenBucket } from "./rate-limit"
|
|
29
29
|
import { Watchdog } from "./watchdog"
|
|
30
30
|
|
|
@@ -49,11 +49,12 @@ const plugin: Plugin = async (input) => {
|
|
|
49
49
|
const registry = new MemberRegistry()
|
|
50
50
|
const tracker = new DescendantTracker()
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
const
|
|
52
|
+
// Extract the working HeyAPI transport from the plugin-provided v1 client and pass it
|
|
53
|
+
// to the v2 OpencodeClient. The plugin framework provides a v1 client which stores its
|
|
54
|
+
// HeyAPI transport as `_client` (underscore). The v2 constructor accepts it as `client`.
|
|
55
|
+
type V2Transport = ConstructorParameters<typeof OpencodeClient>[0] extends { client?: infer C } ? C : never
|
|
56
|
+
const pluginTransport = (input.client as unknown as { _client: V2Transport })._client
|
|
57
|
+
const rawClient = new OpencodeClient({ client: pluginTransport })
|
|
57
58
|
const client = wrapThrowingClient(rawClient)
|
|
58
59
|
const deps: ToolDeps = { db, registry, tracker, client, directory: input.directory }
|
|
59
60
|
|
|
@@ -172,7 +173,7 @@ const plugin: Plugin = async (input) => {
|
|
|
172
173
|
if (!teamInfo) return
|
|
173
174
|
const prompt = teamInfo.role === "lead"
|
|
174
175
|
? buildLeadSystemPrompt(db, teamInfo.teamId)
|
|
175
|
-
: buildTeammateSystemPrompt(db, teamInfo.teamId, teamInfo.memberName
|
|
176
|
+
: buildTeammateSystemPrompt(db, teamInfo.teamId, teamInfo.memberName ?? "unknown")
|
|
176
177
|
output.system.push(prompt)
|
|
177
178
|
},
|
|
178
179
|
|
package/src/notify.ts
CHANGED
|
@@ -3,17 +3,6 @@ import type { PluginClient } from "./types"
|
|
|
3
3
|
|
|
4
4
|
type TeamEventType = "spawn" | "message" | "completed" | "error" | "shutdown"
|
|
5
5
|
|
|
6
|
-
interface SpawnEvent { memberName: string; agent: string }
|
|
7
|
-
interface MessageEvent { from: string; to: string }
|
|
8
|
-
interface MemberEvent { memberName: string }
|
|
9
|
-
|
|
10
|
-
type TeamEventData =
|
|
11
|
-
| { type: "spawn"; data: SpawnEvent }
|
|
12
|
-
| { type: "message"; data: MessageEvent }
|
|
13
|
-
| { type: "completed"; data: MemberEvent }
|
|
14
|
-
| { type: "error"; data: MemberEvent }
|
|
15
|
-
| { type: "shutdown"; data: MemberEvent }
|
|
16
|
-
|
|
17
6
|
const TOAST_CONFIG: Record<TeamEventType, { variant: "info" | "success" | "warning" | "error"; duration: number }> = {
|
|
18
7
|
spawn: { variant: "success", duration: 3000 },
|
|
19
8
|
message: { variant: "info", duration: 3000 },
|
package/src/schema.ts
CHANGED
|
@@ -88,7 +88,8 @@ export function applyMigrations(db: Database): void {
|
|
|
88
88
|
const { user_version: current } = db.query("PRAGMA user_version").get() as { user_version: number }
|
|
89
89
|
|
|
90
90
|
for (let i = current; i < MIGRATIONS.length; i++) {
|
|
91
|
-
|
|
91
|
+
const migration = MIGRATIONS[i]
|
|
92
|
+
if (migration) db.exec(migration)
|
|
92
93
|
db.exec(`PRAGMA user_version = ${i + 1}`)
|
|
93
94
|
}
|
|
94
95
|
}
|
package/src/state.ts
CHANGED
|
@@ -59,6 +59,11 @@ export class MemberRegistry {
|
|
|
59
59
|
isTeamSession(sessionId: string): boolean {
|
|
60
60
|
return this.bySession.has(sessionId)
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/** Get all registered session IDs. */
|
|
64
|
+
allSessionIds(): Set<string> {
|
|
65
|
+
return new Set(this.bySession.keys())
|
|
66
|
+
}
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
const DEFAULT_MAX_DEPTH = 10
|
package/src/system-prompt.ts
CHANGED
|
@@ -28,9 +28,9 @@ export function buildLeadSystemPrompt(db: Database, teamId: string): string {
|
|
|
28
28
|
countMap[row.status] = row.count
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
const completed = countMap
|
|
32
|
-
const inProgress = countMap
|
|
33
|
-
const pending = countMap
|
|
31
|
+
const completed = countMap.completed ?? 0
|
|
32
|
+
const inProgress = countMap.in_progress ?? 0
|
|
33
|
+
const pending = countMap.pending ?? 0
|
|
34
34
|
|
|
35
35
|
const memberList = members
|
|
36
36
|
.map((m) => `${m.name} [${STATUS_DISPLAY[m.status] ?? m.status}]`)
|
|
@@ -87,9 +87,9 @@ export function buildTeamCompactionContext(
|
|
|
87
87
|
countMap[row.status] = row.count
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
const completed = countMap
|
|
91
|
-
const inProgress = countMap
|
|
92
|
-
const pending = countMap
|
|
90
|
+
const completed = countMap.completed ?? 0
|
|
91
|
+
const inProgress = countMap.in_progress ?? 0
|
|
92
|
+
const pending = countMap.pending ?? 0
|
|
93
93
|
|
|
94
94
|
const roleLine = role === "lead"
|
|
95
95
|
? `[Team Context] You are the lead of team "${team.name}".`
|
|
@@ -13,7 +13,7 @@ export async function executeTeamBroadcast(
|
|
|
13
13
|
const teamInfo = findTeamBySession(deps.db, deps.registry, sessionId)
|
|
14
14
|
if (!teamInfo) throw new Error("This session is not in a team. Use team_create first.")
|
|
15
15
|
|
|
16
|
-
const senderName = teamInfo.role === "lead" ? "lead" : teamInfo.memberName
|
|
16
|
+
const senderName = teamInfo.role === "lead" ? "lead" : (teamInfo.memberName ?? "unknown")
|
|
17
17
|
|
|
18
18
|
const msgId = broadcastMessage(deps.db, {
|
|
19
19
|
teamId: teamInfo.teamId,
|
|
@@ -39,24 +39,17 @@ export async function executeTeamBroadcast(
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// Fire-and-forget: deliver to all recipients. Message is already persisted in DB.
|
|
43
43
|
let delivered = 0
|
|
44
44
|
for (const recipient of recipients) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
45
|
+
deps.client.session.promptAsync({
|
|
46
|
+
sessionID: recipient.sessionId,
|
|
47
|
+
parts: [{ type: "text", text: `[Team broadcast from ${senderName}]: ${args.text}` }],
|
|
48
|
+
}).then(() => {
|
|
50
49
|
delivered++
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Only mark delivered if at least one recipient received it
|
|
57
|
-
if (delivered > 0) {
|
|
58
|
-
markDelivered(deps.db, msgId)
|
|
50
|
+
if (delivered === 1) markDelivered(deps.db, msgId)
|
|
51
|
+
}).catch(() => { /* partial failure is expected */ })
|
|
59
52
|
}
|
|
60
53
|
|
|
61
|
-
return `Broadcast sent to ${
|
|
54
|
+
return `Broadcast sent to ${recipients.length} recipient${recipients.length !== 1 ? "s" : ""}.`
|
|
62
55
|
}
|
package/src/tools/team-claim.ts
CHANGED
|
@@ -13,7 +13,7 @@ export async function executeTeamClaim(
|
|
|
13
13
|
const teamInfo = findTeamBySession(deps.db, deps.registry, sessionId)
|
|
14
14
|
if (!teamInfo) throw new Error("This session is not in a team.")
|
|
15
15
|
|
|
16
|
-
const claimerName = teamInfo.role === "lead" ? "lead" : teamInfo.memberName
|
|
16
|
+
const claimerName = teamInfo.role === "lead" ? "lead" : (teamInfo.memberName ?? "unknown")
|
|
17
17
|
|
|
18
18
|
const task = deps.db.query("SELECT * FROM team_task WHERE id = ? AND team_id = ?")
|
|
19
19
|
.get(args.task_id, teamInfo.teamId) as Record<string, unknown> | null
|
|
@@ -14,7 +14,7 @@ export async function executeTeamMessage(
|
|
|
14
14
|
const teamInfo = findTeamBySession(deps.db, deps.registry, sessionId)
|
|
15
15
|
if (!teamInfo) throw new Error("This session is not in a team. Use team_create first.")
|
|
16
16
|
|
|
17
|
-
const senderName = teamInfo.role === "lead" ? "lead" : teamInfo.memberName
|
|
17
|
+
const senderName = teamInfo.role === "lead" ? "lead" : (teamInfo.memberName ?? "unknown")
|
|
18
18
|
|
|
19
19
|
const recipientSessionId = resolveRecipientSession(deps.db, deps.registry, teamInfo.teamId, args.to)
|
|
20
20
|
if (!recipientSessionId) throw new Error(`Recipient "${args.to}" not found in team "${teamInfo.teamName}"`)
|
|
@@ -85,12 +85,14 @@ export async function executeTeamMessage(
|
|
|
85
85
|
return `Message sent to ${args.to}. (queued — recipient is busy)`
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// Fire-and-forget: message is already persisted in DB. If delivery fails,
|
|
89
|
+
// the idle-flush backstop will redeliver it.
|
|
90
|
+
deps.client.session.promptAsync({
|
|
89
91
|
sessionID: recipientSessionId,
|
|
90
92
|
parts: [{ type: "text", text: deliveryText }],
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
}).then(() => {
|
|
94
|
+
markDelivered(deps.db, msgId)
|
|
95
|
+
}).catch(() => { /* message stays delivered=0, idle-flush will retry */ })
|
|
94
96
|
|
|
95
97
|
return `Message sent to ${args.to}.`
|
|
96
98
|
}
|
package/src/tools/team-spawn.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ToolDeps, PermissionRule } from "../types"
|
|
2
|
-
import {
|
|
2
|
+
import { validateMemberName } from "../util"
|
|
3
3
|
import { findTeamBySession } from "../types"
|
|
4
|
+
import { sendMessage } from "../messaging"
|
|
5
|
+
import { handleLeadIdleFlush } from "../hooks"
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Execute the team_spawn tool. Creates a child session and starts a teammate.
|
|
@@ -156,23 +158,45 @@ export async function executeTeamSpawn(
|
|
|
156
158
|
|
|
157
159
|
const contextStr = context.join("\n")
|
|
158
160
|
|
|
159
|
-
// Fire-and-forget: send prompt to teammate session
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
161
|
+
// Fire-and-forget: send prompt to teammate session.
|
|
162
|
+
// Do NOT await — promptAsync is HTTP 204 (returns immediately on the server)
|
|
163
|
+
// but awaiting it can block if the transport is slow or broken (e.g. proxy).
|
|
164
|
+
// The child session is already created and registered. If delivery fails,
|
|
165
|
+
// the async .catch() rolls back the member and the watchdog handles stale members.
|
|
166
|
+
deps.client.session.promptAsync({
|
|
167
|
+
sessionID: childSessionId,
|
|
168
|
+
parts: [{ type: "text", text: contextStr }],
|
|
169
|
+
agent: args.agent,
|
|
170
|
+
}).catch(() => {
|
|
171
|
+
try {
|
|
172
|
+
// Async rollback: clean up DB (by session_id to avoid deleting a re-spawned member with the same name), registry, session, and worktree
|
|
173
|
+
deps.db.run("DELETE FROM team_member WHERE team_id = ? AND session_id = ?", [teamInfo.teamId, childSessionId])
|
|
174
|
+
deps.registry.unregister(childSessionId)
|
|
175
|
+
deps.client.session.abort({ sessionID: childSessionId }).catch(() => { /* best effort */ })
|
|
176
|
+
if (worktreeDir) {
|
|
177
|
+
deps.client.worktree.remove({ worktreeRemoveInput: { directory: worktreeDir } }).catch(() => { /* best effort */ })
|
|
178
|
+
}
|
|
179
|
+
// Notify user that spawn failed so the lead doesn't think the teammate is active
|
|
180
|
+
deps.client.tui.showToast({
|
|
181
|
+
title: "Team",
|
|
182
|
+
message: `Teammate "${args.name}" failed to start and was removed`,
|
|
183
|
+
variant: "error",
|
|
184
|
+
duration: 5000,
|
|
185
|
+
}).catch(() => { /* TUI may not be available */ })
|
|
186
|
+
// Notify the lead model so it can react (retry, adjust plan, etc.)
|
|
187
|
+
sendMessage(deps.db, {
|
|
188
|
+
teamId: teamInfo.teamId,
|
|
189
|
+
from: "system",
|
|
190
|
+
to: "lead",
|
|
191
|
+
content: `Teammate "${args.name}" failed to start and was removed. You may retry the spawn.`,
|
|
192
|
+
})
|
|
193
|
+
const team = deps.db.query("SELECT lead_session_id FROM team WHERE id = ?")
|
|
194
|
+
.get(teamInfo.teamId) as { lead_session_id: string } | null
|
|
195
|
+
if (team) {
|
|
196
|
+
handleLeadIdleFlush(deps.db, deps.client, team.lead_session_id).catch(() => {})
|
|
197
|
+
}
|
|
198
|
+
} catch { /* rollback failed — watchdog will clean up stale member */ }
|
|
199
|
+
})
|
|
176
200
|
|
|
177
201
|
const branchInfo = worktreeBranch ? ` (branch: ${worktreeBranch})` : ""
|
|
178
202
|
const planInfo = usePlanApproval ? " [plan mode — will send plan for approval]" : ""
|