@hueyexe/opencode-ensemble 0.7.1 → 0.8.0
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 +66 -11
- package/dist/index.js +304 -20
- package/package.json +1 -1
- package/src/config.ts +75 -0
- package/src/index.ts +34 -13
- package/src/progress.ts +85 -0
- package/src/system-prompt.ts +61 -1
- package/src/tools/team-cleanup.ts +83 -6
- package/src/tools/team-spawn.ts +15 -0
- package/src/tools/team-status.ts +25 -3
- package/src/watchdog.ts +67 -0
package/src/watchdog.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
2
|
import type { PluginClient } from "./types"
|
|
3
3
|
import type { MemberRegistry } from "./state"
|
|
4
|
+
import type { ProgressTracker } from "./progress"
|
|
5
|
+
import { sendMessage } from "./messaging"
|
|
4
6
|
|
|
5
7
|
interface WatchdogOpts {
|
|
6
8
|
db: Database
|
|
@@ -10,6 +12,14 @@ interface WatchdogOpts {
|
|
|
10
12
|
ttlMs: number
|
|
11
13
|
/** How often to run the check. Defaults to 60s. */
|
|
12
14
|
checkIntervalMs?: number
|
|
15
|
+
/** Progress tracker for stall detection. */
|
|
16
|
+
progressTracker?: ProgressTracker
|
|
17
|
+
/** Stall detection threshold in ms. 0 disables. */
|
|
18
|
+
stallThresholdMs?: number
|
|
19
|
+
/** Min steps before token-based stall check. */
|
|
20
|
+
stallMinSteps?: number
|
|
21
|
+
/** Output token threshold for stall detection. */
|
|
22
|
+
stallTokenThreshold?: number
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
/**
|
|
@@ -22,6 +32,10 @@ export class Watchdog {
|
|
|
22
32
|
private readonly registry: MemberRegistry
|
|
23
33
|
private readonly ttlMs: number
|
|
24
34
|
private readonly checkIntervalMs: number
|
|
35
|
+
private readonly progressTracker?: ProgressTracker
|
|
36
|
+
private readonly stallThresholdMs: number
|
|
37
|
+
private readonly stallMinSteps: number
|
|
38
|
+
private readonly stallTokenThreshold: number
|
|
25
39
|
private timer: ReturnType<typeof setInterval> | undefined
|
|
26
40
|
|
|
27
41
|
constructor(opts: WatchdogOpts) {
|
|
@@ -30,6 +44,10 @@ export class Watchdog {
|
|
|
30
44
|
this.registry = opts.registry
|
|
31
45
|
this.ttlMs = opts.ttlMs
|
|
32
46
|
this.checkIntervalMs = opts.checkIntervalMs ?? 60_000
|
|
47
|
+
this.progressTracker = opts.progressTracker
|
|
48
|
+
this.stallThresholdMs = opts.stallThresholdMs ?? 0
|
|
49
|
+
this.stallMinSteps = opts.stallMinSteps ?? 3
|
|
50
|
+
this.stallTokenThreshold = opts.stallTokenThreshold ?? 500
|
|
33
51
|
}
|
|
34
52
|
|
|
35
53
|
private static STALE_THRESHOLD_MS = Number(process.env.STALE_WORKTREE_THRESHOLD_MS) || 300_000
|
|
@@ -61,9 +79,58 @@ export class Watchdog {
|
|
|
61
79
|
}
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
/** Check for stalled busy members and escalate to lead + nudge teammate. */
|
|
83
|
+
async checkStalled(): Promise<void> {
|
|
84
|
+
if (!this.progressTracker || this.stallThresholdMs === 0) return
|
|
85
|
+
|
|
86
|
+
const busy = this.db.query(
|
|
87
|
+
`SELECT tm.team_id, tm.name, tm.session_id
|
|
88
|
+
FROM team_member tm
|
|
89
|
+
JOIN team t ON tm.team_id = t.id
|
|
90
|
+
WHERE t.status = 'active' AND tm.status = 'busy'`
|
|
91
|
+
).all() as Array<{ team_id: string; name: string; session_id: string }>
|
|
92
|
+
|
|
93
|
+
for (const member of busy) {
|
|
94
|
+
if (this.progressTracker.isReported(member.session_id)) continue
|
|
95
|
+
|
|
96
|
+
const tokenStalled = this.progressTracker.isTokenStalled(member.session_id, this.stallMinSteps, this.stallTokenThreshold)
|
|
97
|
+
const timeStalled = this.progressTracker.isTimeStalled(member.session_id, this.stallThresholdMs)
|
|
98
|
+
|
|
99
|
+
if (!tokenStalled && !timeStalled) continue
|
|
100
|
+
|
|
101
|
+
this.progressTracker.markReported(member.session_id)
|
|
102
|
+
const reason = tokenStalled ? "low output tokens" : "no communication"
|
|
103
|
+
|
|
104
|
+
// Nudge the teammate directly
|
|
105
|
+
this.client.session.promptAsync({
|
|
106
|
+
sessionID: member.session_id,
|
|
107
|
+
parts: [{ type: "text", text: "[System]: You appear stalled — no progress detected. Report your current status to the lead via team_message, or wrap up your work." }],
|
|
108
|
+
}).catch(() => { /* best effort */ })
|
|
109
|
+
|
|
110
|
+
// Notify the lead
|
|
111
|
+
sendMessage(this.db, {
|
|
112
|
+
teamId: member.team_id,
|
|
113
|
+
from: "system",
|
|
114
|
+
to: "lead",
|
|
115
|
+
content: `Teammate "${member.name}" appears stalled (${reason}). Consider checking on them via team_message or shutting them down.`,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Toast for the user
|
|
119
|
+
try {
|
|
120
|
+
await this.client.tui.showToast({
|
|
121
|
+
title: "Team",
|
|
122
|
+
message: `${member.name} appears stalled`,
|
|
123
|
+
variant: "warning",
|
|
124
|
+
duration: 5000,
|
|
125
|
+
})
|
|
126
|
+
} catch { /* TUI may not be available */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
64
130
|
/** Run a single check for stale busy members. */
|
|
65
131
|
async check(): Promise<void> {
|
|
66
132
|
await this.cleanupStaleWorktrees()
|
|
133
|
+
await this.checkStalled()
|
|
67
134
|
if (this.ttlMs === 0) return
|
|
68
135
|
|
|
69
136
|
const cutoff = Date.now() - this.ttlMs
|