bosun 0.42.0 → 0.42.2
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/.env.example +12 -0
- package/README.md +2 -0
- package/agent/agent-pool.mjs +34 -1
- package/agent/agent-work-report.mjs +89 -3
- package/agent/analyze-agent-work-helpers.mjs +14 -0
- package/agent/analyze-agent-work.mjs +23 -3
- package/agent/primary-agent.mjs +23 -1
- package/bosun-tui.mjs +4 -3
- package/bosun.schema.json +1 -1
- package/config/config.mjs +58 -0
- package/config/workspace-health.mjs +36 -6
- package/git/diff-stats.mjs +550 -124
- package/github/github-app-auth.mjs +9 -5
- package/infra/maintenance.mjs +13 -6
- package/infra/monitor.mjs +398 -10
- package/infra/runtime-accumulator.mjs +9 -1
- package/infra/session-tracker.mjs +163 -1
- package/infra/tui-bridge.mjs +415 -0
- package/infra/worktree-recovery-state.mjs +159 -0
- package/kanban/kanban-adapter.mjs +41 -8
- package/lib/repo-map.mjs +411 -0
- package/package.json +140 -137
- package/server/ui-server.mjs +953 -59
- package/shell/codex-config.mjs +34 -8
- package/task/task-cli.mjs +93 -19
- package/task/task-executor.mjs +397 -8
- package/task/task-store.mjs +194 -1
- package/telegram/telegram-bot.mjs +267 -18
- package/tools/vitest-runner.mjs +108 -0
- package/tui/app.mjs +252 -148
- package/tui/components/status-header.mjs +88 -131
- package/tui/lib/ws-bridge.mjs +125 -35
- package/tui/screens/agents-screen-helpers.mjs +219 -0
- package/tui/screens/agents.mjs +287 -270
- package/tui/screens/status.mjs +51 -189
- package/tui/screens/tasks.mjs +41 -253
- package/ui/app.js +52 -23
- package/ui/components/chat-view.js +263 -84
- package/ui/components/diff-viewer.js +324 -140
- package/ui/components/kanban-board.js +13 -9
- package/ui/components/session-list.js +111 -41
- package/ui/demo-defaults.js +481 -59
- package/ui/demo.html +32 -0
- package/ui/modules/session-api.js +320 -5
- package/ui/modules/stream-timeline.js +356 -0
- package/ui/modules/telegram.js +5 -2
- package/ui/modules/worktree-recovery.js +85 -0
- package/ui/styles.css +44 -0
- package/ui/tabs/chat.js +19 -4
- package/ui/tabs/dashboard.js +22 -0
- package/ui/tabs/infra.js +25 -0
- package/ui/tabs/tasks.js +119 -11
- package/voice/voice-auth-manager.mjs +10 -5
- package/workflow/workflow-engine.mjs +179 -1
- package/workflow/workflow-nodes.mjs +872 -16
- package/workflow/workflow-templates.mjs +4 -0
- package/workflow-templates/github.mjs +2 -1
- package/workflow-templates/planning.mjs +2 -1
- package/workflow-templates/sub-workflows.mjs +10 -0
- package/workflow-templates/task-batch.mjs +9 -8
- package/workflow-templates/task-execution.mjs +30 -12
- package/workflow-templates/task-lifecycle.mjs +59 -4
- package/workspace/shared-knowledge.mjs +409 -155
|
@@ -114,6 +114,7 @@ function resolveSessionMaxMessages(type, metadata, explicitMax, fallbackMax) {
|
|
|
114
114
|
const FLUSH_INTERVAL_MS = 2000;
|
|
115
115
|
|
|
116
116
|
const SESSION_EVENT_LISTENERS = new Set();
|
|
117
|
+
const SESSION_STATE_LISTENERS = new Set();
|
|
117
118
|
|
|
118
119
|
export function addSessionEventListener(listener) {
|
|
119
120
|
if (typeof listener !== "function") return () => {};
|
|
@@ -121,6 +122,12 @@ export function addSessionEventListener(listener) {
|
|
|
121
122
|
return () => SESSION_EVENT_LISTENERS.delete(listener);
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
export function addSessionStateListener(listener) {
|
|
126
|
+
if (typeof listener !== "function") return () => {};
|
|
127
|
+
SESSION_STATE_LISTENERS.add(listener);
|
|
128
|
+
return () => SESSION_STATE_LISTENERS.delete(listener);
|
|
129
|
+
}
|
|
130
|
+
|
|
124
131
|
function emitSessionEvent(session, message) {
|
|
125
132
|
if (!session || !message || SESSION_EVENT_LISTENERS.size === 0) return;
|
|
126
133
|
const payload = {
|
|
@@ -145,6 +152,37 @@ function emitSessionEvent(session, message) {
|
|
|
145
152
|
}
|
|
146
153
|
}
|
|
147
154
|
|
|
155
|
+
function emitSessionStateEvent(session, reason, extra = {}) {
|
|
156
|
+
if (!session || SESSION_STATE_LISTENERS.size === 0) return;
|
|
157
|
+
const normalizedReason = String(reason || "updated").trim() || "updated";
|
|
158
|
+
const payload = {
|
|
159
|
+
sessionId: session.id || session.taskId,
|
|
160
|
+
taskId: session.taskId || session.id,
|
|
161
|
+
reason: normalizedReason,
|
|
162
|
+
session: {
|
|
163
|
+
id: session.id || session.taskId,
|
|
164
|
+
taskId: session.taskId || session.id,
|
|
165
|
+
type: session.type || "task",
|
|
166
|
+
status: session.status || "active",
|
|
167
|
+
lastActiveAt: session.lastActiveAt || new Date().toISOString(),
|
|
168
|
+
turnCount: session.turnCount || 0,
|
|
169
|
+
title: session.taskTitle || session.title || null,
|
|
170
|
+
},
|
|
171
|
+
event: {
|
|
172
|
+
kind: "state",
|
|
173
|
+
reason: normalizedReason,
|
|
174
|
+
...extra,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
for (const listener of SESSION_STATE_LISTENERS) {
|
|
178
|
+
try {
|
|
179
|
+
listener(payload);
|
|
180
|
+
} catch {
|
|
181
|
+
// best-effort listeners
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
148
186
|
// ── SessionTracker Class ────────────────────────────────────────────────────
|
|
149
187
|
|
|
150
188
|
export class SessionTracker {
|
|
@@ -226,8 +264,12 @@ export class SessionTracker {
|
|
|
226
264
|
lastActivityAt: Date.now(),
|
|
227
265
|
metadata: {},
|
|
228
266
|
insights: buildSessionInsights({ messages: [] }),
|
|
267
|
+
trajectory: { version: 1, replayable: true, steps: [] },
|
|
268
|
+
summary: null,
|
|
229
269
|
});
|
|
270
|
+
const session = this.#sessions.get(taskId);
|
|
230
271
|
this.#markDirty(taskId);
|
|
272
|
+
emitSessionStateEvent(session, "session-created", { title: taskTitle || taskId });
|
|
231
273
|
}
|
|
232
274
|
|
|
233
275
|
/**
|
|
@@ -279,6 +321,7 @@ export class SessionTracker {
|
|
|
279
321
|
if (Number.isFinite(maxMessages) && maxMessages > 0) {
|
|
280
322
|
while (session.messages.length > maxMessages) session.messages.shift();
|
|
281
323
|
}
|
|
324
|
+
this.#appendTrajectoryStep(session, event);
|
|
282
325
|
this.#refreshDerivedState(session);
|
|
283
326
|
this.#markDirty(taskId);
|
|
284
327
|
emitSessionEvent(session, msg);
|
|
@@ -312,6 +355,7 @@ export class SessionTracker {
|
|
|
312
355
|
if (Number.isFinite(maxMessages) && maxMessages > 0) {
|
|
313
356
|
while (session.messages.length > maxMessages) session.messages.shift();
|
|
314
357
|
}
|
|
358
|
+
this.#appendTrajectoryStep(session, event);
|
|
315
359
|
this.#refreshDerivedState(session);
|
|
316
360
|
this.#markDirty(taskId);
|
|
317
361
|
emitSessionEvent(session, msg);
|
|
@@ -329,6 +373,7 @@ export class SessionTracker {
|
|
|
329
373
|
if (Number.isFinite(maxMessages) && maxMessages > 0) {
|
|
330
374
|
while (session.messages.length > maxMessages) session.messages.shift();
|
|
331
375
|
}
|
|
376
|
+
this.#appendTrajectoryStep(session, event);
|
|
332
377
|
this.#refreshDerivedState(session);
|
|
333
378
|
this.#markDirty(taskId);
|
|
334
379
|
emitSessionEvent(session, msg);
|
|
@@ -348,6 +393,7 @@ export class SessionTracker {
|
|
|
348
393
|
this.#refreshDerivedState(session);
|
|
349
394
|
this.#accumulateCompletedSession(session, taskId);
|
|
350
395
|
this.#markDirty(taskId);
|
|
396
|
+
emitSessionStateEvent(session, "session-ended", { status });
|
|
351
397
|
}
|
|
352
398
|
|
|
353
399
|
/**
|
|
@@ -578,10 +624,13 @@ export class SessionTracker {
|
|
|
578
624
|
metadata,
|
|
579
625
|
maxMessages: resolvedMax,
|
|
580
626
|
insights: buildSessionInsights({ messages: [] }),
|
|
627
|
+
trajectory: { version: 1, replayable: true, steps: [] },
|
|
628
|
+
summary: null,
|
|
581
629
|
};
|
|
582
630
|
this.#sessions.set(id, session);
|
|
583
631
|
this.#markDirty(id);
|
|
584
632
|
this.#flushDirty(); // immediate write for create
|
|
633
|
+
emitSessionStateEvent(session, "session-created");
|
|
585
634
|
return session;
|
|
586
635
|
}
|
|
587
636
|
|
|
@@ -599,18 +648,23 @@ export class SessionTracker {
|
|
|
599
648
|
const derivedStatus = progress?.status === "ended"
|
|
600
649
|
? "completed"
|
|
601
650
|
: (progress?.status || s.status);
|
|
651
|
+
const lastActiveAt = s.lastActiveAt || new Date(s.lastActivityAt).toISOString();
|
|
602
652
|
list.push({
|
|
603
653
|
id: s.id || s.taskId,
|
|
604
654
|
taskId: s.taskId,
|
|
605
655
|
title: s.taskTitle || s.title || null,
|
|
606
656
|
type: s.type || "task",
|
|
607
657
|
status: derivedStatus,
|
|
658
|
+
lifecycleStatus: s.status || "active",
|
|
659
|
+
runtimeState: progress?.status || null,
|
|
660
|
+
runtimeUpdatedAt: lastActiveAt,
|
|
661
|
+
runtimeIsLive: Boolean(progress && progress.status !== "ended" && progress.status !== "not_found"),
|
|
608
662
|
workspaceId: String(s?.metadata?.workspaceId || "").trim() || null,
|
|
609
663
|
workspaceDir: String(s?.metadata?.workspaceDir || "").trim() || null,
|
|
610
664
|
branch: String(s?.metadata?.branch || "").trim() || null,
|
|
611
665
|
turnCount: s.turnCount || 0,
|
|
612
666
|
createdAt: s.createdAt || new Date(s.startedAt).toISOString(),
|
|
613
|
-
lastActiveAt
|
|
667
|
+
lastActiveAt,
|
|
614
668
|
idleMs: progress?.idleMs ?? 0,
|
|
615
669
|
elapsedMs: progress?.elapsedMs ?? Math.max(0, Date.now() - Number(s.startedAt || Date.now())),
|
|
616
670
|
recommendation: progress?.recommendation || "none",
|
|
@@ -658,6 +712,7 @@ export class SessionTracker {
|
|
|
658
712
|
this.#refreshDerivedState(session);
|
|
659
713
|
this.#accumulateCompletedSession(session, sessionId);
|
|
660
714
|
this.#markDirty(sessionId);
|
|
715
|
+
emitSessionStateEvent(session, "session-status", { status });
|
|
661
716
|
}
|
|
662
717
|
|
|
663
718
|
/**
|
|
@@ -671,6 +726,7 @@ export class SessionTracker {
|
|
|
671
726
|
session.taskTitle = newTitle;
|
|
672
727
|
session.title = newTitle;
|
|
673
728
|
this.#markDirty(sessionId);
|
|
729
|
+
emitSessionStateEvent(session, "session-renamed", { title: newTitle });
|
|
674
730
|
}
|
|
675
731
|
|
|
676
732
|
/**
|
|
@@ -748,6 +804,13 @@ export class SessionTracker {
|
|
|
748
804
|
this.#flushDirty();
|
|
749
805
|
}
|
|
750
806
|
|
|
807
|
+
/**
|
|
808
|
+
* Flush all dirty sessions to disk immediately (alias for flush).
|
|
809
|
+
*/
|
|
810
|
+
flushNow() {
|
|
811
|
+
this.#flushDirty();
|
|
812
|
+
}
|
|
813
|
+
|
|
751
814
|
/**
|
|
752
815
|
* Stop all timers and flush pending writes (for cleanup).
|
|
753
816
|
*/
|
|
@@ -894,6 +957,7 @@ export class SessionTracker {
|
|
|
894
957
|
this.#refreshDerivedState(session);
|
|
895
958
|
this.#accumulateCompletedSession(session, id);
|
|
896
959
|
this.#markDirty(id);
|
|
960
|
+
emitSessionStateEvent(session, "session-idle-timeout", { status: session.status, idleMs });
|
|
897
961
|
reaped++;
|
|
898
962
|
}
|
|
899
963
|
}
|
|
@@ -953,6 +1017,84 @@ export class SessionTracker {
|
|
|
953
1017
|
return true;
|
|
954
1018
|
}
|
|
955
1019
|
|
|
1020
|
+
#appendTrajectoryStep(session, event) {
|
|
1021
|
+
if (!session) return;
|
|
1022
|
+
if (!session.trajectory) {
|
|
1023
|
+
session.trajectory = { version: 1, replayable: true, steps: [] };
|
|
1024
|
+
}
|
|
1025
|
+
const step = this.#extractTrajectoryStep(event, session);
|
|
1026
|
+
if (step) {
|
|
1027
|
+
session.trajectory.steps.push(step);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
#extractTrajectoryStep(event, session) {
|
|
1032
|
+
const ts = new Date().toISOString();
|
|
1033
|
+
const id = `step-${Date.now()}-${randomToken(6)}`;
|
|
1034
|
+
|
|
1035
|
+
// String event
|
|
1036
|
+
if (typeof event === "string") {
|
|
1037
|
+
return { id, kind: "system", summary: event.trim().slice(0, 200), timestamp: ts };
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Direct message format (role/content)
|
|
1041
|
+
if (event?.role && event?.content !== undefined) {
|
|
1042
|
+
const content = String(event.content).slice(0, 200);
|
|
1043
|
+
const timestamp = event.timestamp || ts;
|
|
1044
|
+
if (event.role === "user") return { id, kind: "user_message", summary: content, timestamp };
|
|
1045
|
+
if (event.role === "assistant") return { id, kind: "assistant", summary: content, timestamp };
|
|
1046
|
+
return { id, kind: event.role, summary: content, timestamp };
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// SDK item.started events
|
|
1050
|
+
if (event?.type === "item.started" && event?.item) {
|
|
1051
|
+
const item = event.item;
|
|
1052
|
+
if (item.type === "command_execution") {
|
|
1053
|
+
return { id, kind: "tool_call", summary: `Ran ${item.command || "unknown"}`, timestamp: ts };
|
|
1054
|
+
}
|
|
1055
|
+
if (item.type === "reasoning") {
|
|
1056
|
+
return { id, kind: "reasoning", summary: item.text || "", timestamp: ts };
|
|
1057
|
+
}
|
|
1058
|
+
if (item.type === "function_call" || item.type === "mcp_tool_call") {
|
|
1059
|
+
return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: ts };
|
|
1060
|
+
}
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// SDK item.completed events
|
|
1065
|
+
if (event?.type === "item.completed" && event?.item) {
|
|
1066
|
+
const item = event.item;
|
|
1067
|
+
if (item.type === "reasoning") {
|
|
1068
|
+
return { id, kind: "reasoning", summary: item.text || "", timestamp: ts };
|
|
1069
|
+
}
|
|
1070
|
+
if (item.type === "function_call" || item.type === "mcp_tool_call") {
|
|
1071
|
+
return { id, kind: "tool_call", summary: `${item.name || "call"} ${item.arguments || ""}`.trim(), timestamp: ts };
|
|
1072
|
+
}
|
|
1073
|
+
if (item.type === "command_execution") {
|
|
1074
|
+
const cmd = item.command || "";
|
|
1075
|
+
const hasPriorStart = (session?.trajectory?.steps || []).some(
|
|
1076
|
+
(s) => s.kind === "tool_call" && s.summary === `Ran ${cmd}`,
|
|
1077
|
+
);
|
|
1078
|
+
if (hasPriorStart) {
|
|
1079
|
+
return { id, kind: "tool_result", summary: `${cmd} (exit ${item.exit_code ?? "?"})`, timestamp: ts };
|
|
1080
|
+
}
|
|
1081
|
+
return { id, kind: "command", summary: cmd, timestamp: ts };
|
|
1082
|
+
}
|
|
1083
|
+
if (item.type === "agent_message") {
|
|
1084
|
+
return { id, kind: "assistant", summary: item.text || "", timestamp: ts };
|
|
1085
|
+
}
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Assistant message events
|
|
1090
|
+
if (event?.type === "assistant.message") {
|
|
1091
|
+
const content = event?.data?.content || event?.content || "";
|
|
1092
|
+
return { id, kind: "agent_message", summary: content.slice(0, 200), timestamp: ts };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
956
1098
|
#refreshDerivedState(session) {
|
|
957
1099
|
if (!session) return;
|
|
958
1100
|
try {
|
|
@@ -963,6 +1105,22 @@ export class SessionTracker {
|
|
|
963
1105
|
} catch {
|
|
964
1106
|
// Inspector insights are best-effort only.
|
|
965
1107
|
}
|
|
1108
|
+
try {
|
|
1109
|
+
const steps = session.trajectory?.steps || [];
|
|
1110
|
+
const totalSteps = steps.length;
|
|
1111
|
+
const isFailed = session.status === "failed";
|
|
1112
|
+
const isLong = totalSteps > 12;
|
|
1113
|
+
const failedOrLongRun = isFailed || isLong;
|
|
1114
|
+
const resumable = failedOrLongRun;
|
|
1115
|
+
const shortSteps = steps.slice(-12).map((s) => ({ kind: s.kind, summary: s.summary }));
|
|
1116
|
+
const latestStep =
|
|
1117
|
+
steps.length > 0
|
|
1118
|
+
? { kind: steps[steps.length - 1].kind, summary: steps[steps.length - 1].summary }
|
|
1119
|
+
: null;
|
|
1120
|
+
session.summary = { failedOrLongRun, resumable, totalSteps, shortSteps, latestStep };
|
|
1121
|
+
} catch {
|
|
1122
|
+
// Summary computation is best-effort only.
|
|
1123
|
+
}
|
|
966
1124
|
}
|
|
967
1125
|
|
|
968
1126
|
#ensureDir() {
|
|
@@ -1002,6 +1160,8 @@ export class SessionTracker {
|
|
|
1002
1160
|
messages: session.messages || [],
|
|
1003
1161
|
metadata: session.metadata || {},
|
|
1004
1162
|
insights: session.insights || null,
|
|
1163
|
+
trajectory: session.trajectory || null,
|
|
1164
|
+
summary: session.summary || null,
|
|
1005
1165
|
};
|
|
1006
1166
|
writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
1007
1167
|
} catch (err) {
|
|
@@ -1081,6 +1241,8 @@ export class SessionTracker {
|
|
|
1081
1241
|
lastActivityAt: lastActive || Date.now(),
|
|
1082
1242
|
metadata: data.metadata || {},
|
|
1083
1243
|
insights: data.insights || buildSessionInsights({ messages: data.messages || [] }),
|
|
1244
|
+
trajectory: data.trajectory || { version: 1, replayable: true, steps: [] },
|
|
1245
|
+
summary: data.summary || null,
|
|
1084
1246
|
});
|
|
1085
1247
|
const restored = this.#sessions.get(id);
|
|
1086
1248
|
if (restored && isTerminalSessionStatus(restored.status) && !restored.accumulatedAt) {
|