@towles/tool 0.0.129 → 0.0.131
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/package.json +1 -1
- package/packages/agentboard/apps/tui/src/components/SessionCard.tsx +54 -49
- package/packages/agentboard/apps/tui/src/index.tsx +45 -35
- package/packages/agentboard/packages/runtime/src/agents/tracker.test.ts +103 -0
- package/packages/agentboard/packages/runtime/src/agents/tracker.ts +50 -0
- package/packages/agentboard/packages/runtime/src/server/index.ts +3 -0
- package/packages/agentboard/packages/runtime/src/shared.ts +1 -0
- package/packages/core/.claude-plugin/plugin.json +1 -1
- package/src/commands/graph/analyzer.test.ts +8 -2
- package/src/commands/graph.test.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSignal, For, Show, onCleanup } from "solid-js";
|
|
2
2
|
import type { Accessor } from "solid-js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { SessionData, Theme } from "@tt-agentboard/runtime";
|
|
4
4
|
import { truncate } from "@tt-agentboard/runtime";
|
|
5
5
|
import { UNSEEN_ICON, BOLD, DIM, toneColor } from "../constants";
|
|
6
6
|
import { DiffStats } from "./DiffStats";
|
|
@@ -9,16 +9,6 @@ import { formatElapsed } from "./elapsed";
|
|
|
9
9
|
import { liveStatusIcon, unseenTerminalColor } from "./status-visuals";
|
|
10
10
|
import { familyColor } from "./family-color";
|
|
11
11
|
|
|
12
|
-
const STATUS_TEXT: Record<AgentStatus, string> = {
|
|
13
|
-
idle: "",
|
|
14
|
-
running: "running",
|
|
15
|
-
done: "done",
|
|
16
|
-
error: "error",
|
|
17
|
-
waiting: "waiting",
|
|
18
|
-
question: "question",
|
|
19
|
-
interrupted: "stopped",
|
|
20
|
-
};
|
|
21
|
-
|
|
22
12
|
export interface SessionCardProps {
|
|
23
13
|
session: SessionData;
|
|
24
14
|
isFocused: boolean;
|
|
@@ -77,7 +67,7 @@ export function SessionCard(props: SessionCardProps) {
|
|
|
77
67
|
};
|
|
78
68
|
|
|
79
69
|
const truncName = () => truncate(props.session.name, 18);
|
|
80
|
-
const truncBranch = () => (props.session.branch ? truncate(props.session.branch,
|
|
70
|
+
const truncBranch = () => (props.session.branch ? truncate(props.session.branch, 45) : "");
|
|
81
71
|
|
|
82
72
|
const hasDiff = () => {
|
|
83
73
|
const { linesAdded, linesRemoved, commitsDelta, filesChanged } = props.session;
|
|
@@ -128,7 +118,7 @@ export function SessionCard(props: SessionCardProps) {
|
|
|
128
118
|
</Show>
|
|
129
119
|
|
|
130
120
|
<box flexDirection="column" flexGrow={1} paddingRight={1}>
|
|
131
|
-
<box flexDirection="row">
|
|
121
|
+
<box flexDirection="row" height={1}>
|
|
132
122
|
<text truncate flexGrow={1}>
|
|
133
123
|
<span
|
|
134
124
|
style={{
|
|
@@ -139,25 +129,32 @@ export function SessionCard(props: SessionCardProps) {
|
|
|
139
129
|
{truncName()}
|
|
140
130
|
</span>
|
|
141
131
|
</text>
|
|
142
|
-
<Show when={
|
|
143
|
-
<
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
{statusIcon()}
|
|
147
|
-
{runningAgents() > 1 ? String(runningAgents()) : ""}
|
|
148
|
-
</span>
|
|
149
|
-
</text>
|
|
132
|
+
<Show when={hasDiff()}>
|
|
133
|
+
<box flexShrink={0} paddingLeft={1}>
|
|
134
|
+
<DiffStats session={props.session} palette={() => P()} />
|
|
135
|
+
</box>
|
|
150
136
|
</Show>
|
|
137
|
+
<box width={3} flexShrink={0}>
|
|
138
|
+
<Show when={statusIcon()}>
|
|
139
|
+
<text>
|
|
140
|
+
<span style={{ fg: statusColor() }}>
|
|
141
|
+
{" "}
|
|
142
|
+
{statusIcon()}
|
|
143
|
+
{runningAgents() > 1 ? String(runningAgents()) : ""}
|
|
144
|
+
</span>
|
|
145
|
+
</text>
|
|
146
|
+
</Show>
|
|
147
|
+
</box>
|
|
151
148
|
</box>
|
|
152
149
|
|
|
153
150
|
<Show when={props.session.branch}>
|
|
154
|
-
<
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
<box flexDirection="row" height={1}>
|
|
152
|
+
<text truncate flexShrink={1}>
|
|
153
|
+
<span style={{ fg: props.isFocused ? P().pink : P().overlay0 }}>
|
|
154
|
+
{truncBranch()}
|
|
155
|
+
</span>
|
|
156
|
+
</text>
|
|
157
|
+
</box>
|
|
161
158
|
</Show>
|
|
162
159
|
|
|
163
160
|
<Show when={metaSummary()}>
|
|
@@ -184,8 +181,6 @@ export function SessionCard(props: SessionCardProps) {
|
|
|
184
181
|
</For>
|
|
185
182
|
</box>
|
|
186
183
|
</box>
|
|
187
|
-
|
|
188
|
-
<box height={1} />
|
|
189
184
|
</box>
|
|
190
185
|
);
|
|
191
186
|
}
|
|
@@ -230,7 +225,16 @@ function AgentRow(props: AgentRowProps) {
|
|
|
230
225
|
return SC()[props.agent.status];
|
|
231
226
|
};
|
|
232
227
|
|
|
233
|
-
const
|
|
228
|
+
const cacheLabel = () => {
|
|
229
|
+
const details = props.agent.details;
|
|
230
|
+
if (!details) return null;
|
|
231
|
+
const expiresAt =
|
|
232
|
+
details.cacheExpiresAt ??
|
|
233
|
+
(details.lastActivityAt != null ? details.lastActivityAt + 60 * 60 * 1000 : null);
|
|
234
|
+
if (expiresAt == null) return null;
|
|
235
|
+
const minutesLeft = Math.ceil((expiresAt - props.now()) / 60_000);
|
|
236
|
+
return minutesLeft <= 0 ? "cache expired" : `cache ${minutesLeft}m`;
|
|
237
|
+
};
|
|
234
238
|
|
|
235
239
|
let flashTimer: ReturnType<typeof setTimeout> | null = null;
|
|
236
240
|
const triggerFlash = () => {
|
|
@@ -259,10 +263,20 @@ function AgentRow(props: AgentRowProps) {
|
|
|
259
263
|
props.onFocusPane();
|
|
260
264
|
}}
|
|
261
265
|
>
|
|
262
|
-
<box flexDirection="row">
|
|
263
|
-
<text
|
|
266
|
+
<box flexDirection="row" height={1}>
|
|
267
|
+
<text flexShrink={0}>
|
|
264
268
|
<span style={{ fg: color() }}>{icon()}</span>
|
|
265
|
-
|
|
269
|
+
</text>
|
|
270
|
+
<text flexGrow={1} flexShrink={1} truncate>
|
|
271
|
+
<Show when={props.agent.threadName}>
|
|
272
|
+
<span style={{ fg: isUnseen() ? color() : P().overlay0 }}>
|
|
273
|
+
{" "}
|
|
274
|
+
{truncate(props.agent.threadName!.replace(/\s+/g, " ").trim(), 40)}
|
|
275
|
+
</span>
|
|
276
|
+
</Show>
|
|
277
|
+
</text>
|
|
278
|
+
<Show when={props.agent.status === "running" && props.agent.details?.lastActivityAt}>
|
|
279
|
+
<text flexShrink={0}>
|
|
266
280
|
<span
|
|
267
281
|
style={{
|
|
268
282
|
fg: props.isKeyboardFocused ? P().subtext0 : P().overlay1,
|
|
@@ -272,11 +286,6 @@ function AgentRow(props: AgentRowProps) {
|
|
|
272
286
|
{" "}
|
|
273
287
|
{formatElapsed(props.now() - (props.agent.details?.lastActivityAt ?? props.now()))}
|
|
274
288
|
</span>
|
|
275
|
-
</Show>
|
|
276
|
-
</text>
|
|
277
|
-
<Show when={!isUnseen()}>
|
|
278
|
-
<text flexShrink={0}>
|
|
279
|
-
<span style={{ fg: color(), attributes: DIM }}>{statusText()}</span>
|
|
280
289
|
</text>
|
|
281
290
|
</Show>
|
|
282
291
|
<text
|
|
@@ -293,16 +302,6 @@ function AgentRow(props: AgentRowProps) {
|
|
|
293
302
|
</text>
|
|
294
303
|
</box>
|
|
295
304
|
|
|
296
|
-
<Show when={props.agent.threadName}>
|
|
297
|
-
<box height={2} flexShrink={0}>
|
|
298
|
-
<text>
|
|
299
|
-
<span style={{ fg: isUnseen() ? color() : P().overlay0 }}>
|
|
300
|
-
{truncate(props.agent.threadName!.replace(/\s+/g, " ").trim(), 60)}
|
|
301
|
-
</span>
|
|
302
|
-
</text>
|
|
303
|
-
</box>
|
|
304
|
-
</Show>
|
|
305
|
-
|
|
306
305
|
<Show when={props.agent.status === "running" && props.agent.details}>
|
|
307
306
|
{(d) => {
|
|
308
307
|
const details = d();
|
|
@@ -324,6 +323,12 @@ function AgentRow(props: AgentRowProps) {
|
|
|
324
323
|
);
|
|
325
324
|
}}
|
|
326
325
|
</Show>
|
|
326
|
+
|
|
327
|
+
<Show when={cacheLabel()}>
|
|
328
|
+
<text truncate>
|
|
329
|
+
<span style={{ fg: P().overlay0, attributes: DIM }}>{cacheLabel()}</span>
|
|
330
|
+
</text>
|
|
331
|
+
</Show>
|
|
327
332
|
</box>
|
|
328
333
|
);
|
|
329
334
|
}
|
|
@@ -555,43 +555,53 @@ function App() {
|
|
|
555
555
|
/>
|
|
556
556
|
|
|
557
557
|
{/* Session list */}
|
|
558
|
-
<scrollbox flexGrow={1} flexShrink={1} paddingTop={
|
|
558
|
+
<scrollbox flexGrow={1} flexShrink={1} paddingTop={0}>
|
|
559
|
+
<box height={1}>
|
|
560
|
+
<text style={{ fg: P().overlay0 }}>{DIVIDER}</text>
|
|
561
|
+
</box>
|
|
559
562
|
<For each={sessions}>
|
|
560
563
|
{(session, i) => (
|
|
561
|
-
<
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
session: session.name
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
564
|
+
<box flexDirection="column" flexShrink={0}>
|
|
565
|
+
<Show when={i() > 0}>
|
|
566
|
+
<box height={1}>
|
|
567
|
+
<text style={{ fg: P().surface2 }}>{DIVIDER}</text>
|
|
568
|
+
</box>
|
|
569
|
+
</Show>
|
|
570
|
+
<SessionCard
|
|
571
|
+
session={session}
|
|
572
|
+
isFocused={isFocused(session.name)}
|
|
573
|
+
isCurrent={session.name === currentSession()}
|
|
574
|
+
spinIdx={spinIdx}
|
|
575
|
+
now={now}
|
|
576
|
+
theme={theme}
|
|
577
|
+
statusColors={S}
|
|
578
|
+
focusedAgentIdx={
|
|
579
|
+
isFocused(session.name) && panelFocus() === "agents" ? focusedAgentIdx() : -1
|
|
580
|
+
}
|
|
581
|
+
onSelect={() => {
|
|
582
|
+
setFocusedSession(session.name);
|
|
583
|
+
send({ type: "focus-session", name: session.name });
|
|
584
|
+
switchToSession(session.name);
|
|
585
|
+
}}
|
|
586
|
+
onDismissAgent={(agent) => {
|
|
587
|
+
send({
|
|
588
|
+
type: "dismiss-agent",
|
|
589
|
+
session: session.name,
|
|
590
|
+
agent: agent.agent,
|
|
591
|
+
threadId: agent.threadId,
|
|
592
|
+
});
|
|
593
|
+
}}
|
|
594
|
+
onFocusAgentPane={(agent) => {
|
|
595
|
+
send({
|
|
596
|
+
type: "focus-agent-pane",
|
|
597
|
+
session: session.name,
|
|
598
|
+
agent: agent.agent,
|
|
599
|
+
threadId: agent.threadId,
|
|
600
|
+
threadName: agent.threadName,
|
|
601
|
+
});
|
|
602
|
+
}}
|
|
603
|
+
/>
|
|
604
|
+
</box>
|
|
595
605
|
)}
|
|
596
606
|
</For>
|
|
597
607
|
</scrollbox>
|
|
@@ -152,6 +152,109 @@ describe("AgentTracker", () => {
|
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
describe("pruneStale", () => {
|
|
156
|
+
const TWELVE_HOURS = 12 * 60 * 60 * 1000;
|
|
157
|
+
|
|
158
|
+
it("prunes waiting agents older than threshold", () => {
|
|
159
|
+
tracker.applyEvent(makeEvent({ status: "waiting", ts: Date.now() - TWELVE_HOURS - 1000 }));
|
|
160
|
+
tracker.pruneStale(TWELVE_HOURS);
|
|
161
|
+
expect(tracker.getState("main")).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("prunes running agents older than threshold", () => {
|
|
165
|
+
tracker.applyEvent(makeEvent({ status: "running", ts: Date.now() - TWELVE_HOURS - 1000 }));
|
|
166
|
+
tracker.pruneStale(TWELVE_HOURS);
|
|
167
|
+
expect(tracker.getState("main")).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("leaves fresh agents alone", () => {
|
|
171
|
+
tracker.applyEvent(makeEvent({ status: "waiting", ts: Date.now() - 1000 }));
|
|
172
|
+
tracker.pruneStale(TWELVE_HOURS);
|
|
173
|
+
expect(tracker.getState("main")).not.toBeNull();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("respects pinned instances", () => {
|
|
177
|
+
tracker.applyEvent(makeEvent({ status: "waiting", ts: Date.now() - TWELVE_HOURS - 1000 }));
|
|
178
|
+
tracker.setPinnedInstances("main", ["claude-code"]);
|
|
179
|
+
tracker.pruneStale(TWELVE_HOURS);
|
|
180
|
+
expect(tracker.getState("main")).not.toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("uses details.lastActivityAt when present", () => {
|
|
184
|
+
tracker.applyEvent(
|
|
185
|
+
makeEvent({
|
|
186
|
+
status: "waiting",
|
|
187
|
+
ts: Date.now() - TWELVE_HOURS - 1000,
|
|
188
|
+
details: { lastActivityAt: Date.now() - 1000 },
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
tracker.pruneStale(TWELVE_HOURS);
|
|
192
|
+
expect(tracker.getState("main")).not.toBeNull();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("falls back to event.ts when lastActivityAt missing", () => {
|
|
196
|
+
tracker.applyEvent(makeEvent({ status: "waiting", ts: Date.now() - TWELVE_HOURS - 1000 }));
|
|
197
|
+
tracker.pruneStale(TWELVE_HOURS);
|
|
198
|
+
expect(tracker.getState("main")).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("pruneSupersededByPane", () => {
|
|
203
|
+
it("drops older instance when same agent reappears with new threadId in same pane", () => {
|
|
204
|
+
tracker.applyEvent(makeEvent({ threadId: "t1", status: "waiting", paneId: "%5", ts: 1000 }));
|
|
205
|
+
tracker.applyEvent(makeEvent({ threadId: "t2", status: "running", paneId: "%5", ts: 2000 }));
|
|
206
|
+
tracker.setPinnedInstances("main", ["claude-code:t2"]);
|
|
207
|
+
tracker.pruneSupersededByPane();
|
|
208
|
+
const agents = tracker.getAgents("main");
|
|
209
|
+
expect(agents).toHaveLength(1);
|
|
210
|
+
expect(agents[0]!.threadId).toBe("t2");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("keeps both when instances are in different panes", () => {
|
|
214
|
+
tracker.applyEvent(makeEvent({ threadId: "t1", status: "waiting", paneId: "%5", ts: 1000 }));
|
|
215
|
+
tracker.applyEvent(makeEvent({ threadId: "t2", status: "running", paneId: "%7", ts: 2000 }));
|
|
216
|
+
tracker.pruneSupersededByPane();
|
|
217
|
+
expect(tracker.getAgents("main")).toHaveLength(2);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("keeps both when they're different agent types in the same pane", () => {
|
|
221
|
+
tracker.applyEvent(makeEvent({ agent: "claude-code", paneId: "%5", ts: 1000 }));
|
|
222
|
+
tracker.applyEvent(makeEvent({ agent: "amp", paneId: "%5", ts: 2000 }));
|
|
223
|
+
tracker.pruneSupersededByPane();
|
|
224
|
+
expect(tracker.getAgents("main")).toHaveLength(2);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("ignores instances without a paneId", () => {
|
|
228
|
+
tracker.applyEvent(makeEvent({ threadId: "t1", paneId: undefined, ts: 1000 }));
|
|
229
|
+
tracker.applyEvent(makeEvent({ threadId: "t2", paneId: undefined, ts: 2000 }));
|
|
230
|
+
tracker.pruneSupersededByPane();
|
|
231
|
+
expect(tracker.getAgents("main")).toHaveLength(2);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("uses details.lastActivityAt for recency comparison", () => {
|
|
235
|
+
tracker.applyEvent(
|
|
236
|
+
makeEvent({
|
|
237
|
+
threadId: "t1",
|
|
238
|
+
paneId: "%5",
|
|
239
|
+
ts: 5000,
|
|
240
|
+
details: { lastActivityAt: 9000 },
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
tracker.applyEvent(
|
|
244
|
+
makeEvent({
|
|
245
|
+
threadId: "t2",
|
|
246
|
+
paneId: "%5",
|
|
247
|
+
ts: 6000,
|
|
248
|
+
details: { lastActivityAt: 7000 },
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
tracker.pruneSupersededByPane();
|
|
252
|
+
const agents = tracker.getAgents("main");
|
|
253
|
+
expect(agents).toHaveLength(1);
|
|
254
|
+
expect(agents[0]!.threadId).toBe("t1");
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
155
258
|
describe("multi-thread support", () => {
|
|
156
259
|
it("tracks multiple threads for the same agent", () => {
|
|
157
260
|
tracker.applyEvent(makeEvent({ threadId: "t1", status: "running" }));
|
|
@@ -148,6 +148,56 @@ export class AgentTracker {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* When multiple instances of the same agent share a paneId, keep only the most recent.
|
|
153
|
+
* The currently-live instance is pinned by the pane scanner, so this only removes superseded
|
|
154
|
+
* predecessors (e.g. a Claude Code session that was /exited and replaced in the same pane).
|
|
155
|
+
*/
|
|
156
|
+
pruneSupersededByPane(): void {
|
|
157
|
+
for (const [session, sessionInstances] of this.instances) {
|
|
158
|
+
const groups = new Map<string, { key: string; ts: number }[]>();
|
|
159
|
+
for (const [key, event] of sessionInstances) {
|
|
160
|
+
if (!event.paneId) continue;
|
|
161
|
+
const groupKey = `${event.paneId}\0${event.agent}`;
|
|
162
|
+
const ts = event.details?.lastActivityAt ?? event.ts;
|
|
163
|
+
const list = groups.get(groupKey) ?? [];
|
|
164
|
+
list.push({ key, ts });
|
|
165
|
+
groups.set(groupKey, list);
|
|
166
|
+
}
|
|
167
|
+
for (const list of groups.values()) {
|
|
168
|
+
if (list.length < 2) continue;
|
|
169
|
+
list.sort((a, b) => b.ts - a.ts);
|
|
170
|
+
for (let i = 1; i < list.length; i++) {
|
|
171
|
+
const k = list[i]!.key;
|
|
172
|
+
if (this.isPinned(session, k)) continue;
|
|
173
|
+
sessionInstances.delete(k);
|
|
174
|
+
this.unseenInstances.delete(this.unseenKey(session, k));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (sessionInstances.size === 0) {
|
|
178
|
+
this.instances.delete(session);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Auto-prune any instance whose last activity is older than timeoutMs, regardless of status. Skips pinned. */
|
|
184
|
+
pruneStale(timeoutMs: number): void {
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
for (const [session, sessionInstances] of this.instances) {
|
|
187
|
+
for (const [key, event] of sessionInstances) {
|
|
188
|
+
if (this.isPinned(session, key)) continue;
|
|
189
|
+
const lastSeen = event.details?.lastActivityAt ?? event.ts;
|
|
190
|
+
if (now - lastSeen > timeoutMs) {
|
|
191
|
+
sessionInstances.delete(key);
|
|
192
|
+
this.unseenInstances.delete(this.unseenKey(session, key));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (sessionInstances.size === 0) {
|
|
196
|
+
this.instances.delete(session);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
151
201
|
/** Auto-prune terminal instances older than timeout, but only if instance is not unseen or pinned */
|
|
152
202
|
pruneTerminal(): void {
|
|
153
203
|
const now = Date.now();
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
PID_FILE,
|
|
24
24
|
SERVER_IDLE_TIMEOUT_MS,
|
|
25
25
|
STUCK_RUNNING_TIMEOUT_MS,
|
|
26
|
+
STALE_AGENT_TIMEOUT_MS,
|
|
26
27
|
} from "../shared";
|
|
27
28
|
import type { ServerState, SessionData, ClientCommand, FocusUpdate, MetadataTone } from "../shared";
|
|
28
29
|
|
|
@@ -391,6 +392,8 @@ export function startServer(
|
|
|
391
392
|
invalidateCurrentSessionCache();
|
|
392
393
|
tracker.pruneStuck(STUCK_RUNNING_TIMEOUT_MS);
|
|
393
394
|
tracker.pruneTerminal();
|
|
395
|
+
tracker.pruneStale(STALE_AGENT_TIMEOUT_MS);
|
|
396
|
+
tracker.pruneSupersededByPane();
|
|
394
397
|
lastState = computeState();
|
|
395
398
|
syncGitWatchers(lastState.sessions, broadcastState);
|
|
396
399
|
const msg = JSON.stringify(lastState);
|
|
@@ -11,6 +11,7 @@ export const SERVER_HOST: string = process.env.TT_AGENTBOARD_HOST || DEFAULT_SER
|
|
|
11
11
|
export const PID_FILE = "/tmp/agentboard.pid";
|
|
12
12
|
export const SERVER_IDLE_TIMEOUT_MS = 30_000;
|
|
13
13
|
export const STUCK_RUNNING_TIMEOUT_MS = 3 * 60 * 1000;
|
|
14
|
+
export const STALE_AGENT_TIMEOUT_MS = 12 * 60 * 60 * 1000;
|
|
14
15
|
export const JOURNAL_IDLE_TIMEOUT_MS = 120_000;
|
|
15
16
|
|
|
16
17
|
export interface SessionData {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tt",
|
|
3
3
|
"description": "Core dev workflow commands and skills: interview-me, write-prd, prd-to-issues, tdd, improve-architecture, refine-text, task, parallel-slots, towles-tool.",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.131",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Chris Towles"
|
|
7
7
|
}
|
|
@@ -18,7 +18,13 @@ function textBlock(text: string): ContentBlock {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function toolUseBlock(name: string, input: Record<string, unknown>): ContentBlock {
|
|
21
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
type: "tool_use" as const,
|
|
23
|
+
id: "tool-stub",
|
|
24
|
+
name,
|
|
25
|
+
input,
|
|
26
|
+
caller: { type: "direct" },
|
|
27
|
+
} as unknown as ContentBlock;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
function makeUsage(overrides: Partial<Usage> = {}): Usage {
|
|
@@ -32,7 +38,7 @@ function makeUsage(overrides: Partial<Usage> = {}): Usage {
|
|
|
32
38
|
server_tool_use: null,
|
|
33
39
|
service_tier: null,
|
|
34
40
|
...overrides,
|
|
35
|
-
};
|
|
41
|
+
} as unknown as Usage;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
function makeEntry(overrides: Partial<JournalEntry> = {}): JournalEntry {
|