@hueyexe/opencode-ensemble 0.13.0 → 0.13.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/README.md +5 -5
- package/dist/index.js +35 -6
- package/package.json +1 -1
- package/src/hooks.ts +16 -1
- package/src/index.ts +13 -4
- package/src/messaging.ts +11 -0
- package/src/recovery.ts +7 -1
- package/src/schema.ts +2 -0
- package/src/tools/team-broadcast.ts +9 -2
- package/src/tools/team-message.ts +6 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@hueyexe/opencode-ensemble)
|
|
4
4
|
[](https://www.npmjs.com/package/@hueyexe/opencode-ensemble)
|
|
5
|
-
[]()
|
|
6
6
|
[]()
|
|
7
7
|
[]()
|
|
8
8
|
[](./LICENSE)
|
|
@@ -15,7 +15,7 @@ Plugin built on the public OpenCode SDK. No internal dependencies.
|
|
|
15
15
|
|
|
16
16
|
```json
|
|
17
17
|
{
|
|
18
|
-
"plugin": ["@hueyexe/opencode-ensemble@0.13.
|
|
18
|
+
"plugin": ["@hueyexe/opencode-ensemble@0.13.1"]
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -136,7 +136,7 @@ Add to your OpenCode config with a pinned version. Project-level or global.
|
|
|
136
136
|
|
|
137
137
|
```json
|
|
138
138
|
{
|
|
139
|
-
"plugin": ["@hueyexe/opencode-ensemble@0.13.
|
|
139
|
+
"plugin": ["@hueyexe/opencode-ensemble@0.13.1"]
|
|
140
140
|
}
|
|
141
141
|
```
|
|
142
142
|
|
|
@@ -144,7 +144,7 @@ Add to your OpenCode config with a pinned version. Project-level or global.
|
|
|
144
144
|
|
|
145
145
|
```json
|
|
146
146
|
{
|
|
147
|
-
"plugin": ["@hueyexe/opencode-ensemble@0.13.
|
|
147
|
+
"plugin": ["@hueyexe/opencode-ensemble@0.13.1"]
|
|
148
148
|
}
|
|
149
149
|
```
|
|
150
150
|
|
|
@@ -406,7 +406,7 @@ Same coordination model (shared tasks, peer messaging, lead coordination) with s
|
|
|
406
406
|
```bash
|
|
407
407
|
bun install
|
|
408
408
|
bun run typecheck
|
|
409
|
-
bun test #
|
|
409
|
+
bun test # 492 tests
|
|
410
410
|
bun run build
|
|
411
411
|
```
|
|
412
412
|
|
package/dist/index.js
CHANGED
|
@@ -15454,7 +15454,8 @@ var MIGRATIONS = [
|
|
|
15454
15454
|
`ALTER TABLE team_member ADD COLUMN plan_approval TEXT NOT NULL DEFAULT 'none'
|
|
15455
15455
|
CHECK(plan_approval IN ('none', 'pending', 'approved', 'rejected'));`,
|
|
15456
15456
|
`ALTER TABLE team ADD COLUMN lead_agent TEXT;`,
|
|
15457
|
-
`ALTER TABLE team_member ADD COLUMN workspace_id TEXT
|
|
15457
|
+
`ALTER TABLE team_member ADD COLUMN workspace_id TEXT;`,
|
|
15458
|
+
`ALTER TABLE team_member ADD COLUMN reported_to_lead INTEGER NOT NULL DEFAULT 0;`
|
|
15458
15459
|
];
|
|
15459
15460
|
function applyMigrations(db) {
|
|
15460
15461
|
const { user_version: current } = db.query("PRAGMA user_version").get();
|
|
@@ -15576,6 +15577,10 @@ function getUndeliveredMessages(db, teamId) {
|
|
|
15576
15577
|
function markDelivered(db, messageId) {
|
|
15577
15578
|
db.run("UPDATE team_message SET delivered = 1 WHERE id = ?", [messageId]);
|
|
15578
15579
|
}
|
|
15580
|
+
function hasReportedCompletion(db, teamId, memberName) {
|
|
15581
|
+
const row = db.query("SELECT reported_to_lead FROM team_member WHERE team_id = ? AND name = ?").get(teamId, memberName);
|
|
15582
|
+
return row?.reported_to_lead === 1;
|
|
15583
|
+
}
|
|
15579
15584
|
|
|
15580
15585
|
// src/recovery.ts
|
|
15581
15586
|
init_merge_helper();
|
|
@@ -15643,6 +15648,10 @@ async function recoverUndeliveredMessages(db, client3, registry2) {
|
|
|
15643
15648
|
}
|
|
15644
15649
|
if (!recipientSessionId)
|
|
15645
15650
|
continue;
|
|
15651
|
+
if (hasReportedCompletion(db, team.id, msg.to_name)) {
|
|
15652
|
+
markDelivered(db, msg.id);
|
|
15653
|
+
continue;
|
|
15654
|
+
}
|
|
15646
15655
|
try {
|
|
15647
15656
|
await client3.session.promptAsync({
|
|
15648
15657
|
sessionID: recipientSessionId,
|
|
@@ -15778,10 +15787,16 @@ function handleSessionStatusEvent(db, registry2, sessionId, status) {
|
|
|
15778
15787
|
if (member.status === newStatus)
|
|
15779
15788
|
return;
|
|
15780
15789
|
db.run("UPDATE team_member SET status = ?, execution_status = 'idle', time_updated = ? WHERE team_id = ? AND name = ?", [newStatus, Date.now(), entry.teamId, entry.memberName]);
|
|
15790
|
+
if (member.status === "busy" && newStatus === "ready") {
|
|
15791
|
+
const leadMsgCount = db.query("SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND from_name = ? AND to_name = 'lead'").get(entry.teamId, entry.memberName).c;
|
|
15792
|
+
if (leadMsgCount > 0) {
|
|
15793
|
+
db.run("UPDATE team_member SET reported_to_lead = 1 WHERE team_id = ? AND name = ?", [entry.teamId, entry.memberName]);
|
|
15794
|
+
}
|
|
15795
|
+
}
|
|
15781
15796
|
return { memberName: entry.memberName, teamId: entry.teamId, from: member.status, to: newStatus };
|
|
15782
15797
|
} else if (status === "busy") {
|
|
15783
15798
|
if (member.status === "ready" || member.status === "error") {
|
|
15784
|
-
db.run("UPDATE team_member SET status = 'busy', execution_status = 'running', time_updated = ? WHERE team_id = ? AND name = ?", [Date.now(), entry.teamId, entry.memberName]);
|
|
15799
|
+
db.run("UPDATE team_member SET status = 'busy', execution_status = 'running', reported_to_lead = 0, time_updated = ? WHERE team_id = ? AND name = ?", [Date.now(), entry.teamId, entry.memberName]);
|
|
15785
15800
|
return { memberName: entry.memberName, teamId: entry.teamId, from: member.status, to: "busy" };
|
|
15786
15801
|
}
|
|
15787
15802
|
if (member.status === "shutdown_requested") {
|
|
@@ -17189,6 +17204,9 @@ async function executeTeamMessage(deps, args, sessionId) {
|
|
|
17189
17204
|
});
|
|
17190
17205
|
return `Message sent to ${args.to}.`;
|
|
17191
17206
|
}
|
|
17207
|
+
if (hasReportedCompletion(deps.db, teamInfo.teamId, args.to)) {
|
|
17208
|
+
return `Message stored for ${args.to} (teammate has completed their task \u2014 message will not wake them).`;
|
|
17209
|
+
}
|
|
17192
17210
|
const deliveryText = `[Team message from ${senderName}]: ${messageText}`;
|
|
17193
17211
|
deps.client.session.promptAsync({
|
|
17194
17212
|
sessionID: recipientSessionId,
|
|
@@ -17223,7 +17241,12 @@ async function executeTeamBroadcast(deps, args, sessionId) {
|
|
|
17223
17241
|
}
|
|
17224
17242
|
}
|
|
17225
17243
|
let delivered = 0;
|
|
17244
|
+
let skipped = 0;
|
|
17226
17245
|
for (const recipient of recipients) {
|
|
17246
|
+
if (recipient.name !== "lead" && hasReportedCompletion(deps.db, teamInfo.teamId, recipient.name)) {
|
|
17247
|
+
skipped++;
|
|
17248
|
+
continue;
|
|
17249
|
+
}
|
|
17227
17250
|
deps.client.session.promptAsync({
|
|
17228
17251
|
sessionID: recipient.sessionId,
|
|
17229
17252
|
parts: [{ type: "text", text: `[Team broadcast from ${senderName}]: ${args.text}` }]
|
|
@@ -17235,7 +17258,8 @@ async function executeTeamBroadcast(deps, args, sessionId) {
|
|
|
17235
17258
|
log(`team_broadcast:deliver:failed to=${recipient.name} err=${err instanceof Error ? err.message : String(err)}`);
|
|
17236
17259
|
});
|
|
17237
17260
|
}
|
|
17238
|
-
|
|
17261
|
+
const sent = recipients.length - skipped;
|
|
17262
|
+
return `Broadcast sent to ${sent} recipient${sent !== 1 ? "s" : ""}.`;
|
|
17239
17263
|
}
|
|
17240
17264
|
|
|
17241
17265
|
// src/tools/team-shutdown.ts
|
|
@@ -17930,6 +17954,8 @@ var plugin = async (input) => {
|
|
|
17930
17954
|
const tracker = new DescendantTracker;
|
|
17931
17955
|
const nudgedMembers = new Set;
|
|
17932
17956
|
const progressTracker = new ProgressTracker;
|
|
17957
|
+
const wakeLeadTimestamps = new Map;
|
|
17958
|
+
const WAKE_LEAD_COOLDOWN_MS = 5000;
|
|
17933
17959
|
const pluginTransport = input.client._client;
|
|
17934
17960
|
const rawClient = new OpencodeClient({ client: pluginTransport });
|
|
17935
17961
|
initLog(rawClient);
|
|
@@ -18020,7 +18046,7 @@ var plugin = async (input) => {
|
|
|
18020
18046
|
}
|
|
18021
18047
|
}
|
|
18022
18048
|
const nudgeKey = `${transition.teamId}:${transition.memberName}`;
|
|
18023
|
-
if (!nudgedMembers.has(nudgeKey) && shouldNudgeIdleMember(db, transition.teamId, transition.memberName)) {
|
|
18049
|
+
if (!nudgedMembers.has(nudgeKey) && shouldNudgeIdleMember(db, transition.teamId, transition.memberName) && !hasReportedCompletion(db, transition.teamId, transition.memberName)) {
|
|
18024
18050
|
nudgedMembers.add(nudgeKey);
|
|
18025
18051
|
log(`nudge:idle-without-report name=${transition.memberName}`);
|
|
18026
18052
|
client3.session.promptAsync({
|
|
@@ -18063,7 +18089,10 @@ var plugin = async (input) => {
|
|
|
18063
18089
|
const team = db.query("SELECT id FROM team WHERE lead_session_id = ? AND status = 'active'").get(sessionID);
|
|
18064
18090
|
if (team) {
|
|
18065
18091
|
const pending = db.query("SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND to_name = 'lead' AND delivered = 0").get(team.id);
|
|
18066
|
-
|
|
18092
|
+
const allDone = db.query("SELECT COUNT(*) as c FROM team_member WHERE team_id = ? AND status NOT IN ('ready', 'shutdown', 'error')").get(team.id).c === 0;
|
|
18093
|
+
const lastWake = wakeLeadTimestamps.get(team.id) ?? 0;
|
|
18094
|
+
if (pending.c > 0 && !allDone && Date.now() - lastWake > WAKE_LEAD_COOLDOWN_MS) {
|
|
18095
|
+
wakeLeadTimestamps.set(team.id, Date.now());
|
|
18067
18096
|
log(`wake-lead: ${pending.c} pending messages, sending promptAsync`);
|
|
18068
18097
|
client3.session.promptAsync({
|
|
18069
18098
|
sessionID,
|
|
@@ -18076,7 +18105,7 @@ var plugin = async (input) => {
|
|
|
18076
18105
|
const member = db.query(`SELECT tm.team_id, tm.name FROM team_member tm
|
|
18077
18106
|
JOIN team t ON tm.team_id = t.id
|
|
18078
18107
|
WHERE tm.session_id = ? AND t.status = 'active'`).get(sessionID);
|
|
18079
|
-
if (member) {
|
|
18108
|
+
if (member && !hasReportedCompletion(db, member.team_id, member.name)) {
|
|
18080
18109
|
const staleThreshold = Date.now() - 5000;
|
|
18081
18110
|
const peerMsgs = db.query("SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND to_name = ? AND delivered = 0 AND time_created < ?").get(member.team_id, member.name, staleThreshold);
|
|
18082
18111
|
if (peerMsgs.c > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hueyexe/opencode-ensemble",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.1",
|
|
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",
|
package/src/hooks.ts
CHANGED
|
@@ -41,11 +41,26 @@ export function handleSessionStatusEvent(
|
|
|
41
41
|
"UPDATE team_member SET status = ?, execution_status = 'idle', time_updated = ? WHERE team_id = ? AND name = ?",
|
|
42
42
|
[newStatus, Date.now(), entry.teamId, entry.memberName]
|
|
43
43
|
)
|
|
44
|
+
// Mark teammate as having reported if they sent at least one message to lead (issue #3).
|
|
45
|
+
// Set on busy→ready transition so Q&A messages during work don't prematurely block delivery.
|
|
46
|
+
if (member.status === "busy" && newStatus === "ready") {
|
|
47
|
+
const leadMsgCount = (db.query(
|
|
48
|
+
"SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND from_name = ? AND to_name = 'lead'"
|
|
49
|
+
).get(entry.teamId, entry.memberName) as { c: number }).c
|
|
50
|
+
if (leadMsgCount > 0) {
|
|
51
|
+
db.run(
|
|
52
|
+
"UPDATE team_member SET reported_to_lead = 1 WHERE team_id = ? AND name = ?",
|
|
53
|
+
[entry.teamId, entry.memberName]
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
44
57
|
return { memberName: entry.memberName, teamId: entry.teamId, from: member.status, to: newStatus }
|
|
45
58
|
} else if (status === "busy") {
|
|
46
59
|
if (member.status === "ready" || member.status === "error") {
|
|
60
|
+
// Reset reported_to_lead so re-activated teammates can receive messages again (issue #3).
|
|
61
|
+
// INVARIANT: every promptAsync delivery path must check hasReportedCompletion() to prevent loops.
|
|
47
62
|
db.run(
|
|
48
|
-
"UPDATE team_member SET status = 'busy', execution_status = 'running', time_updated = ? WHERE team_id = ? AND name = ?",
|
|
63
|
+
"UPDATE team_member SET status = 'busy', execution_status = 'running', reported_to_lead = 0, time_updated = ? WHERE team_id = ? AND name = ?",
|
|
49
64
|
[Date.now(), entry.teamId, entry.memberName]
|
|
50
65
|
)
|
|
51
66
|
return { memberName: entry.memberName, teamId: entry.teamId, from: member.status, to: "busy" }
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { MemberRegistry, DescendantTracker } from "./state"
|
|
|
10
10
|
import { isWorktreeInstance } from "./util"
|
|
11
11
|
import { handleSessionStatusEvent, handleSessionCreatedEvent, checkToolIsolation, shouldNudgeIdleMember } from "./hooks"
|
|
12
12
|
import { notifyTeamEvent, notifyWorkingProgress } from "./notify"
|
|
13
|
-
import { sendMessage } from "./messaging"
|
|
13
|
+
import { sendMessage, hasReportedCompletion } from "./messaging"
|
|
14
14
|
import { buildLeadSystemPrompt, buildTeammateSystemPrompt, buildTeamCompactionContext } from "./system-prompt"
|
|
15
15
|
import { log, initLog } from "./log"
|
|
16
16
|
import { findTeamBySession } from "./types"
|
|
@@ -58,6 +58,8 @@ const plugin: Plugin = async (input) => {
|
|
|
58
58
|
const tracker = new DescendantTracker()
|
|
59
59
|
const nudgedMembers = new Set<string>()
|
|
60
60
|
const progressTracker = new ProgressTracker()
|
|
61
|
+
const wakeLeadTimestamps = new Map<string, number>()
|
|
62
|
+
const WAKE_LEAD_COOLDOWN_MS = 5000
|
|
61
63
|
|
|
62
64
|
// Extract the working HeyAPI transport from the plugin-provided v1 client and pass it
|
|
63
65
|
// to the v2 OpencodeClient. The plugin framework provides a v1 client which stores its
|
|
@@ -176,8 +178,9 @@ const plugin: Plugin = async (input) => {
|
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
// Nudge teammate if they went idle without reporting to the lead (once only)
|
|
181
|
+
// Skip if they already reported completion (issue #3 — prevents re-waking completed teammates)
|
|
179
182
|
const nudgeKey = `${transition.teamId}:${transition.memberName}`
|
|
180
|
-
if (!nudgedMembers.has(nudgeKey) && shouldNudgeIdleMember(db, transition.teamId, transition.memberName)) {
|
|
183
|
+
if (!nudgedMembers.has(nudgeKey) && shouldNudgeIdleMember(db, transition.teamId, transition.memberName) && !hasReportedCompletion(db, transition.teamId, transition.memberName)) {
|
|
181
184
|
nudgedMembers.add(nudgeKey)
|
|
182
185
|
log(`nudge:idle-without-report name=${transition.memberName}`)
|
|
183
186
|
client.session.promptAsync({
|
|
@@ -232,7 +235,13 @@ const plugin: Plugin = async (input) => {
|
|
|
232
235
|
const team = db.query("SELECT id FROM team WHERE lead_session_id = ? AND status = 'active'").get(sessionID) as { id: string } | null
|
|
233
236
|
if (team) {
|
|
234
237
|
const pending = db.query("SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND to_name = 'lead' AND delivered = 0").get(team.id) as { c: number }
|
|
235
|
-
if (
|
|
238
|
+
// Skip wake if all teammates are done or if we woke recently (issue #3 — breaks completion loop)
|
|
239
|
+
const allDone = (db.query(
|
|
240
|
+
"SELECT COUNT(*) as c FROM team_member WHERE team_id = ? AND status NOT IN ('ready', 'shutdown', 'error')"
|
|
241
|
+
).get(team.id) as { c: number }).c === 0
|
|
242
|
+
const lastWake = wakeLeadTimestamps.get(team.id) ?? 0
|
|
243
|
+
if (pending.c > 0 && !allDone && Date.now() - lastWake > WAKE_LEAD_COOLDOWN_MS) {
|
|
244
|
+
wakeLeadTimestamps.set(team.id, Date.now())
|
|
236
245
|
log(`wake-lead: ${pending.c} pending messages, sending promptAsync`)
|
|
237
246
|
client.session.promptAsync({
|
|
238
247
|
sessionID,
|
|
@@ -250,7 +259,7 @@ const plugin: Plugin = async (input) => {
|
|
|
250
259
|
JOIN team t ON tm.team_id = t.id
|
|
251
260
|
WHERE tm.session_id = ? AND t.status = 'active'`
|
|
252
261
|
).get(sessionID) as { team_id: string; name: string } | null
|
|
253
|
-
if (member) {
|
|
262
|
+
if (member && !hasReportedCompletion(db, member.team_id, member.name)) {
|
|
254
263
|
const staleThreshold = Date.now() - 5000
|
|
255
264
|
const peerMsgs = db.query(
|
|
256
265
|
"SELECT COUNT(*) as c FROM team_message WHERE team_id = ? AND to_name = ? AND delivered = 0 AND time_created < ?"
|
package/src/messaging.ts
CHANGED
|
@@ -76,3 +76,14 @@ export function getUndeliveredMessages(db: Database, teamId: string): MessageRow
|
|
|
76
76
|
export function markDelivered(db: Database, messageId: string): void {
|
|
77
77
|
db.run("UPDATE team_message SET delivered = 1 WHERE id = ?", [messageId])
|
|
78
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if a teammate has reported completion to the lead.
|
|
82
|
+
* Returns true if the reported_to_lead flag is set on the member.
|
|
83
|
+
*/
|
|
84
|
+
export function hasReportedCompletion(db: Database, teamId: string, memberName: string): boolean {
|
|
85
|
+
const row = db.query(
|
|
86
|
+
"SELECT reported_to_lead FROM team_member WHERE team_id = ? AND name = ?"
|
|
87
|
+
).get(teamId, memberName) as { reported_to_lead: number } | null
|
|
88
|
+
return row?.reported_to_lead === 1
|
|
89
|
+
}
|
package/src/recovery.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
2
|
import type { PluginClient } from "./types"
|
|
3
3
|
import type { MemberRegistry } from "./state"
|
|
4
|
-
import { getUndeliveredMessages, markDelivered } from "./messaging"
|
|
4
|
+
import { getUndeliveredMessages, markDelivered, hasReportedCompletion } from "./messaging"
|
|
5
5
|
import { preserveBranch, preservedBranchName } from "./tools/merge-helper"
|
|
6
6
|
import { log } from "./log"
|
|
7
7
|
|
|
@@ -123,6 +123,12 @@ export async function recoverUndeliveredMessages(
|
|
|
123
123
|
|
|
124
124
|
if (!recipientSessionId) continue
|
|
125
125
|
|
|
126
|
+
// Skip delivery to teammates who have already reported completion (issue #3)
|
|
127
|
+
if (hasReportedCompletion(db, team.id, msg.to_name!)) {
|
|
128
|
+
markDelivered(db, msg.id)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
126
132
|
try {
|
|
127
133
|
await client.session.promptAsync({
|
|
128
134
|
sessionID: recipientSessionId,
|
package/src/schema.ts
CHANGED
|
@@ -80,6 +80,8 @@ export const MIGRATIONS: string[] = [
|
|
|
80
80
|
`ALTER TABLE team ADD COLUMN lead_agent TEXT;`,
|
|
81
81
|
// Migration 6: Track workspace ID for worktree-session binding
|
|
82
82
|
`ALTER TABLE team_member ADD COLUMN workspace_id TEXT;`,
|
|
83
|
+
// Migration 7: Track whether teammate has reported to lead (completion loop prevention, issue #3)
|
|
84
|
+
`ALTER TABLE team_member ADD COLUMN reported_to_lead INTEGER NOT NULL DEFAULT 0;`,
|
|
83
85
|
]
|
|
84
86
|
|
|
85
87
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ToolDeps } from "../types"
|
|
2
2
|
import { requireTeamMember } from "./shared"
|
|
3
|
-
import { broadcastMessage, markDelivered } from "../messaging"
|
|
3
|
+
import { broadcastMessage, markDelivered, hasReportedCompletion } from "../messaging"
|
|
4
4
|
import { log } from "../log"
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -40,8 +40,14 @@ export async function executeTeamBroadcast(
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Fire-and-forget: deliver to all recipients. Message is already persisted in DB.
|
|
43
|
+
// Skip completed teammates to prevent re-waking them (issue #3).
|
|
43
44
|
let delivered = 0
|
|
45
|
+
let skipped = 0
|
|
44
46
|
for (const recipient of recipients) {
|
|
47
|
+
if (recipient.name !== "lead" && hasReportedCompletion(deps.db, teamInfo.teamId, recipient.name)) {
|
|
48
|
+
skipped++
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
45
51
|
deps.client.session.promptAsync({
|
|
46
52
|
sessionID: recipient.sessionId,
|
|
47
53
|
parts: [{ type: "text", text: `[Team broadcast from ${senderName}]: ${args.text}` }],
|
|
@@ -53,5 +59,6 @@ export async function executeTeamBroadcast(
|
|
|
53
59
|
})
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
const sent = recipients.length - skipped
|
|
63
|
+
return `Broadcast sent to ${sent} recipient${sent !== 1 ? "s" : ""}.`
|
|
57
64
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ToolDeps } from "../types"
|
|
2
2
|
import { resolveRecipientSession } from "../types"
|
|
3
3
|
import { requireTeamMember } from "./shared"
|
|
4
|
-
import { sendMessage, markDelivered } from "../messaging"
|
|
4
|
+
import { sendMessage, markDelivered, hasReportedCompletion } from "../messaging"
|
|
5
5
|
import { log } from "../log"
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -90,6 +90,11 @@ export async function executeTeamMessage(
|
|
|
90
90
|
return `Message sent to ${args.to}.`
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// Guard: skip promptAsync delivery to teammates who have already reported completion (issue #3)
|
|
94
|
+
if (hasReportedCompletion(deps.db, teamInfo.teamId, args.to)) {
|
|
95
|
+
return `Message stored for ${args.to} (teammate has completed their task — message will not wake them).`
|
|
96
|
+
}
|
|
97
|
+
|
|
93
98
|
// For member-to-member messages, fire-and-forget delivery is safe.
|
|
94
99
|
const deliveryText = `[Team message from ${senderName}]: ${messageText}`
|
|
95
100
|
deps.client.session.promptAsync({
|