claude-code-kanban 3.10.0 → 4.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "3.10.0",
3
+ "version": "4.0.0",
4
4
  "description": "A web-based Kanban board for viewing Claude Code tasks with agent teams support",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "claude-code-kanban",
3
- "version": "1.1.5",
3
+ "version": "2.0.0",
4
4
  "description": "Agent activity tracking for claude-code-kanban dashboard"
5
5
  }
@@ -1,6 +1,7 @@
1
1
  #!/bin/bash
2
- # Tracks subagent lifecycle: one JSON file per agent, grouped by session
3
- # Layout: ~/.claude/.cck/agent-activity/{sessionId}/{agentId}.json
2
+ # Tracks subagent lifecycle: one append-only JSONL file per agent, grouped by session
3
+ # Layout: ~/.claude/.cck/agent-activity/{sessionId}/{agentId}.jsonl
4
+ # Each line is a lifecycle event (start | idle | stop). Server folds last-line-wins.
4
5
 
5
6
  INPUT=$(cat)
6
7
 
@@ -70,23 +71,16 @@ if [ "$EVENT" = "TeammateIdle" ] && [ -z "$AGENT_ID" ] && [ -n "$TEAMMATE_NAME"
70
71
  [ ! -f "$MAP_FILE" ] && exit 0
71
72
  AGENT_ID=$(cat "$MAP_FILE")
72
73
  [ -z "$AGENT_ID" ] && exit 0
73
- FILE="$DIR/$AGENT_ID.json"
74
+ FILE="$DIR/$AGENT_ID.jsonl"
74
75
  TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
75
- STARTED_AT="$TS"
76
- if [ -f "$FILE" ]; then
77
- PREV_START=$(jq -r '.startedAt // ""' "$FILE" 2>/dev/null)
78
- [ -n "$PREV_START" ] && STARTED_AT="$PREV_START"
79
- fi
80
- cat > "$FILE" <<EOF
81
- {"agentId":"$AGENT_ID","type":"$TEAMMATE_NAME","status":"idle","startedAt":"$STARTED_AT","updatedAt":"$TS"}
82
- EOF
76
+ echo "{\"agentId\":\"$AGENT_ID\",\"type\":\"$TEAMMATE_NAME\",\"event\":\"idle\",\"status\":\"idle\",\"updatedAt\":\"$TS\"}" >> "$FILE"
83
77
  exit 0
84
78
  fi
85
79
 
86
80
  [ -z "$AGENT_ID" ] && exit 0
87
81
 
88
82
  DIR="$CCK_ACTIVITY/$SESSION_ID"
89
- FILE="$DIR/$AGENT_ID.json"
83
+ FILE="$DIR/$AGENT_ID.jsonl"
90
84
 
91
85
  # On Start: skip if no type (internal agents like AskUserQuestion)
92
86
  # On Stop/Idle: only skip if no existing file (never tracked)
@@ -102,48 +96,19 @@ mkdir -p "$DIR"
102
96
  TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
103
97
 
104
98
  if [ "$EVENT" = "SubagentStart" ]; then
105
- cat > "$FILE" <<EOF
106
- {"agentId":"$AGENT_ID","type":"$AGENT_TYPE_RAW","status":"active","startedAt":"$TS","updatedAt":"$TS"}
107
- EOF
108
- # Write name→id mapping for TeammateIdle resolution
109
- # Delete previous file only if not active (idle/stopped = teammate re-spawn, active = parallel subagent)
99
+ echo "{\"agentId\":\"$AGENT_ID\",\"type\":\"$AGENT_TYPE_RAW\",\"event\":\"start\",\"status\":\"active\",\"startedAt\":\"$TS\",\"updatedAt\":\"$TS\"}" >> "$FILE"
100
+ # Mapping always points at latest agent of this type (used by TeammateIdle resolution).
110
101
  if [ -n "$AGENT_TYPE_RAW" ]; then
111
- MAP_FILE="$DIR/_name-${AGENT_TYPE_RAW}.id"
112
- if [ -f "$MAP_FILE" ]; then
113
- OLD_ID=$(cat "$MAP_FILE")
114
- if [ -n "$OLD_ID" ] && [ "$OLD_ID" != "$AGENT_ID" ]; then
115
- OLD_FILE="$DIR/$OLD_ID.json"
116
- OLD_STATUS=""
117
- [ -f "$OLD_FILE" ] && OLD_STATUS=$(jq -r '.status // ""' "$OLD_FILE" 2>/dev/null)
118
- [ "$OLD_STATUS" != "active" ] && rm -f "$OLD_FILE"
119
- fi
120
- fi
121
- echo -n "$AGENT_ID" > "$MAP_FILE"
102
+ echo -n "$AGENT_ID" > "$DIR/_name-${AGENT_TYPE_RAW}.id"
122
103
  fi
123
104
 
124
105
  elif [ "$EVENT" = "SubagentStop" ]; then
125
- AGENT_TYPE="$AGENT_TYPE_RAW"
126
- STARTED_AT="$TS"
127
- if [ -f "$FILE" ]; then
128
- eval "$(jq -r '@sh "PREV_TYPE=\(.type // "unknown")", @sh "PREV_START=\(.startedAt // "")"' "$FILE")"
129
- [ -z "$AGENT_TYPE" ] && AGENT_TYPE="$PREV_TYPE"
130
- [ -n "$PREV_START" ] && STARTED_AT="$PREV_START"
131
- fi
132
106
  echo "$INPUT" | jq -c \
133
- --arg id "$AGENT_ID" --arg type "$AGENT_TYPE" --arg started "$STARTED_AT" --arg ts "$TS" \
134
- '{agentId: $id, type: $type, status: "stopped", startedAt: $started,
107
+ --arg id "$AGENT_ID" --arg type "$AGENT_TYPE_RAW" --arg ts "$TS" \
108
+ '{agentId: $id, type: $type, event: "stop", status: "stopped",
135
109
  lastMessage: (.last_assistant_message // ""), stoppedAt: $ts, updatedAt: $ts}' \
136
- > "$FILE"
110
+ >> "$FILE"
137
111
 
138
112
  elif [ "$EVENT" = "TeammateIdle" ]; then
139
- AGENT_TYPE="$AGENT_TYPE_RAW"
140
- STARTED_AT="$TS"
141
- if [ -f "$FILE" ]; then
142
- eval "$(jq -r '@sh "PREV_TYPE=\(.type // "unknown")", @sh "PREV_START=\(.startedAt // "")"' "$FILE")"
143
- [ -z "$AGENT_TYPE" ] && AGENT_TYPE="$PREV_TYPE"
144
- [ -n "$PREV_START" ] && STARTED_AT="$PREV_START"
145
- fi
146
- cat > "$FILE" <<EOF
147
- {"agentId":"$AGENT_ID","type":"$AGENT_TYPE","status":"idle","startedAt":"$STARTED_AT","updatedAt":"$TS"}
148
- EOF
113
+ echo "{\"agentId\":\"$AGENT_ID\",\"type\":\"$AGENT_TYPE_RAW\",\"event\":\"idle\",\"status\":\"idle\",\"updatedAt\":\"$TS\"}" >> "$FILE"
149
114
  fi
package/public/app.js CHANGED
@@ -2458,6 +2458,7 @@ function renderSessions() {
2458
2458
  const showCtx = !!session.contextStatus;
2459
2459
  const linkedDocsCount = getSessionPreviewPaths(session.id).length;
2460
2460
  const bookmarksCount = loadPins(session.id).length;
2461
+ const hasScratchpad = !!(localStorage.getItem(_sessionScratchpadKey(session.id)) || '').trim();
2461
2462
  const tempClass = session.hasRecentLog || session.inProgress || session.hasWaitingForUser ? 'warm' : 'stale';
2462
2463
  return `
2463
2464
  <button onclick="fetchTasks('${session.id}')" data-session-id="${session.id}" class="session-item ${isActive ? 'active' : ''} ${session.hasWaitingForUser ? 'permission-pending' : ''} ${tempClass} ${showCtx ? 'has-context' : ''}" title="${tooltip}">
@@ -2474,6 +2475,7 @@ function renderSessions() {
2474
2475
  ${session.hasPlan ? `<span class="plan-indicator" onclick="event.stopPropagation(); openPlanForSession('${session.id}')" title="View plan"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></span>` : ''}
2475
2476
  ${linkedDocsCount > 0 ? `<span class="linked-docs-badge" onclick="event.stopPropagation(); showSessionInfoModal('${session.id}')" title="${linkedDocsCount} linked document${linkedDocsCount > 1 ? 's' : ''}">${linkSvg(10)}${linkedDocsCount}</span>` : ''}
2476
2477
  ${bookmarksCount > 0 ? `<span class="bookmarks-badge" onclick="event.stopPropagation(); openSessionWithBookmarks('${session.id}')" title="${bookmarksCount} bookmarked message${bookmarksCount > 1 ? 's' : ''}"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>${bookmarksCount}</span>` : ''}
2478
+ ${hasScratchpad ? `<span class="scratchpad-badge" onclick="event.stopPropagation(); openSessionScratchpad('${session.id}')" title="Open scratchpad"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></span>` : ''}
2477
2479
  ${session.hasRunningAgents ? '<span class="agent-badge" title="Active agents">🤖</span>' : ''}
2478
2480
  ${session.planSourceSessionId ? `<span class="plan-indicator" title="Implements plan — click to reveal plan session" onclick="event.stopPropagation(); revealPlanSession('${escapeHtml(session.planSourceSessionId)}')">📋</span>` : ''}
2479
2481
  ${session.hasWaitingForUser ? '<span class="agent-badge" title="Waiting for user">❓</span>' : ''}
@@ -3613,9 +3615,17 @@ const _scratchpadCharcount = document.getElementById('scratchpad-charcount');
3613
3615
 
3614
3616
  let _scratchpadKeyOverride = null;
3615
3617
 
3618
+ function _sessionScratchpadKey(sessionId) {
3619
+ return `scratchpad-${sessionId}`;
3620
+ }
3621
+
3622
+ function _isSessionScratchpadKey(key) {
3623
+ return key.startsWith('scratchpad-') && !key.startsWith('scratchpad-project:');
3624
+ }
3625
+
3616
3626
  function _scratchpadKey() {
3617
3627
  if (_scratchpadKeyOverride) return _scratchpadKeyOverride;
3618
- if (currentSessionId) return `scratchpad-${currentSessionId}`;
3628
+ if (currentSessionId) return _sessionScratchpadKey(currentSessionId);
3619
3629
  if (currentProjectPath) return `scratchpad-project:${currentProjectPath}`;
3620
3630
  return null;
3621
3631
  }
@@ -3628,6 +3638,11 @@ function toggleScratchpad() {
3628
3638
  }
3629
3639
  }
3630
3640
 
3641
+ // biome-ignore lint/correctness/noUnusedVariables: used in HTML onclick
3642
+ function openSessionScratchpad(sessionId) {
3643
+ showScratchpad(_sessionScratchpadKey(sessionId));
3644
+ }
3645
+
3631
3646
  function showScratchpad(keyOverride) {
3632
3647
  _scratchpadKeyOverride = keyOverride || null;
3633
3648
  const key = _scratchpadKey();
@@ -3652,11 +3667,16 @@ function saveScratchpad() {
3652
3667
  const key = _scratchpadKey();
3653
3668
  if (!key) return;
3654
3669
  const val = _scratchpadTextarea.value;
3655
- if (val.trim()) {
3670
+ const had = !!(localStorage.getItem(key) || '').trim();
3671
+ const has = !!val.trim();
3672
+ if (has) {
3656
3673
  localStorage.setItem(key, val);
3657
3674
  } else {
3658
3675
  localStorage.removeItem(key);
3659
3676
  }
3677
+ if (had !== has && _isSessionScratchpadKey(key)) {
3678
+ renderSessions();
3679
+ }
3660
3680
  }
3661
3681
 
3662
3682
  _scratchpadTextarea.addEventListener('input', () => {
@@ -4075,7 +4095,7 @@ function _findOrphanedKeys() {
4075
4095
  const key = localStorage.key(i);
4076
4096
  if (key.startsWith('pinned-messages-')) {
4077
4097
  if (!known.has(key.slice('pinned-messages-'.length))) orphaned.push(key);
4078
- } else if (key.startsWith('scratchpad-') && !key.startsWith('scratchpad-project:')) {
4098
+ } else if (_isSessionScratchpadKey(key)) {
4079
4099
  if (!known.has(key.slice('scratchpad-'.length))) orphaned.push(key);
4080
4100
  } else if (key.startsWith(PREVIEW_STORAGE_PREFIX)) {
4081
4101
  if (!known.has(key.slice(PREVIEW_STORAGE_PREFIX.length))) orphaned.push(key);
package/public/style.css CHANGED
@@ -2536,7 +2536,8 @@ body::before {
2536
2536
  }
2537
2537
 
2538
2538
  .linked-docs-badge,
2539
- .bookmarks-badge {
2539
+ .bookmarks-badge,
2540
+ .scratchpad-badge {
2540
2541
  display: inline-flex;
2541
2542
  align-items: center;
2542
2543
  gap: 2px;
@@ -2552,7 +2553,8 @@ body::before {
2552
2553
  }
2553
2554
 
2554
2555
  .linked-docs-badge:hover,
2555
- .bookmarks-badge:hover {
2556
+ .bookmarks-badge:hover,
2557
+ .scratchpad-badge:hover {
2556
2558
  border-color: var(--accent);
2557
2559
  color: var(--text-primary);
2558
2560
  }
package/server.js CHANGED
@@ -102,12 +102,19 @@ const SESSION_STALE_MS = 300000;
102
102
 
103
103
  const WAITING_RESOLVE_GRACE_MS = 15000;
104
104
 
105
+ function readAgentJsonl(filePath) {
106
+ const raw = readFileSync(filePath, 'utf8');
107
+ const merged = {};
108
+ for (const line of raw.split(/\r?\n/)) {
109
+ if (!line.trim()) continue;
110
+ try { Object.assign(merged, JSON.parse(line)); } catch (_) { /* skip malformed */ }
111
+ }
112
+ return merged;
113
+ }
114
+
105
115
  function persistAgent(dir, agent) {
106
- const file = path.join(dir, agent.agentId + '.json');
107
- const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
108
- fs.writeFile(tmp, JSON.stringify(agent), 'utf8')
109
- .then(() => fs.rename(tmp, file))
110
- .catch(() => { fs.unlink(tmp).catch(() => {}); });
116
+ const file = path.join(dir, agent.agentId + '.jsonl');
117
+ fs.appendFile(file, JSON.stringify({ ...agent, event: 'server-update' }) + '\n', 'utf8').catch(() => {});
111
118
  }
112
119
 
113
120
  function checkWaitingForUser(agentDir, logMtime) {
@@ -125,6 +132,10 @@ function checkWaitingForUser(agentDir, logMtime) {
125
132
  return null;
126
133
  }
127
134
 
135
+ function agentDisplayName(agent) {
136
+ return agent.type || agent.name;
137
+ }
138
+
128
139
  function isGhostAgent(agent) {
129
140
  if (agent.startedAt !== agent.updatedAt || agent.lastMessage) return false;
130
141
  return (Date.now() - new Date(agent.startedAt).getTime()) >= AGENT_STALE_MS;
@@ -156,9 +167,9 @@ function checkAgentStatus(agentDir, stale, logMtime, isTeam) {
156
167
  if (result.waitingForUser) result.hasActive = true;
157
168
  if (stale && !isTeam) return result;
158
169
  try {
159
- for (const file of readdirSync(agentDir).filter(f => f.endsWith('.json') && !f.startsWith('_'))) {
170
+ for (const file of readdirSync(agentDir).filter(f => f.endsWith('.jsonl') && !f.startsWith('_'))) {
160
171
  try {
161
- const agent = JSON.parse(readFileSync(path.join(agentDir, file), 'utf8'));
172
+ const agent = readAgentJsonl(path.join(agentDir, file));
162
173
  if (isTeam && (agent.status === 'active' || agent.status === 'idle')) {
163
174
  result.hasActive = true;
164
175
  if (agent.status === 'active') result.hasRunning = true;
@@ -1050,17 +1061,17 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
1050
1061
  const isTeam = !!teamConfig;
1051
1062
  const teamMemberNames = isTeam ? new Set(teamConfig.members.map(m => m.name)) : null;
1052
1063
 
1053
- const files = readdirSync(agentDir).filter(f => f.endsWith('.json') && !f.startsWith('_'));
1064
+ const files = readdirSync(agentDir).filter(f => f.endsWith('.jsonl') && !f.startsWith('_'));
1054
1065
  const agents = [];
1055
1066
  for (const file of files) {
1056
1067
  try {
1057
- const agent = JSON.parse(readFileSync(path.join(agentDir, file), 'utf8'));
1068
+ const agent = readAgentJsonl(path.join(agentDir, file));
1058
1069
  if (isGhostAgent(agent)) continue;
1059
1070
  const agentTs = agent.updatedAt || agent.startedAt;
1060
1071
  const agentStale = !sessionStale && agentTs && (Date.now() - new Date(agentTs).getTime()) > AGENT_STALE_MS;
1061
1072
  if (!isAgentFresh(agent) || sessionStale || agentStale) {
1062
1073
  if (agent.status === 'active' || agent.status === 'idle') {
1063
- const agentName = agent.type || agent.name;
1074
+ const agentName = agentDisplayName(agent);
1064
1075
  const isTeamMember = isTeam && agentName && teamMemberNames.has(agentName);
1065
1076
  if (!isTeamMember) {
1066
1077
  agent.status = 'stopped';
@@ -1077,7 +1088,7 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
1077
1088
  const terminated = getTerminatedTeammates(meta.jsonlPath);
1078
1089
  if (terminated.size) {
1079
1090
  for (const agent of liveAgents) {
1080
- const agentName = agent.type || agent.name;
1091
+ const agentName = agentDisplayName(agent);
1081
1092
  if (agentName && terminated.has(agentName)) {
1082
1093
  const terminatedAt = terminated.get(agentName);
1083
1094
  if (terminatedAt && agent.startedAt && terminatedAt < agent.startedAt) continue;
@@ -1168,14 +1179,40 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
1168
1179
  }
1169
1180
  if (Object.keys(teamColors).length) {
1170
1181
  for (const agent of agents) {
1171
- const name = agent.type || agent.name;
1182
+ const name = agentDisplayName(agent);
1172
1183
  if (name && teamColors[name]) agent.color = teamColors[name];
1173
1184
  }
1174
1185
  }
1175
1186
  }
1176
1187
 
1188
+ // Collapse teammate re-spawns: when a teammate goes idle and is later re-engaged,
1189
+ // a fresh agentId is spawned. Hide older idle/stopped entries when a newer same-name
1190
+ // teammate exists; never hide an `active` agent (parallel teammate work would vanish).
1191
+ // Subagents (Explore, general-purpose, etc.) are not in teamMemberNames and bypass
1192
+ // dedup entirely, so parallel siblings of the same subagent type remain visible.
1193
+ let visibleAgents = agents;
1194
+ if (teamMemberNames && teamMemberNames.size) {
1195
+ const groups = new Map();
1196
+ for (const a of agents) {
1197
+ const t = agentDisplayName(a);
1198
+ if (!t || !teamMemberNames.has(t)) continue;
1199
+ const list = groups.get(t) || [];
1200
+ list.push(a);
1201
+ groups.set(t, list);
1202
+ }
1203
+ const hidden = new Set();
1204
+ for (const list of groups.values()) {
1205
+ if (list.length < 2) continue;
1206
+ list.sort((a, b) => new Date(b.startedAt || 0) - new Date(a.startedAt || 0));
1207
+ for (const older of list.slice(1)) {
1208
+ if (older.status === 'idle' || older.status === 'stopped') hidden.add(older.agentId);
1209
+ }
1210
+ }
1211
+ if (hidden.size) visibleAgents = agents.filter(a => !hidden.has(a.agentId));
1212
+ }
1213
+
1177
1214
  const waitingForUser = checkWaitingForUser(agentDir, logMtime);
1178
- res.json({ agents, waitingForUser, teamColors });
1215
+ res.json({ agents: visibleAgents, waitingForUser, teamColors });
1179
1216
  } catch (e) {
1180
1217
  res.json({ agents: [], waitingForUser: null });
1181
1218
  }
@@ -1184,13 +1221,14 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
1184
1221
  app.post('/api/sessions/:sessionId/agents/:agentId/stop', (req, res) => {
1185
1222
  const sessionId = resolveSessionId(req.params.sessionId);
1186
1223
  const agentId = sanitizeAgentId(req.params.agentId);
1187
- const agentFile = path.join(AGENT_ACTIVITY_DIR, sessionId, agentId + '.json');
1224
+ const agentFile = path.join(AGENT_ACTIVITY_DIR, sessionId, agentId + '.jsonl');
1188
1225
  if (!existsSync(agentFile)) return res.status(404).json({ error: 'Agent not found' });
1189
1226
  try {
1190
- const agent = JSON.parse(readFileSync(agentFile, 'utf8'));
1227
+ const agent = readAgentJsonl(agentFile);
1191
1228
  agent.status = 'stopped';
1192
1229
  agent.stoppedAt = new Date().toISOString();
1193
- writeFileSync(agentFile, JSON.stringify(agent), 'utf8'); // sync response depends on write
1230
+ const stopEvt = { agentId, type: agent.type, event: 'user-stop', status: 'stopped', stoppedAt: agent.stoppedAt, updatedAt: agent.stoppedAt };
1231
+ writeFileSync(agentFile, readFileSync(agentFile, 'utf8') + JSON.stringify(stopEvt) + '\n', 'utf8'); // sync — response depends on write
1194
1232
  // Also remove waiting state if present
1195
1233
  const waitingFile = path.join(AGENT_ACTIVITY_DIR, sessionId, '_waiting.json');
1196
1234
  if (existsSync(waitingFile)) unlinkSync(waitingFile);
@@ -1331,8 +1369,8 @@ app.get('/api/sessions/:sessionId/messages', (req, res) => {
1331
1369
  if (entry.description) msg.agentDescription = entry.description;
1332
1370
  if (entry.prompt && !msg.agentPrompt) msg.agentPrompt = entry.prompt;
1333
1371
  try {
1334
- const agentFile = path.join(agentDir, entry.agentId + '.json');
1335
- const agent = JSON.parse(readFileSync(agentFile, 'utf8'));
1372
+ const agentFile = path.join(agentDir, entry.agentId + '.jsonl');
1373
+ const agent = readAgentJsonl(agentFile);
1336
1374
  if (agent.lastMessage) msg.agentLastMessage = agent.lastMessage;
1337
1375
  if (agent.prompt && !msg.agentPrompt) msg.agentPrompt = agent.prompt;
1338
1376
  const prompt = msg.agentPrompt || entry.prompt;
@@ -1819,14 +1857,16 @@ const agentActivityWatcher = chokidar.watch(AGENT_ACTIVITY_DIR, {
1819
1857
  const AGENT_FILE_CAP = 20;
1820
1858
 
1821
1859
  agentActivityWatcher.on('all', (event, filePath) => {
1822
- if ((event === 'add' || event === 'change' || event === 'unlink') && filePath.endsWith('.json')) {
1860
+ const base = path.basename(filePath);
1861
+ const isAgentEvent = filePath.endsWith('.jsonl') || base === '_waiting.json';
1862
+ if ((event === 'add' || event === 'change' || event === 'unlink') && isAgentEvent) {
1823
1863
  const relativePath = path.relative(AGENT_ACTIVITY_DIR, filePath);
1824
1864
  const sessionId = relativePath.split(path.sep)[0];
1825
1865
  // Cleanup: if session dir exceeds cap, delete oldest files by mtime
1826
- if (event === 'add') {
1866
+ if (event === 'add' && filePath.endsWith('.jsonl')) {
1827
1867
  try {
1828
1868
  const sessionDir = path.join(AGENT_ACTIVITY_DIR, sessionId);
1829
- const files = readdirSync(sessionDir).filter(f => f.endsWith('.json') && !f.startsWith('_'));
1869
+ const files = readdirSync(sessionDir).filter(f => f.endsWith('.jsonl') && !f.startsWith('_'));
1830
1870
  if (files.length > AGENT_FILE_CAP) {
1831
1871
  const withStats = files.map(f => {
1832
1872
  const fp = path.join(sessionDir, f);