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,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Tracks subagent lifecycle: one
|
|
3
|
-
# Layout: ~/.claude/.cck/agent-activity/{sessionId}/{agentId}.
|
|
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.
|
|
74
|
+
FILE="$DIR/$AGENT_ID.jsonl"
|
|
74
75
|
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
75
|
-
|
|
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.
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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 "$
|
|
134
|
-
'{agentId: $id, type: $type,
|
|
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
|
-
|
|
110
|
+
>> "$FILE"
|
|
137
111
|
|
|
138
112
|
elif [ "$EVENT" = "TeammateIdle" ]; then
|
|
139
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 + '.
|
|
107
|
-
|
|
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('.
|
|
170
|
+
for (const file of readdirSync(agentDir).filter(f => f.endsWith('.jsonl') && !f.startsWith('_'))) {
|
|
160
171
|
try {
|
|
161
|
-
const agent =
|
|
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('.
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 + '.
|
|
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 =
|
|
1227
|
+
const agent = readAgentJsonl(agentFile);
|
|
1191
1228
|
agent.status = 'stopped';
|
|
1192
1229
|
agent.stoppedAt = new Date().toISOString();
|
|
1193
|
-
|
|
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 + '.
|
|
1335
|
-
const agent =
|
|
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
|
-
|
|
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('.
|
|
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);
|