neohive 6.2.2 → 6.3.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/CHANGELOG.md +26 -0
- package/package.json +1 -1
- package/server.js +311 -402
- package/tools/governance.js +5 -2
- package/tools/knowledge.js +4 -4
- package/tools/messaging.js +1 -1
- package/tools/safety.js +1 -1
- package/tools/tasks.js +7 -2
- package/tools/workflows.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [6.3.0] - 2026-04-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Unified `next_action` response chain** — every MCP tool response now includes a single `next_action` field that tells the AI agent exactly what to do next, replacing 10+ scattered hint fields (`_listen`, `_nudge`, `hint`, `action_required`, `unread_action`, `you_have_messages`, `urgent`, `mode_hint`, `_protocol`, etc.)
|
|
8
|
+
- **Tool-specific directives** — each tool sets a context-aware `next_action` (e.g. `update_task(done)` → "Send a summary via send_message(), then call listen()"; `lock_file` → "Edit the file, then call unlock_file() when done")
|
|
9
|
+
- **Coordinator-aware middleware** — post-processing middleware detects responsive coordinators and replaces any `listen()` directive with `consume_messages()` or removes it entirely, preventing coordinators from blocking in listen mode
|
|
10
|
+
- **Persistent listen loop** — `listen()` and `listen_group()` no longer return `retry: true` on timeout; they loop internally with fresh watchers and heartbeats, so agents cannot break out of listen mode
|
|
11
|
+
- **Managed mode next_action** — `buildListenGroupResponse` respects `should_respond` and floor state; agents without the floor get "do NOT respond" instead of "reply via send_message()"
|
|
12
|
+
- **Autonomous get_work() directives** — all 10 return types from `get_work()` carry specific `next_action` values guiding agents through the workflow/verify/advance cycle
|
|
13
|
+
- **Documentation** — `docs/reference/next-action-chain.md` with flow diagrams for standard agents, agent-to-agent communication, responsive/autonomous coordinators, managed mode, and persistent listen
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Post-processing middleware rewrite** — removed the scattered nudge/unread/listen injection block (~80 lines) and replaced with a unified `next_action` block using priority logic: tool-specific > coordinator override > call-count warning > urgent messages > pending messages > default
|
|
18
|
+
- **`buildMessageResponse`** — `_protocol` field inside message objects replaced with top-level `next_action`; `coordinator_mode` field removed from listen responses
|
|
19
|
+
- **`buildListenGroupResponse`** — simplified `next_action` with mode-aware branching (autonomous/managed/standard)
|
|
20
|
+
- **`send_message` / `broadcast`** — removed `you_have_messages`, `urgent`, `mode_hint` fields; added `next_action: "Call listen() to receive replies."`
|
|
21
|
+
- **`verify_and_advance`** — replaced verbose `message` strings with concise `next_action` directives
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **Responsive coordinator contradiction** — `send_message`, `broadcast`, and `create_task` no longer unconditionally tell responsive coordinators to call `listen()`; middleware overrides any `listen()` directive for responsive coordinators
|
|
26
|
+
- **Managed mode contradiction** — `buildListenGroupResponse` no longer sets "Reply via send_message()" when `should_respond: false` and `instructions: "DO NOT RESPOND"` are also present
|
|
27
|
+
- **Autonomous listen_group retry** — removed contradictory `retry: true` from autonomous mode timeout path where `next_action` directs to `get_work()` instead
|
|
28
|
+
|
|
3
29
|
## [6.1.0] - 2026-04-04
|
|
4
30
|
|
|
5
31
|
### Added
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -584,14 +584,14 @@ function buildMessageResponse(msg, consumedIds) {
|
|
|
584
584
|
}
|
|
585
585
|
} catch (e) { log.debug('task reminder in listen failed:', e.message); }
|
|
586
586
|
|
|
587
|
-
// Append report-back protocol reminder to all non-system messages
|
|
588
587
|
const isSystemMsg = msg.from === '__system__' || msg.system === true;
|
|
589
|
-
const
|
|
590
|
-
?
|
|
591
|
-
:
|
|
588
|
+
const nextAction = isSystemMsg
|
|
589
|
+
? 'Process this message, then call listen().'
|
|
590
|
+
: `Do what this message asks. When finished, send_message(to="${msg.from}") with what you did and files changed, then call listen().`;
|
|
592
591
|
|
|
593
592
|
return {
|
|
594
593
|
success: true,
|
|
594
|
+
next_action: nextAction,
|
|
595
595
|
message: {
|
|
596
596
|
id: msg.id,
|
|
597
597
|
from: msg.from,
|
|
@@ -600,11 +600,9 @@ function buildMessageResponse(msg, consumedIds) {
|
|
|
600
600
|
priority: classifyPriority(msg),
|
|
601
601
|
...(msg.reply_to && { reply_to: msg.reply_to }),
|
|
602
602
|
...(msg.thread_id && { thread_id: msg.thread_id }),
|
|
603
|
-
...(reportBackReminder && { _protocol: reportBackReminder }),
|
|
604
603
|
},
|
|
605
604
|
pending_count: pendingCount,
|
|
606
605
|
agents_online: agentsOnline,
|
|
607
|
-
coordinator_mode: getConfig().coordinator_mode || 'responsive',
|
|
608
606
|
...(taskReminder && { task_reminder: taskReminder }),
|
|
609
607
|
};
|
|
610
608
|
}
|
|
@@ -1159,57 +1157,39 @@ function buildGuide(level = 'standard') {
|
|
|
1159
1157
|
const managed = getManagedConfig();
|
|
1160
1158
|
const isManager = managed.manager === registeredName;
|
|
1161
1159
|
if (isManager) {
|
|
1162
|
-
rules.push('
|
|
1163
|
-
rules.push('
|
|
1164
|
-
rules.push('
|
|
1165
|
-
rules.push('
|
|
1166
|
-
rules.push('Use send_message() to give instructions. Use broadcast() for team announcements.');
|
|
1167
|
-
rules.push('STRICT RULE: After EVERY action, call listen() to wait for responses. NEVER use sleep(). Your loop is: act → listen() → act → listen(). This loop NEVER ends.');
|
|
1160
|
+
rules.push('ROLE: You are the Manager. You assign work and control who speaks.');
|
|
1161
|
+
rules.push('LOOP: create_task/create_workflow → yield_floor(agent) → listen() → process results → repeat. Never stop.');
|
|
1162
|
+
rules.push('Use update_task(id, status="done") and advance_workflow() as agents finish.');
|
|
1163
|
+
rules.push('Use set_phase() to move through discussion → planning → execution → review.');
|
|
1168
1164
|
} else {
|
|
1169
|
-
rules.push('
|
|
1170
|
-
rules.push('
|
|
1171
|
-
rules.push('
|
|
1172
|
-
rules.push('STRICT RULES: NEVER use sleep(). NEVER use check_messages() in a loop. NEVER call get_work() in managed mode. Your ONLY loop is: listen() → work → update task → respond → listen(). If listen() times out, call listen() again immediately.');
|
|
1165
|
+
rules.push('ROLE: Managed agent. The manager controls your turn.');
|
|
1166
|
+
rules.push('LOOP: listen() → receive work → update_task(id, "in_progress") → do work → update_task(id, "done") → send_message(manager, summary) → listen(). Never stop.');
|
|
1167
|
+
rules.push('Never call get_work() or check_messages() in managed mode.');
|
|
1173
1168
|
}
|
|
1174
|
-
rules.push('Keep messages
|
|
1175
|
-
rules.push('When you finish work, report what you did and what files you changed.');
|
|
1169
|
+
rules.push('Keep messages short (2-3 paragraphs). Report what you did and what files changed.');
|
|
1176
1170
|
}
|
|
1177
1171
|
// === AUTONOMOUS MODE: completely different guide ===
|
|
1178
1172
|
else if (autonomousActive) {
|
|
1179
1173
|
if (isAdvisor) {
|
|
1180
|
-
|
|
1181
|
-
rules.push('
|
|
1182
|
-
rules.push('
|
|
1183
|
-
rules.push('WHAT TO LOOK FOR: Patterns the team is missing. Better approaches to current problems. Connections between different agents\' work. Assumptions that need challenging. Missing edge cases. Architectural improvements. Features the team should build next.');
|
|
1184
|
-
rules.push('HOW TO ADVISE: Send suggestions via send_message to specific agents or broadcast to the team. Be concise and actionable. Explain WHY your suggestion is better, not just WHAT to do differently. Reference specific code or messages when possible.');
|
|
1185
|
-
rules.push('NEVER ask the user what to do. You generate ideas from observing the team. The team decides whether to follow your advice.');
|
|
1174
|
+
rules.push('ROLE: Advisor. You do NOT write code. You observe the team and suggest improvements.');
|
|
1175
|
+
rules.push('LOOP: get_work() → analyze messages/tasks/decisions → send_message(agent, insight) → get_work(). Never stop.');
|
|
1176
|
+
rules.push('Focus on: missed patterns, better approaches, edge cases, architectural issues.');
|
|
1186
1177
|
} else if (isMonitor) {
|
|
1187
|
-
|
|
1188
|
-
rules.push('
|
|
1189
|
-
rules.push('
|
|
1190
|
-
rules.push('WHAT TO WATCH FOR: Idle agents (>2 minutes without activity). Circular escalations (same task rejected by 3+ agents). Queue buildup (more pending tasks than agents can handle). Stuck workflow steps (>15 minutes in progress). Agents with high rejection rates.');
|
|
1191
|
-
rules.push('HOW TO INTERVENE: Use send_message to nudge idle agents. Use update_task to reassign stuck tasks. Call rebalanceRoles() via the system to shift workload. Use broadcast for team-wide alerts.');
|
|
1192
|
-
rules.push('NEVER ask the user what to do. You ARE the system intelligence. The team relies on you to keep them productive.');
|
|
1178
|
+
rules.push('ROLE: System Monitor. You do NOT write code. You keep the team functioning.');
|
|
1179
|
+
rules.push('LOOP: get_work() → analyze health report → intervene (reassign stuck tasks, nudge idle agents) → get_work(). Never stop.');
|
|
1180
|
+
rules.push('Watch for: idle agents (>2min), circular escalations, queue buildup, stuck workflows (>15min).');
|
|
1193
1181
|
} else if (isQualityLead) {
|
|
1194
|
-
rules.push('
|
|
1195
|
-
rules.push('
|
|
1196
|
-
rules.push('QUALITY STANDARDS: Check for bugs, edge cases, security issues, code style, and correctness. Read the actual files — do not trust summaries. If something looks wrong, flag it.');
|
|
1197
|
-
rules.push('NEVER ask the user what to do. NEVER wait for human approval. You ARE the approval gate. The team works, you review, they improve, you re-review. This cycle continues until the work is excellent.');
|
|
1182
|
+
rules.push('ROLE: Quality Lead. You review ALL team work. Never approve without reading the actual code.');
|
|
1183
|
+
rules.push('LOOP: get_work() → review code → submit_review()/verify_and_advance() or submit_review(status="changes_requested", feedback) → get_work(). Never stop.');
|
|
1198
1184
|
} else {
|
|
1199
|
-
rules.push('
|
|
1185
|
+
rules.push('LOOP: get_work() → do the work → verify_and_advance() → get_work(). Never stop. Never wait for approval.');
|
|
1200
1186
|
rules.push(qualityLeadName
|
|
1201
|
-
? '
|
|
1202
|
-
: '
|
|
1203
|
-
}
|
|
1204
|
-
rules.push('
|
|
1205
|
-
rules.push('
|
|
1206
|
-
rules.push('
|
|
1207
|
-
rules.push('Keep messages to 2-3 paragraphs max.');
|
|
1208
|
-
rules.push('When you finish work, report what you did and what files you changed.');
|
|
1209
|
-
rules.push('Lock files before editing shared code (lock_file / unlock_file).');
|
|
1210
|
-
// UE5 safety rules — prevent concurrent editor operations
|
|
1211
|
-
rules.push('UE5 SAFETY: BEFORE any Unreal Engine editor operation (spawning, modifying scene, placing assets): call lock_file("ue5-editor"). BEFORE compiling/building: call lock_file("ue5-compile"). Unlock immediately after. Only ONE agent can hold each lock — others must wait.');
|
|
1212
|
-
rules.push('Log team decisions with log_decision() so they are not re-debated.');
|
|
1187
|
+
? 'After completing work, report to ' + qualityLeadName + ' (Quality Lead) via send_message, then get_work().'
|
|
1188
|
+
: 'After completing work, call get_work() for your next task.');
|
|
1189
|
+
}
|
|
1190
|
+
rules.push('If stuck: retry up to 3 times, then ask team via send_message, then move to next task.');
|
|
1191
|
+
rules.push('Lock files before editing (lock_file/unlock_file). Log decisions with log_decision().');
|
|
1192
|
+
rules.push('Keep messages short (2-3 paragraphs). Report what you did and what files changed.');
|
|
1213
1193
|
|
|
1214
1194
|
// User-customizable project-specific rules
|
|
1215
1195
|
const guideFile = path.join(DATA_DIR, 'guide.md');
|
|
@@ -1239,101 +1219,64 @@ function buildGuide(level = 'standard') {
|
|
|
1239
1219
|
}
|
|
1240
1220
|
}
|
|
1241
1221
|
|
|
1242
|
-
|
|
1222
|
+
const result = {
|
|
1243
1223
|
rules,
|
|
1244
|
-
|
|
1245
|
-
tier_info: `${rules.length} rules (AUTONOMOUS MODE, ${aliveCount} agents, role: ${myRole || 'unassigned'})`,
|
|
1246
|
-
first_steps: isAdvisor
|
|
1247
|
-
? '1. Call get_work() to get team context (messages, tasks, decisions). 2. Think deeply about patterns, improvements, missing features. 3. Send insights to team. 4. Call get_work() again. Never stop thinking.'
|
|
1248
|
-
: isMonitor
|
|
1249
|
-
? '1. Call get_work() to get system health report. 2. Analyze: idle agents, stuck tasks, circular escalations. 3. Intervene: reassign, nudge, rebalance. 4. Call get_work() again. Never stop monitoring.'
|
|
1250
|
-
: isQualityLead
|
|
1251
|
-
? '1. Call get_work() to find work to review. 2. Review thoroughly. 3. Approve or request changes. 4. Call get_work() again. Never stop.'
|
|
1252
|
-
: '1. Call get_work() to get your assignment. 2. Do the work. 3. Call verify_and_advance(). 4. Call get_work() again. Never stop.',
|
|
1224
|
+
first_steps: 'Call get_work() to get your next assignment.',
|
|
1253
1225
|
autonomous_mode: true,
|
|
1254
1226
|
your_role: myRole || undefined,
|
|
1255
|
-
quality_lead: qualityLeadName || undefined,
|
|
1256
|
-
tool_categories: {
|
|
1257
|
-
'WORK LOOP': 'get_work, verify_and_advance, retry_with_improvement',
|
|
1258
|
-
'MESSAGING': 'send_message, broadcast, check_messages, consume_messages, get_history, handoff, share_file',
|
|
1259
|
-
'COORDINATION': 'get_briefing, log_decision, get_decisions, kb_write, kb_read, kb_list',
|
|
1260
|
-
'TASKS': 'create_task, update_task, list_tasks, suggest_task',
|
|
1261
|
-
'QUALITY': 'request_review, submit_review',
|
|
1262
|
-
'SAFETY': 'lock_file, unlock_file',
|
|
1263
|
-
},
|
|
1264
1227
|
};
|
|
1228
|
+
if (projectRules.length > 0) result.project_rules = projectRules;
|
|
1229
|
+
return result;
|
|
1265
1230
|
}
|
|
1266
1231
|
|
|
1267
1232
|
// === STANDARD MODE (non-autonomous) ===
|
|
1268
|
-
// Self-continuation rules apply in standard mode too (for 2+ agent teams)
|
|
1269
1233
|
if (aliveCount >= 2 && (mode === 'group' || mode === 'managed')) {
|
|
1270
1234
|
if (isQualityLead) {
|
|
1271
|
-
rules.push('
|
|
1235
|
+
rules.push('ROLE: Quality Lead. Review all team work via submit_review(). You are the approval gate.');
|
|
1272
1236
|
} else if (qualityLeadName) {
|
|
1273
|
-
rules.push('
|
|
1237
|
+
rules.push('After completing work, report to ' + qualityLeadName + ' (Quality Lead), then continue.');
|
|
1274
1238
|
}
|
|
1275
1239
|
}
|
|
1276
1240
|
|
|
1277
|
-
// Lead/Coordinator mode: responsive (stay with human) vs autonomous (run in listen loop)
|
|
1278
1241
|
if (isLeadRole && aliveCount >= 2) {
|
|
1279
1242
|
const coordinatorMode = getConfig().coordinator_mode || 'responsive';
|
|
1280
1243
|
if (coordinatorMode === 'responsive') {
|
|
1281
|
-
rules.push('
|
|
1244
|
+
rules.push('COORDINATOR: Use consume_messages() to check updates non-blockingly. Do NOT block in listen() — stay responsive to the user.');
|
|
1282
1245
|
} else {
|
|
1283
|
-
rules.push('
|
|
1246
|
+
rules.push('COORDINATOR: Use listen() to wait for agent results. Only return to human when all tasks are done or blocked.');
|
|
1284
1247
|
}
|
|
1285
|
-
rules.push('
|
|
1248
|
+
rules.push('Coordinators do NOT edit files or write code. Delegate ALL code work to other agents.');
|
|
1286
1249
|
}
|
|
1287
1250
|
|
|
1288
|
-
// Tier 0 — THE one rule (always included at every level)
|
|
1289
1251
|
const listenCmd = isManagedMode() ? 'listen()' : (mode === 'group' ? 'listen_group()' : 'listen()');
|
|
1290
|
-
rules.push(`
|
|
1252
|
+
rules.push(`After EVERY action, call ${listenCmd}. Never use sleep() or poll with check_messages().`);
|
|
1291
1253
|
|
|
1292
|
-
// Minimal level: Tier 0 only — for experienced agents refreshing rules
|
|
1293
1254
|
if (level === 'minimal') {
|
|
1294
|
-
rules.push('
|
|
1295
|
-
rules.push('Lock files before editing shared code (lock_file / unlock_file).');
|
|
1255
|
+
rules.push('Lock files before editing (lock_file/unlock_file).');
|
|
1296
1256
|
if (mode === 'group' || mode === 'managed') {
|
|
1297
|
-
rules.push('Use reply_to
|
|
1298
|
-
rules.push('Messages not addressed to you show should_respond: false. Only respond if you have something new to add.');
|
|
1257
|
+
rules.push('Use reply_to for threading. Ignore messages with should_respond: false unless relevant.');
|
|
1299
1258
|
}
|
|
1300
1259
|
return {
|
|
1301
1260
|
rules,
|
|
1302
|
-
tier_info: `${rules.length} rules (minimal level, ${aliveCount} agents)`,
|
|
1303
1261
|
first_steps: mode === 'direct'
|
|
1304
|
-
? '
|
|
1262
|
+
? 'Call list_agents() to see who is online, then send a message or listen().'
|
|
1305
1263
|
: mode === 'managed'
|
|
1306
|
-
?
|
|
1307
|
-
:
|
|
1264
|
+
? 'Call get_briefing(), then listen() to wait for the manager.'
|
|
1265
|
+
: 'Call get_briefing(), then listen() to join.',
|
|
1308
1266
|
};
|
|
1309
1267
|
}
|
|
1310
1268
|
|
|
1311
|
-
|
|
1312
|
-
rules.push('
|
|
1313
|
-
rules.push('Keep messages to 2-3 paragraphs max.');
|
|
1314
|
-
rules.push('When you finish work, report what you did and what files you changed.');
|
|
1315
|
-
rules.push('Lock files before editing shared code (lock_file / unlock_file).');
|
|
1316
|
-
// UE5 safety rules — prevent concurrent editor operations
|
|
1317
|
-
rules.push('UE5 SAFETY: BEFORE any Unreal Engine editor operation (spawning, modifying scene, placing assets): call lock_file("ue5-editor"). BEFORE compiling/building: call lock_file("ue5-compile"). Unlock immediately after. Only ONE agent can hold each lock — others must wait.');
|
|
1269
|
+
rules.push('Keep messages short (2-3 paragraphs). Report what you did and what files changed.');
|
|
1270
|
+
rules.push('Lock files before editing (lock_file/unlock_file). Log decisions with log_decision().');
|
|
1318
1271
|
|
|
1319
|
-
// Tier 2 — group mode features (shown when group or managed mode)
|
|
1320
1272
|
if (mode === 'group' || mode === 'managed') {
|
|
1321
|
-
rules.push('Use reply_to
|
|
1322
|
-
rules.push('Messages not addressed to you show should_respond: false. Only respond if you have something new to add.');
|
|
1323
|
-
rules.push('Log team decisions with log_decision() so they are not re-debated.');
|
|
1273
|
+
rules.push('Use reply_to for threading (faster cooldown). Ignore should_respond: false unless relevant.');
|
|
1324
1274
|
}
|
|
1325
|
-
|
|
1326
|
-
// Tier 2b — channels (shown when channels exist beyond #general)
|
|
1327
1275
|
if (hasChannels) {
|
|
1328
|
-
rules.push('
|
|
1329
|
-
rules.push('Use channel parameter on send_message to keep discussions focused.');
|
|
1276
|
+
rules.push('Use join_channel() and channel param on send_message to keep discussions focused.');
|
|
1330
1277
|
}
|
|
1331
|
-
|
|
1332
|
-
// Tier 3 — large teams (shown when 5+ agents)
|
|
1333
1278
|
if (aliveCount >= 5) {
|
|
1334
|
-
rules.push(
|
|
1335
|
-
rules.push('Tasks auto-create channels (#task-xxx). Use them for focused discussion instead of #general.');
|
|
1336
|
-
rules.push('Use channels to split into sub-teams. Do not discuss everything in #general.');
|
|
1279
|
+
rules.push('Use task channels (#task-xxx) for focused discussion instead of #general.');
|
|
1337
1280
|
}
|
|
1338
1281
|
|
|
1339
1282
|
// User-customizable project-specific rules from .neohive/guide.md
|
|
@@ -1364,51 +1307,25 @@ function buildGuide(level = 'standard') {
|
|
|
1364
1307
|
}
|
|
1365
1308
|
}
|
|
1366
1309
|
|
|
1367
|
-
|
|
1368
|
-
rules,
|
|
1369
|
-
project_rules: projectRules.length > 0 ? projectRules : undefined,
|
|
1370
|
-
tier_info: `${rules.length} rules (${aliveCount} agents, ${mode} mode${hasChannels ? ', channels active' : ''})`,
|
|
1371
|
-
first_steps: mode === 'direct'
|
|
1372
|
-
? '1. Call list_agents() to see who is online. 2. Send a message or call listen() to wait.'
|
|
1373
|
-
: '1. Call get_briefing() for project context. 2. Call listen_group() to join. 3. Respond and listen_group() again.',
|
|
1374
|
-
tool_categories: {
|
|
1375
|
-
'MESSAGING': 'send_message, broadcast, listen_group, listen, check_messages, consume_messages, get_history, get_summary, search_messages, handoff, share_file',
|
|
1376
|
-
'COORDINATION': 'get_briefing, log_decision, get_decisions, kb_write, kb_read, kb_list, call_vote, cast_vote, vote_status',
|
|
1377
|
-
'TASKS': 'create_task, update_task, list_tasks, declare_dependency, check_dependencies, suggest_task',
|
|
1378
|
-
'QUALITY': 'update_progress, get_progress, request_review, submit_review, get_reputation',
|
|
1379
|
-
'SAFETY': 'lock_file, unlock_file',
|
|
1380
|
-
'CHANNELS': 'join_channel, leave_channel, list_channels',
|
|
1381
|
-
...(mode === 'managed' ? { 'MANAGED MODE': 'claim_manager, yield_floor, set_phase' } : {}),
|
|
1382
|
-
},
|
|
1383
|
-
};
|
|
1384
|
-
|
|
1385
|
-
// Full level: add tool descriptions for complete reference
|
|
1386
|
-
if (level === 'full') {
|
|
1387
|
-
result.tool_details = {
|
|
1388
|
-
'listen_group': 'Blocks until messages arrive. Returns batch with priorities, context, agent statuses.',
|
|
1389
|
-
'send_message': 'Send to agent (to param). reply_to for threading. channel for sub-channels.',
|
|
1390
|
-
'lock_file / unlock_file': 'Exclusive file locking. Auto-releases on disconnect.',
|
|
1391
|
-
'log_decision': 'Persist decisions to prevent re-debating. Visible in get_briefing().',
|
|
1392
|
-
'create_task / update_task': 'Structured task management. Auto-creates channels at 5+ agents.',
|
|
1393
|
-
'kb_write / kb_read': 'Shared knowledge base. Any agent can read/write.',
|
|
1394
|
-
'suggest_task': 'AI-suggested next task based on your strengths and pending work.',
|
|
1395
|
-
'request_review / submit_review': 'Structured code review workflow with notifications.',
|
|
1396
|
-
'declare_dependency': 'Block a task until another completes. Auto-notifies on resolution.',
|
|
1397
|
-
'get_compressed_history': 'Summarized history for catching up without context overflow.',
|
|
1398
|
-
};
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// Task reminder: show agent's pending/in_progress tasks so they remember to update them
|
|
1310
|
+
// Task reminder
|
|
1402
1311
|
if (registeredName) {
|
|
1403
1312
|
try {
|
|
1404
1313
|
const myTasks = getTasks().filter(t => t.assignee === registeredName && (t.status === 'pending' || t.status === 'in_progress'));
|
|
1405
1314
|
if (myTasks.length > 0) {
|
|
1406
|
-
|
|
1407
|
-
rules.push(`TASK STATUS: You have ${myTasks.length} task(s). Use update_task(task_id, "in_progress") when starting and update_task(task_id, "done") when complete. Your tasks: ${myTasks.map(t => t.id + ' "' + t.title.substring(0, 40) + '" (' + t.status + ')').join('; ')}`);
|
|
1315
|
+
rules.push(`You have ${myTasks.length} task(s): ${myTasks.map(t => t.id + ' "' + t.title.substring(0, 40) + '" (' + t.status + ')').join('; ')}. Update status with update_task().`);
|
|
1408
1316
|
}
|
|
1409
1317
|
} catch (e) { log.debug('task reminder in guide failed:', e.message); }
|
|
1410
1318
|
}
|
|
1411
1319
|
|
|
1320
|
+
const result = {
|
|
1321
|
+
rules,
|
|
1322
|
+
first_steps: mode === 'direct'
|
|
1323
|
+
? 'Call list_agents() to see who is online, then send a message or listen().'
|
|
1324
|
+
: 'Call get_briefing(), then listen() to join.',
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
if (projectRules.length > 0) result.project_rules = projectRules;
|
|
1328
|
+
|
|
1412
1329
|
// Cache the result for subsequent calls with same params
|
|
1413
1330
|
_guideCache = { key: cacheKey, result };
|
|
1414
1331
|
return result;
|
|
@@ -1567,79 +1484,82 @@ function toolRegister(name, provider = null, skills = null) {
|
|
|
1567
1484
|
const config = getConfig();
|
|
1568
1485
|
const mode = config.conversation_mode || 'direct';
|
|
1569
1486
|
const otherAgents = Object.keys(getAgents()).filter(n => n !== name);
|
|
1487
|
+
const guide = buildGuide();
|
|
1570
1488
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1489
|
+
// Auto-assign roles when 2+ agents are online
|
|
1490
|
+
const aliveCount = Object.values(getAgents()).filter(a => isPidAlive(a.pid, a.last_activity)).length;
|
|
1491
|
+
let assignedRole = guide.your_role || undefined;
|
|
1492
|
+
if (aliveCount >= 2) {
|
|
1493
|
+
try {
|
|
1494
|
+
const roleAssignments = autoAssignRoles();
|
|
1495
|
+
if (roleAssignments && roleAssignments[name]) assignedRole = roleAssignments[name];
|
|
1496
|
+
} catch (e) { log.debug("role assignment failed:", e.message); }
|
|
1497
|
+
}
|
|
1578
1498
|
|
|
1579
|
-
// Recovery
|
|
1499
|
+
// --- Recovery detection ---
|
|
1580
1500
|
const myTasks = getTasks().filter(t => t.assignee === name && t.status !== 'done');
|
|
1581
1501
|
const myWorkspace = getWorkspace(name);
|
|
1582
|
-
// Scale fix: tail-read last 30 messages instead of entire history
|
|
1583
1502
|
const recentHistory = tailReadJsonl(getHistoryFile(currentBranch), 30);
|
|
1584
1503
|
const myRecentMsgs = recentHistory.filter(m => m.to === name || m.from === name).slice(-5);
|
|
1504
|
+
let isResuming = false;
|
|
1505
|
+
let resumeContext = null;
|
|
1585
1506
|
|
|
1586
|
-
if (myTasks.length > 0 || Object.keys(myWorkspace).length > 0 || myRecentMsgs.length > 0) {
|
|
1587
|
-
result.recovery = {};
|
|
1588
|
-
if (myTasks.length > 0) result.recovery.your_active_tasks = myTasks.map(t => ({ id: t.id, title: t.title, status: t.status }));
|
|
1589
|
-
if (Object.keys(myWorkspace).length > 0) result.recovery.your_workspace_keys = Object.keys(myWorkspace);
|
|
1590
|
-
if (myRecentMsgs.length > 0) result.recovery.recent_messages = myRecentMsgs.map(m => ({ from: m.from, to: m.to, preview: m.content.substring(0, 100), timestamp: m.timestamp }));
|
|
1591
|
-
result.recovery.hint = 'You have prior context from a previous session. Call get_briefing() for a full project summary.';
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
// Auto-recovery: load crash snapshot if it exists (TTL: 1 hour)
|
|
1595
1507
|
const recoveryFile = path.join(DATA_DIR, `recovery-${name}.json`);
|
|
1596
1508
|
if (fs.existsSync(recoveryFile)) {
|
|
1597
1509
|
try {
|
|
1598
1510
|
const snapshot = JSON.parse(fs.readFileSync(recoveryFile, 'utf8'));
|
|
1599
1511
|
const snapshotAge = Date.now() - new Date(snapshot.died_at).getTime();
|
|
1600
1512
|
if (snapshotAge > 3600000) {
|
|
1601
|
-
// Stale snapshot (>1 hour) — discard
|
|
1602
1513
|
try { fs.unlinkSync(recoveryFile); } catch {}
|
|
1603
1514
|
} else {
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if (snapshot.
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
if (snapshot.channels && snapshot.channels.length > 0) result.recovery.your_channels = snapshot.channels;
|
|
1614
|
-
if (snapshot.last_messages_sent) result.recovery.last_messages_sent = snapshot.last_messages_sent;
|
|
1615
|
-
// Agent memory fields
|
|
1616
|
-
if (snapshot.decisions_made && snapshot.decisions_made.length > 0) result.recovery.decisions_made = snapshot.decisions_made;
|
|
1617
|
-
if (snapshot.tasks_completed && snapshot.tasks_completed.length > 0) result.recovery.tasks_completed = snapshot.tasks_completed;
|
|
1618
|
-
if (snapshot.kb_entries_written && snapshot.kb_entries_written.length > 0) result.recovery.kb_entries_written = snapshot.kb_entries_written;
|
|
1619
|
-
if (snapshot.graceful) result.recovery.was_graceful = true;
|
|
1620
|
-
result.recovery.hint = snapshot.graceful
|
|
1621
|
-
? 'You are RESUMING from a previous session that exited gracefully. Your memory (decisions, completed tasks, KB entries) is below. Continue where you left off.'
|
|
1622
|
-
: 'You are RESUMING a previous session that crashed. Review your active tasks and locked files below, then continue where you left off. Do NOT restart work from scratch.';
|
|
1623
|
-
// Clean up snapshot after loading
|
|
1515
|
+
isResuming = true;
|
|
1516
|
+
resumeContext = {
|
|
1517
|
+
crashed_ago: Math.round(snapshotAge / 1000) + 's',
|
|
1518
|
+
was_graceful: !!snapshot.graceful,
|
|
1519
|
+
};
|
|
1520
|
+
if (snapshot.active_tasks && snapshot.active_tasks.length > 0) resumeContext.active_tasks = snapshot.active_tasks;
|
|
1521
|
+
if (snapshot.locked_files && snapshot.locked_files.length > 0) resumeContext.files_to_relock = snapshot.locked_files;
|
|
1522
|
+
if (snapshot.decisions_made && snapshot.decisions_made.length > 0) resumeContext.decisions_made = snapshot.decisions_made;
|
|
1523
|
+
if (snapshot.tasks_completed && snapshot.tasks_completed.length > 0) resumeContext.tasks_completed = snapshot.tasks_completed;
|
|
1624
1524
|
try { fs.unlinkSync(recoveryFile); } catch {}
|
|
1625
1525
|
}
|
|
1626
1526
|
} catch (e) { log.debug("recovery file parse failed:", e.message); }
|
|
1627
1527
|
}
|
|
1628
1528
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1529
|
+
if (!isResuming && (myTasks.length > 0 || myRecentMsgs.length > 0)) {
|
|
1530
|
+
isResuming = true;
|
|
1531
|
+
resumeContext = {};
|
|
1532
|
+
if (myTasks.length > 0) resumeContext.active_tasks = myTasks.map(t => ({ id: t.id, title: t.title, status: t.status }));
|
|
1533
|
+
}
|
|
1631
1534
|
|
|
1632
|
-
//
|
|
1633
|
-
|
|
1634
|
-
if (
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1535
|
+
// --- Build next_action: the ONE thing the agent should do right now ---
|
|
1536
|
+
let nextAction;
|
|
1537
|
+
if (isResuming && resumeContext && resumeContext.active_tasks && resumeContext.active_tasks.length > 0) {
|
|
1538
|
+
nextAction = `You have ${resumeContext.active_tasks.length} unfinished task(s). Call list_tasks() to see them and resume work.`;
|
|
1539
|
+
} else if (guide.first_steps) {
|
|
1540
|
+
const firstStep = guide.first_steps.split(/\d+\.\s+/).filter(Boolean)[0];
|
|
1541
|
+
nextAction = firstStep ? firstStep.trim().replace(/\.$/, '') : guide.first_steps;
|
|
1542
|
+
} else {
|
|
1543
|
+
nextAction = 'Call get_briefing() to load project context';
|
|
1641
1544
|
}
|
|
1642
1545
|
|
|
1546
|
+
// --- Build the result: next_action FIRST, then context ---
|
|
1547
|
+
const result = {
|
|
1548
|
+
success: true,
|
|
1549
|
+
registered: name,
|
|
1550
|
+
next_action: nextAction,
|
|
1551
|
+
mode,
|
|
1552
|
+
agents_online: otherAgents,
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
if (assignedRole) result.your_role = assignedRole;
|
|
1556
|
+
if (isResuming) result.resuming = resumeContext;
|
|
1557
|
+
|
|
1558
|
+
result.guide = guide;
|
|
1559
|
+
|
|
1560
|
+
// Notify other agents
|
|
1561
|
+
fireEvent('agent_join', { agent: name });
|
|
1562
|
+
|
|
1643
1563
|
return result;
|
|
1644
1564
|
} finally {
|
|
1645
1565
|
unlockAgentsFile();
|
|
@@ -1659,9 +1579,8 @@ function setListening(isListening) {
|
|
|
1659
1579
|
if (!registeredName) return;
|
|
1660
1580
|
_isCurrentlyListening = !!isListening;
|
|
1661
1581
|
|
|
1662
|
-
// Track listen calls in heartbeat for auto-nudge system
|
|
1663
1582
|
if (isListening) {
|
|
1664
|
-
touchActivity(true);
|
|
1583
|
+
touchActivity(true);
|
|
1665
1584
|
}
|
|
1666
1585
|
|
|
1667
1586
|
try {
|
|
@@ -2044,19 +1963,6 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
|
|
|
2044
1963
|
result.note = `Agent "${to}" is currently working (not in listen mode). Message queued — they'll see it when they finish their current task and call listen_group().`;
|
|
2045
1964
|
}
|
|
2046
1965
|
|
|
2047
|
-
// Mode awareness hint: warn if agent seems to be in wrong mode
|
|
2048
|
-
const currentMode = getConfig().conversation_mode || 'direct';
|
|
2049
|
-
if (currentMode === 'group' || currentMode === 'managed') {
|
|
2050
|
-
result.mode_hint = `You're in ${currentMode} mode. Use listen_group() (or listen() — both auto-detect) to stay in the conversation.`;
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
// Nudge: check if THIS agent has unread messages waiting
|
|
2054
|
-
const myPending = getUnconsumedMessages(registeredName);
|
|
2055
|
-
if (myPending.length > 0) {
|
|
2056
|
-
result.you_have_messages = myPending.length;
|
|
2057
|
-
result.urgent = `You have ${myPending.length} unread message(s) waiting. Call listen_group() after this to read them.`;
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
1966
|
// Coordinator enforcement: warn if sending work assignment without creating a task first
|
|
2061
1967
|
const senderProfile = getProfiles()[registeredName];
|
|
2062
1968
|
const senderRole = senderProfile && senderProfile.role ? senderProfile.role.toLowerCase() : '';
|
|
@@ -2067,7 +1973,7 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
|
|
|
2067
1973
|
const recentTasks = getTasks().filter(t => {
|
|
2068
1974
|
if (t.assignee !== to) return false;
|
|
2069
1975
|
const age = Date.now() - new Date(t.created_at).getTime();
|
|
2070
|
-
return age < 60000;
|
|
1976
|
+
return age < 60000;
|
|
2071
1977
|
});
|
|
2072
1978
|
if (recentTasks.length === 0) {
|
|
2073
1979
|
result.task_warning = `No task created for this assignment to ${to}. Use create_task(title, description, "${to}") to formally track this work.`;
|
|
@@ -2075,6 +1981,8 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
|
|
|
2075
1981
|
}
|
|
2076
1982
|
}
|
|
2077
1983
|
|
|
1984
|
+
result.next_action = 'Call listen() to receive replies.';
|
|
1985
|
+
|
|
2078
1986
|
return result;
|
|
2079
1987
|
}
|
|
2080
1988
|
|
|
@@ -2132,10 +2040,7 @@ function toolBroadcast(content) {
|
|
|
2132
2040
|
sendsSinceLastListen++;
|
|
2133
2041
|
unaddressedSends++; // broadcasts are always unaddressed
|
|
2134
2042
|
const aliveOthers = otherAgents.filter(n => { const a = agents[n]; return isPidAlive(a.pid, a.last_activity); });
|
|
2135
|
-
const result = { success: true, messageId: msg.id, recipient_count: aliveOthers.length,
|
|
2136
|
-
// Nudge for own unread messages
|
|
2137
|
-
const myPending = getUnconsumedMessages(registeredName);
|
|
2138
|
-
if (myPending.length > 0) { result.you_have_messages = myPending.length; result.urgent = `You have ${myPending.length} unread message(s). Call listen_group() soon.`; }
|
|
2043
|
+
const result = { success: true, messageId: msg.id, recipient_count: aliveOthers.length, next_action: 'Call listen() to receive replies.' };
|
|
2139
2044
|
return result;
|
|
2140
2045
|
}
|
|
2141
2046
|
|
|
@@ -2161,21 +2066,8 @@ function toolBroadcast(content) {
|
|
|
2161
2066
|
touchActivity();
|
|
2162
2067
|
lastSentAt = Date.now();
|
|
2163
2068
|
|
|
2164
|
-
const result = { success: true, sent_to: ids, recipient_count: ids.length };
|
|
2069
|
+
const result = { success: true, sent_to: ids, recipient_count: ids.length, next_action: 'Call listen() to receive replies.' };
|
|
2165
2070
|
if (skipped.length > 0) result.skipped = skipped;
|
|
2166
|
-
// Show which recipients are busy vs listening
|
|
2167
|
-
const agentsNow = getAgents();
|
|
2168
|
-
const busy = ids.filter(function(i) { return agentsNow[i.to] && !agentsNow[i.to].listening_since; }).map(function(i) { return i.to; });
|
|
2169
|
-
if (busy.length > 0) {
|
|
2170
|
-
result.busy_agents = busy;
|
|
2171
|
-
result.note = busy.join(', ') + (busy.length === 1 ? ' is' : ' are') + ' currently working (not listening). Messages queued.';
|
|
2172
|
-
}
|
|
2173
|
-
// Nudge for own unread messages
|
|
2174
|
-
const myPending = getUnconsumedMessages(registeredName);
|
|
2175
|
-
if (myPending.length > 0) {
|
|
2176
|
-
result.you_have_messages = myPending.length;
|
|
2177
|
-
result.urgent = `You have ${myPending.length} unread message(s). Call listen_group() soon.`;
|
|
2178
|
-
}
|
|
2179
2071
|
return result;
|
|
2180
2072
|
}
|
|
2181
2073
|
|
|
@@ -2442,8 +2334,9 @@ async function toolListen(from = null, outcome = null, task_id = null, summary =
|
|
|
2442
2334
|
|
|
2443
2335
|
let watcher;
|
|
2444
2336
|
let fallbackInterval;
|
|
2337
|
+
let timer;
|
|
2338
|
+
let heartbeatTimer;
|
|
2445
2339
|
|
|
2446
|
-
// Helper: check for new messages
|
|
2447
2340
|
const checkMessages = () => {
|
|
2448
2341
|
const { messages: newMsgs, newOffset } = readNewMessages(lastReadOffset);
|
|
2449
2342
|
lastReadOffset = newOffset;
|
|
@@ -2463,34 +2356,44 @@ async function toolListen(from = null, outcome = null, task_id = null, summary =
|
|
|
2463
2356
|
return false;
|
|
2464
2357
|
};
|
|
2465
2358
|
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
if (checkMessages()) { clearInterval(fallbackInterval); return; }
|
|
2475
|
-
pollCount++;
|
|
2476
|
-
if (pollCount === 10) {
|
|
2477
|
-
clearInterval(fallbackInterval);
|
|
2478
|
-
fallbackInterval = setInterval(() => {
|
|
2479
|
-
if (checkMessages()) clearInterval(fallbackInterval);
|
|
2480
|
-
}, 2000);
|
|
2481
|
-
}
|
|
2482
|
-
}, 500);
|
|
2483
|
-
}
|
|
2359
|
+
// Auto-restart: instead of returning retry:true and hoping the agent
|
|
2360
|
+
// calls listen() again, loop internally. The agent stays blocked here
|
|
2361
|
+
// until a real message arrives or the IDE/user interrupts the tool call.
|
|
2362
|
+
function setupWatcher() {
|
|
2363
|
+
try { if (watcher) watcher.close(); } catch {}
|
|
2364
|
+
if (fallbackInterval) clearInterval(fallbackInterval);
|
|
2365
|
+
clearTimeout(timer);
|
|
2366
|
+
clearTimeout(heartbeatTimer);
|
|
2484
2367
|
|
|
2485
|
-
|
|
2486
|
-
|
|
2368
|
+
try {
|
|
2369
|
+
const msgFile = getMessagesFile(currentBranch);
|
|
2370
|
+
watcher = fs.watch(msgFile, () => { checkMessages(); });
|
|
2371
|
+
watcher.on('error', () => {});
|
|
2372
|
+
} catch {
|
|
2373
|
+
let pollCount = 0;
|
|
2374
|
+
fallbackInterval = setInterval(() => {
|
|
2375
|
+
if (checkMessages()) { clearInterval(fallbackInterval); return; }
|
|
2376
|
+
pollCount++;
|
|
2377
|
+
if (pollCount === 10) {
|
|
2378
|
+
clearInterval(fallbackInterval);
|
|
2379
|
+
fallbackInterval = setInterval(() => {
|
|
2380
|
+
if (checkMessages()) clearInterval(fallbackInterval);
|
|
2381
|
+
}, 2000);
|
|
2382
|
+
}
|
|
2383
|
+
}, 500);
|
|
2384
|
+
}
|
|
2487
2385
|
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2386
|
+
heartbeatTimer = setInterval(() => { touchHeartbeat(registeredName); }, 15000);
|
|
2387
|
+
|
|
2388
|
+
timer = setTimeout(() => {
|
|
2389
|
+
touchActivity();
|
|
2390
|
+
autoCompact();
|
|
2391
|
+
if (checkMessages()) return;
|
|
2392
|
+
setupWatcher();
|
|
2393
|
+
}, 300000);
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
setupWatcher();
|
|
2494
2397
|
});
|
|
2495
2398
|
}
|
|
2496
2399
|
|
|
@@ -2596,7 +2499,7 @@ async function toolListenCodex(from = null, outcome = null, task_id = null, summ
|
|
|
2596
2499
|
|
|
2597
2500
|
const timer = setTimeout(() => {
|
|
2598
2501
|
setListening(false);
|
|
2599
|
-
done({ retry: true,
|
|
2502
|
+
done({ retry: true, next_action: 'Call listen() again.', message: 'No messages yet.' });
|
|
2600
2503
|
}, 45000);
|
|
2601
2504
|
});
|
|
2602
2505
|
}
|
|
@@ -2888,8 +2791,8 @@ async function toolListenGroup(outcome = null, task_id = null, summary = null) {
|
|
|
2888
2791
|
if (fallbackInterval) clearInterval(fallbackInterval);
|
|
2889
2792
|
if (batch && batch.length > 0) {
|
|
2890
2793
|
resolve(buildListenGroupResponse(batch, consumed, registeredName, listenStart));
|
|
2891
|
-
} else {
|
|
2892
|
-
//
|
|
2794
|
+
} else if (autonomousTimeout) {
|
|
2795
|
+
// Autonomous mode: return so agent goes back to get_work() loop
|
|
2893
2796
|
setListening(false);
|
|
2894
2797
|
sendsSinceLastListen = 0;
|
|
2895
2798
|
sendLimit = 2;
|
|
@@ -2897,74 +2800,94 @@ async function toolListenGroup(outcome = null, task_id = null, summary = null) {
|
|
|
2897
2800
|
resolve({
|
|
2898
2801
|
messages: [],
|
|
2899
2802
|
message_count: 0,
|
|
2900
|
-
|
|
2901
|
-
batch_summary:
|
|
2803
|
+
next_action: 'Call get_work() for your next assignment.',
|
|
2804
|
+
batch_summary: 'No new messages.',
|
|
2902
2805
|
});
|
|
2806
|
+
} else {
|
|
2807
|
+
// Standard/group mode: don't return — re-enter the wait loop internally.
|
|
2808
|
+
// This prevents the agent from breaking out of listen mode.
|
|
2809
|
+
resolved = false;
|
|
2810
|
+
touchHeartbeat(registeredName);
|
|
2811
|
+
autoCompact();
|
|
2812
|
+
const freshBatch = collectBatch();
|
|
2813
|
+
if (freshBatch.length > 0) {
|
|
2814
|
+
resolved = true;
|
|
2815
|
+
resolve(buildListenGroupResponse(freshBatch, consumed, registeredName, listenStart));
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
setupWatchers();
|
|
2903
2819
|
}
|
|
2904
2820
|
};
|
|
2905
2821
|
|
|
2906
2822
|
let watcher;
|
|
2907
2823
|
let channelWatchers = [];
|
|
2908
2824
|
let fallbackInterval;
|
|
2825
|
+
let timer;
|
|
2826
|
+
let heartbeatTimer;
|
|
2909
2827
|
|
|
2910
|
-
|
|
2911
|
-
//
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2828
|
+
function setupWatchers() {
|
|
2829
|
+
// Clean up previous watchers/timers if re-entering
|
|
2830
|
+
try { if (watcher) watcher.close(); } catch {}
|
|
2831
|
+
try { if (channelWatchers.length) channelWatchers.forEach(w => { try { w.close(); } catch {} }); } catch {}
|
|
2832
|
+
if (fallbackInterval) clearInterval(fallbackInterval);
|
|
2833
|
+
clearTimeout(timer);
|
|
2834
|
+
clearTimeout(heartbeatTimer);
|
|
2835
|
+
channelWatchers = [];
|
|
2918
2836
|
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2837
|
+
try {
|
|
2838
|
+
const msgFile = getMessagesFile(currentBranch);
|
|
2839
|
+
watcher = fs.watch(msgFile, () => {
|
|
2840
|
+
const batch = collectBatch();
|
|
2841
|
+
if (batch.length > 0) done(batch);
|
|
2842
|
+
});
|
|
2843
|
+
watcher.on('error', () => {});
|
|
2844
|
+
|
|
2845
|
+
const myChannels = getAgentChannels(registeredName);
|
|
2846
|
+
for (const ch of myChannels) {
|
|
2847
|
+
if (ch === 'general') continue;
|
|
2848
|
+
const chFile = getChannelMessagesFile(ch);
|
|
2849
|
+
if (fs.existsSync(chFile)) {
|
|
2850
|
+
try {
|
|
2851
|
+
const chWatcher = fs.watch(chFile, () => {
|
|
2852
|
+
const batch = collectBatch();
|
|
2853
|
+
if (batch.length > 0) done(batch);
|
|
2854
|
+
});
|
|
2855
|
+
chWatcher.on('error', () => {});
|
|
2856
|
+
channelWatchers.push(chWatcher);
|
|
2857
|
+
} catch (e) { log.debug("channel watcher setup failed:", e.message); }
|
|
2858
|
+
}
|
|
2933
2859
|
}
|
|
2860
|
+
} catch {
|
|
2861
|
+
let pollCount = 0;
|
|
2862
|
+
fallbackInterval = setInterval(() => {
|
|
2863
|
+
const batch = collectBatch();
|
|
2864
|
+
if (batch.length > 0) {
|
|
2865
|
+
clearInterval(fallbackInterval);
|
|
2866
|
+
done(batch);
|
|
2867
|
+
}
|
|
2868
|
+
pollCount++;
|
|
2869
|
+
if (pollCount === 10) {
|
|
2870
|
+
clearInterval(fallbackInterval);
|
|
2871
|
+
fallbackInterval = setInterval(() => {
|
|
2872
|
+
const batch = collectBatch();
|
|
2873
|
+
if (batch.length > 0) { clearInterval(fallbackInterval); done(batch); }
|
|
2874
|
+
}, 2000);
|
|
2875
|
+
}
|
|
2876
|
+
}, 500);
|
|
2934
2877
|
}
|
|
2935
|
-
} catch {
|
|
2936
|
-
// fs.watch not available — fall back to adaptive polling
|
|
2937
|
-
let pollCount = 0;
|
|
2938
|
-
fallbackInterval = setInterval(() => {
|
|
2939
|
-
const batch = collectBatch();
|
|
2940
|
-
if (batch.length > 0) {
|
|
2941
|
-
clearInterval(fallbackInterval);
|
|
2942
|
-
done(batch);
|
|
2943
|
-
}
|
|
2944
|
-
pollCount++;
|
|
2945
|
-
// Adaptive: slow down after initial fast checks
|
|
2946
|
-
if (pollCount === 10) {
|
|
2947
|
-
clearInterval(fallbackInterval);
|
|
2948
|
-
fallbackInterval = setInterval(() => {
|
|
2949
|
-
const batch = collectBatch();
|
|
2950
|
-
if (batch.length > 0) { clearInterval(fallbackInterval); done(batch); }
|
|
2951
|
-
}, 2000); // slow poll every 2s
|
|
2952
|
-
}
|
|
2953
|
-
}, 500); // fast poll first 5s
|
|
2954
|
-
}
|
|
2955
2878
|
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2879
|
+
heartbeatTimer = setInterval(() => {
|
|
2880
|
+
touchHeartbeat(registeredName);
|
|
2881
|
+
}, 15000);
|
|
2882
|
+
|
|
2883
|
+
const effectiveTimeout = autonomousTimeout
|
|
2884
|
+
? Math.min(autonomousTimeout, MAX_LISTEN_MS)
|
|
2885
|
+
: MAX_LISTEN_MS;
|
|
2960
2886
|
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
? Math.min(autonomousTimeout, MAX_LISTEN_MS)
|
|
2964
|
-
: MAX_LISTEN_MS;
|
|
2887
|
+
timer = setTimeout(() => done([]), effectiveTimeout);
|
|
2888
|
+
}
|
|
2965
2889
|
|
|
2966
|
-
|
|
2967
|
-
const timer = setTimeout(() => done([]), effectiveTimeout);
|
|
2890
|
+
setupWatchers();
|
|
2968
2891
|
});
|
|
2969
2892
|
}
|
|
2970
2893
|
|
|
@@ -3142,10 +3065,15 @@ function buildListenGroupResponse(batch, consumed, agentName, listenStart) {
|
|
|
3142
3065
|
}
|
|
3143
3066
|
}
|
|
3144
3067
|
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3068
|
+
if (isAutonomousMode()) {
|
|
3069
|
+
result.next_action = 'Process these messages, then call get_work().';
|
|
3070
|
+
} else if (result.should_respond === false) {
|
|
3071
|
+
result.next_action = 'Read these messages but do NOT respond. Call listen() to wait for your turn.';
|
|
3072
|
+
} else if (result.instructions && result.instructions.includes('MANAGER')) {
|
|
3073
|
+
result.next_action = 'You are the manager. Decide who speaks next, then call listen().';
|
|
3074
|
+
} else {
|
|
3075
|
+
result.next_action = 'Process these messages. Reply via send_message(), then call listen() again.';
|
|
3076
|
+
}
|
|
3149
3077
|
|
|
3150
3078
|
// Task reminder: remind agent of their outstanding tasks
|
|
3151
3079
|
try {
|
|
@@ -4203,6 +4131,7 @@ async function toolGetWork(params = {}) {
|
|
|
4203
4131
|
if (myStep) {
|
|
4204
4132
|
const result = {
|
|
4205
4133
|
type: 'workflow_step', priority: 'assigned', step: myStep,
|
|
4134
|
+
next_action: 'Do this work now. When done, call verify_and_advance().',
|
|
4206
4135
|
instruction: `You have assigned work: "${myStep.description}" (Workflow: "${myStep.workflow_name}"). Do this NOW. When done, call verify_and_advance().`
|
|
4207
4136
|
};
|
|
4208
4137
|
// Attach relevant KB skills for this task
|
|
@@ -4228,6 +4157,7 @@ async function toolGetWork(params = {}) {
|
|
|
4228
4157
|
return {
|
|
4229
4158
|
type: 'messages', priority: 'respond',
|
|
4230
4159
|
messages: pending.slice(0, 10), total: pending.length,
|
|
4160
|
+
next_action: 'Process these messages, then call get_work() again.',
|
|
4231
4161
|
instruction: 'Process these messages first, then call get_work() again.'
|
|
4232
4162
|
};
|
|
4233
4163
|
}
|
|
@@ -4252,6 +4182,7 @@ async function toolGetWork(params = {}) {
|
|
|
4252
4182
|
if (claimed) {
|
|
4253
4183
|
const claimResult = {
|
|
4254
4184
|
type: 'claimed_task', priority: 'self_assigned', task: best,
|
|
4185
|
+
next_action: 'Start working on this task now. Call verify_and_advance() when done.',
|
|
4255
4186
|
instruction: `No one was working on "${best.title}". I've assigned it to you. Start working on it now.`
|
|
4256
4187
|
};
|
|
4257
4188
|
const taskSkills = searchKBForTask(best.title + ' ' + (best.description || ''));
|
|
@@ -4269,6 +4200,7 @@ async function toolGetWork(params = {}) {
|
|
|
4269
4200
|
if (helpReqs.length > 0) {
|
|
4270
4201
|
return {
|
|
4271
4202
|
type: 'help_teammate', priority: 'assist', request: helpReqs[0],
|
|
4203
|
+
next_action: `Help ${helpReqs[0].from || 'your teammate'}, then call get_work() again.`,
|
|
4272
4204
|
instruction: `${helpReqs[0].from || 'A teammate'} needs help: "${helpReqs[0].content.substring(0, 200)}". Assist them.`
|
|
4273
4205
|
};
|
|
4274
4206
|
}
|
|
@@ -4278,6 +4210,7 @@ async function toolGetWork(params = {}) {
|
|
|
4278
4210
|
if (reviews.length > 0) {
|
|
4279
4211
|
return {
|
|
4280
4212
|
type: 'review', priority: 'review', review: reviews[0],
|
|
4213
|
+
next_action: 'Review this work, then call submit_review().',
|
|
4281
4214
|
instruction: `Review request from ${reviews[0].requested_by}: "${reviews[0].file}". Review their work and submit_review().`
|
|
4282
4215
|
};
|
|
4283
4216
|
}
|
|
@@ -4287,6 +4220,7 @@ async function toolGetWork(params = {}) {
|
|
|
4287
4220
|
if (blocked.length > 0) {
|
|
4288
4221
|
return {
|
|
4289
4222
|
type: 'unblock', priority: 'unblock', task: blocked[0],
|
|
4223
|
+
next_action: 'Try to unblock this task, then call get_work() again.',
|
|
4290
4224
|
instruction: `"${blocked[0].title}" is blocked. See if you can help unblock it.`
|
|
4291
4225
|
};
|
|
4292
4226
|
}
|
|
@@ -4310,6 +4244,7 @@ async function toolGetWork(params = {}) {
|
|
|
4310
4244
|
return {
|
|
4311
4245
|
type: 'stolen_task', priority: 'work_steal', task: stealable.task,
|
|
4312
4246
|
from_agent: stealable.from_agent,
|
|
4247
|
+
next_action: 'Start working on this task now. Call verify_and_advance() when done.',
|
|
4313
4248
|
instruction: stealable.message + ' Start working on it now.',
|
|
4314
4249
|
};
|
|
4315
4250
|
}
|
|
@@ -4322,6 +4257,7 @@ async function toolGetWork(params = {}) {
|
|
|
4322
4257
|
return {
|
|
4323
4258
|
type: 'messages', priority: 'respond',
|
|
4324
4259
|
messages: newMsgs.slice(0, 10), total: newMsgs.length,
|
|
4260
|
+
next_action: 'Process these messages, then call get_work() again.',
|
|
4325
4261
|
instruction: 'New messages arrived. Process them, then call get_work() again.'
|
|
4326
4262
|
};
|
|
4327
4263
|
}
|
|
@@ -4331,6 +4267,7 @@ async function toolGetWork(params = {}) {
|
|
|
4331
4267
|
if (upcoming) {
|
|
4332
4268
|
return {
|
|
4333
4269
|
type: 'prep_work', priority: 'proactive', step: upcoming,
|
|
4270
|
+
next_action: 'Prepare for this upcoming step, then call get_work() again.',
|
|
4334
4271
|
instruction: `Your next workflow step "${upcoming.description}" is coming up (Workflow: "${upcoming.workflow_name}"). Prepare for it: read relevant files, understand the dependencies, plan your approach.`
|
|
4335
4272
|
};
|
|
4336
4273
|
}
|
|
@@ -4340,9 +4277,10 @@ async function toolGetWork(params = {}) {
|
|
|
4340
4277
|
touchActivity();
|
|
4341
4278
|
const idleResult = {
|
|
4342
4279
|
type: 'idle',
|
|
4280
|
+
next_action: isManagedMode() ? 'Call listen() to wait for work.' : 'Call get_work() again in 30 seconds.',
|
|
4343
4281
|
instruction: isManagedMode()
|
|
4344
4282
|
? 'No work available right now. Call listen() to wait for the manager to assign work or give you the floor.'
|
|
4345
|
-
: 'No work available right now. Call get_work() again in 30 seconds.
|
|
4283
|
+
: 'No work available right now. Call get_work() again in 30 seconds.'
|
|
4346
4284
|
};
|
|
4347
4285
|
// Item 4: warn demoted agents
|
|
4348
4286
|
const agentRep = getReputation();
|
|
@@ -4399,7 +4337,7 @@ async function toolVerifyAndAdvance(params) {
|
|
|
4399
4337
|
const report = generateCompletionReport(wf);
|
|
4400
4338
|
const retrospective = logRetrospective(wf.id); // Item 9: analyze retry patterns
|
|
4401
4339
|
touchActivity();
|
|
4402
|
-
return { status: flagged ? 'workflow_complete_flagged' : 'workflow_complete', workflow_id: wf.id,
|
|
4340
|
+
return { status: flagged ? 'workflow_complete_flagged' : 'workflow_complete', workflow_id: wf.id, next_action: 'Call get_work() for your next assignment.', report, retrospective };
|
|
4403
4341
|
}
|
|
4404
4342
|
|
|
4405
4343
|
const agents = getAgents();
|
|
@@ -4421,7 +4359,7 @@ async function toolVerifyAndAdvance(params) {
|
|
|
4421
4359
|
status: flagged ? 'advanced_with_flag' : 'advanced', workflow_id: wf.id,
|
|
4422
4360
|
completed_step: currentStep.id,
|
|
4423
4361
|
next_steps: nextSteps.map(s => ({ id: s.id, description: s.description, assignee: s.assignee })),
|
|
4424
|
-
|
|
4362
|
+
next_action: 'Call get_work() for your next assignment.',
|
|
4425
4363
|
};
|
|
4426
4364
|
}
|
|
4427
4365
|
|
|
@@ -4452,7 +4390,7 @@ async function toolVerifyAndAdvance(params) {
|
|
|
4452
4390
|
touchActivity();
|
|
4453
4391
|
return {
|
|
4454
4392
|
status: 'needs_help', workflow_id: wf.id,
|
|
4455
|
-
|
|
4393
|
+
next_action: 'Call get_work() for other work while waiting for help.',
|
|
4456
4394
|
};
|
|
4457
4395
|
}
|
|
4458
4396
|
|
|
@@ -7679,13 +7617,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
7679
7617
|
} catch { return 'your coordinator'; }
|
|
7680
7618
|
})();
|
|
7681
7619
|
return {
|
|
7682
|
-
content: [{ type: 'text', text:
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
}
|
|
7620
|
+
content: [{ type: 'text', text:
|
|
7621
|
+
`BLOCKED — ${name}() was not executed. You made ${consecutiveNonListenCalls} tool calls without calling listen(). ` +
|
|
7622
|
+
`Do these two steps IN ORDER:\n` +
|
|
7623
|
+
`1. send_message(to="${coordinator}", content="I was blocked after ${consecutiveNonListenCalls} calls without listen(). I need to call ${name}. Should I proceed?")\n` +
|
|
7624
|
+
`2. listen()\n` +
|
|
7625
|
+
`Do NOT skip step 1. Do NOT call any other tool. Start with send_message now.`
|
|
7626
|
+
}],
|
|
7689
7627
|
isError: true,
|
|
7690
7628
|
};
|
|
7691
7629
|
}
|
|
@@ -7900,48 +7838,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
7900
7838
|
};
|
|
7901
7839
|
}
|
|
7902
7840
|
|
|
7903
|
-
// Global hook: on non-listen tools, check for pending messages and nudge with escalating urgency
|
|
7904
|
-
// Enhanced nudge: includes sender names, addressed count, and message preview
|
|
7905
|
-
const listenTools = ['listen', 'wait_for_reply'];
|
|
7906
|
-
if (registeredName && !listenTools.includes(name) && (isGroupMode() || isManagedMode())) {
|
|
7907
|
-
try {
|
|
7908
|
-
const pending = getUnconsumedMessages(registeredName);
|
|
7909
|
-
if (pending.length > 0 && !result.you_have_messages) {
|
|
7910
|
-
// Build rich nudge: WHO sent, WHETHER addressed, WHAT preview
|
|
7911
|
-
const senders = {};
|
|
7912
|
-
let addressedCount = 0;
|
|
7913
|
-
for (const m of pending) {
|
|
7914
|
-
senders[m.from] = (senders[m.from] || 0) + 1;
|
|
7915
|
-
if (m.addressed_to && m.addressed_to.includes(registeredName)) addressedCount++;
|
|
7916
|
-
}
|
|
7917
|
-
const senderSummary = Object.entries(senders).map(([n, c]) => `${c} from ${n}`).join(', ');
|
|
7918
|
-
const latest = pending[pending.length - 1];
|
|
7919
|
-
const preview = latest.content.substring(0, 80).replace(/\n/g, ' ');
|
|
7920
|
-
|
|
7921
|
-
result._pending_messages = pending.length;
|
|
7922
|
-
result._senders = senders;
|
|
7923
|
-
result._addressed_to_you = addressedCount;
|
|
7924
|
-
result._preview = `${latest.from}: "${preview}..."`;
|
|
7925
|
-
|
|
7926
|
-
// Escalate urgency based on oldest pending message age
|
|
7927
|
-
const oldestAge = pending.reduce((max, m) => {
|
|
7928
|
-
const age = Date.now() - new Date(m.timestamp).getTime();
|
|
7929
|
-
return age > max ? age : max;
|
|
7930
|
-
}, 0);
|
|
7931
|
-
const ageSec = Math.round(oldestAge / 1000);
|
|
7932
|
-
const addressedHint = addressedCount > 0 ? ` (${addressedCount} addressed to you)` : '';
|
|
7933
|
-
if (ageSec > 120) {
|
|
7934
|
-
result._nudge = `CRITICAL: ${pending.length} messages waiting ${Math.round(ageSec / 60)}+ min${addressedHint}: ${senderSummary}. Latest: "${preview}...". Call listen_group() NOW.`;
|
|
7935
|
-
} else if (ageSec > 30) {
|
|
7936
|
-
result._nudge = `URGENT: ${pending.length} messages waiting ${ageSec}s${addressedHint}: ${senderSummary}. Latest: "${preview}...". Call listen_group() soon.`;
|
|
7937
|
-
} else {
|
|
7938
|
-
result._nudge = `${pending.length} messages waiting${addressedHint}: ${senderSummary}. Latest: "${preview}...". Call listen_group().`;
|
|
7939
|
-
}
|
|
7940
|
-
}
|
|
7941
|
-
} catch (e) { log.debug("nudge detection failed:", e.message); }
|
|
7942
|
-
}
|
|
7943
|
-
|
|
7944
7841
|
// Global hook: reputation tracking
|
|
7842
|
+
const listenTools = ['listen', 'wait_for_reply'];
|
|
7945
7843
|
if (registeredName && result.success) {
|
|
7946
7844
|
try {
|
|
7947
7845
|
const repMap = {
|
|
@@ -7952,9 +7850,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
7952
7850
|
'submit_review': 'review_submit',
|
|
7953
7851
|
};
|
|
7954
7852
|
if (repMap[name]) trackReputation(registeredName, repMap[name]);
|
|
7955
|
-
// Track task completion specifically
|
|
7956
7853
|
if (name === 'update_task' && args?.status === 'done') {
|
|
7957
|
-
// Calculate task completion time
|
|
7958
7854
|
const tasks = getTasks();
|
|
7959
7855
|
const doneTask = tasks.find(t => t.id === args.task_id);
|
|
7960
7856
|
const taskTimeSec = doneTask ? Math.round((Date.now() - new Date(doneTask.created_at).getTime()) / 1000) : 0;
|
|
@@ -7963,48 +7859,61 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
7963
7859
|
} catch (e) { log.debug('reputation tracking failed:', e.message); }
|
|
7964
7860
|
}
|
|
7965
7861
|
|
|
7966
|
-
// Global hook: auto-compress conversation periodically
|
|
7967
7862
|
if (name === 'send_message' || name === 'broadcast') {
|
|
7968
7863
|
try { autoCompress(); } catch (e) { log.debug('auto-compress failed:', e.message); }
|
|
7969
7864
|
}
|
|
7970
7865
|
|
|
7971
|
-
//
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
const prof = getProfiles()[registeredName];
|
|
7975
|
-
const role = prof && prof.role ? prof.role.toLowerCase() : '';
|
|
7976
|
-
if (role === 'lead' || role === 'manager' || role === 'coordinator') {
|
|
7977
|
-
const coordMode = getConfig().coordinator_mode || 'responsive';
|
|
7978
|
-
result.coordinator_mode = coordMode;
|
|
7979
|
-
result.coordinator_hint = coordMode === 'responsive'
|
|
7980
|
-
? 'MODE: Stay with me — do NOT call listen(). Use consume_messages/workflow_status between human interactions.'
|
|
7981
|
-
: 'MODE: Run autonomously — use listen() to wait for agent results.';
|
|
7982
|
-
}
|
|
7983
|
-
} catch (e) { log.debug('coordinator mode hint failed:', e.message); }
|
|
7984
|
-
}
|
|
7985
|
-
|
|
7986
|
-
// Unread message hint: check if agent has pending messages on every tool call
|
|
7987
|
-
// This ensures agents see messages even when they forget to call listen()
|
|
7866
|
+
// Unified next_action: the ONE field that tells the agent what to do next.
|
|
7867
|
+
// Tool-specific next_action (set by the handler) takes priority.
|
|
7868
|
+
// Middleware fills in a default if the tool didn't set one.
|
|
7988
7869
|
if (registeredName && typeof result === 'object' && result !== null && !listenTools.includes(name)) {
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
const
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
}
|
|
7997
|
-
|
|
7998
|
-
|
|
7870
|
+
const isResponsiveCoordinator = (() => {
|
|
7871
|
+
try {
|
|
7872
|
+
const prof = getProfiles()[registeredName];
|
|
7873
|
+
const role = prof && prof.role ? prof.role.toLowerCase() : '';
|
|
7874
|
+
if (role === 'lead' || role === 'manager' || role === 'coordinator') {
|
|
7875
|
+
return (getConfig().coordinator_mode || 'responsive') === 'responsive';
|
|
7876
|
+
}
|
|
7877
|
+
} catch {}
|
|
7878
|
+
return false;
|
|
7879
|
+
})();
|
|
7999
7880
|
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
7881
|
+
if (isResponsiveCoordinator) {
|
|
7882
|
+
// Responsive coordinators must NEVER be told to call listen().
|
|
7883
|
+
// Replace any tool-set listen() directive with consume_messages() or nothing.
|
|
7884
|
+
if (!result.next_action || /\blisten\(\)/i.test(result.next_action)) {
|
|
7885
|
+
try {
|
|
7886
|
+
const pending = getUnconsumedMessages(registeredName);
|
|
7887
|
+
if (pending.length > 0) {
|
|
7888
|
+
result.next_action = `${pending.length} agent update(s) waiting. Call consume_messages() to read them.`;
|
|
7889
|
+
} else {
|
|
7890
|
+
delete result.next_action;
|
|
7891
|
+
}
|
|
7892
|
+
} catch {
|
|
7893
|
+
delete result.next_action;
|
|
7894
|
+
}
|
|
7895
|
+
}
|
|
8006
7896
|
} else {
|
|
8007
|
-
result.
|
|
7897
|
+
if (!result.next_action) {
|
|
7898
|
+
try {
|
|
7899
|
+
const pending = getUnconsumedMessages(registeredName);
|
|
7900
|
+
if (pending.length > 0) {
|
|
7901
|
+
const oldest = pending[0];
|
|
7902
|
+
const ageSec = Math.floor((Date.now() - new Date(oldest.timestamp).getTime()) / 1000);
|
|
7903
|
+
if (ageSec > 120) {
|
|
7904
|
+
result.next_action = `URGENT: ${pending.length} message(s) waiting ${Math.round(ageSec / 60)}+ min. Call listen() now.`;
|
|
7905
|
+
} else {
|
|
7906
|
+
result.next_action = `${pending.length} unread message(s). Call listen().`;
|
|
7907
|
+
}
|
|
7908
|
+
} else {
|
|
7909
|
+
result.next_action = 'Call listen() to receive messages.';
|
|
7910
|
+
}
|
|
7911
|
+
} catch {}
|
|
7912
|
+
}
|
|
7913
|
+
|
|
7914
|
+
if (consecutiveNonListenCalls >= 3) {
|
|
7915
|
+
result.next_action = `WARNING: ${consecutiveNonListenCalls} calls without listen(). Tools BLOCKED at 5. Call listen() NOW.`;
|
|
7916
|
+
}
|
|
8008
7917
|
}
|
|
8009
7918
|
}
|
|
8010
7919
|
|
package/tools/governance.js
CHANGED
|
@@ -117,7 +117,7 @@ module.exports = function (ctx) {
|
|
|
117
117
|
|
|
118
118
|
broadcastSystemMessage(`[REVIEW] ${state.registeredName} requests review of "${review.file}": ${review.description || 'No description'}. Call submit_review("${review.id}", "approved"/"changes_requested", "your feedback") to review.`, state.registeredName);
|
|
119
119
|
touchActivity();
|
|
120
|
-
return { success: true, review_id: review.id, file: review.file,
|
|
120
|
+
return { success: true, review_id: review.id, file: review.file, next_action: 'Call listen() to wait for the review.' };
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
function toolSubmitReview(reviewId, status, feedback) {
|
|
@@ -191,7 +191,10 @@ module.exports = function (ctx) {
|
|
|
191
191
|
writeJsonFile(REVIEWS_FILE, reviews);
|
|
192
192
|
touchActivity();
|
|
193
193
|
|
|
194
|
-
const
|
|
194
|
+
const reviewNextAction = review.status === 'approved'
|
|
195
|
+
? 'Call listen() to continue.'
|
|
196
|
+
: 'Call listen() — the author will fix and resubmit.';
|
|
197
|
+
const result = { success: true, review_id: reviewId, status: review.status, next_action: reviewNextAction };
|
|
195
198
|
if (review.review_round) result.review_round = review.review_round;
|
|
196
199
|
if (review.auto_approved) result.auto_approved = true;
|
|
197
200
|
return result;
|
package/tools/knowledge.js
CHANGED
|
@@ -132,7 +132,7 @@ module.exports = function (ctx) {
|
|
|
132
132
|
total_messages: compressed.segments.reduce((s, seg) => s + seg.message_count, 0) + recent.length,
|
|
133
133
|
compressed_count: compressed.segments.reduce((s, seg) => s + seg.message_count, 0),
|
|
134
134
|
recent_count: recent.length,
|
|
135
|
-
|
|
135
|
+
next_action: 'Call listen() to receive messages.',
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -223,9 +223,9 @@ module.exports = function (ctx) {
|
|
|
223
223
|
progress,
|
|
224
224
|
your_tasks: myActiveTasks.map(t => ({ id: t.id, title: t.title, status: t.status })),
|
|
225
225
|
your_completed: myCompletedCount,
|
|
226
|
-
|
|
227
|
-
? `You have ${myActiveTasks.length} active task(s). Continue working.`
|
|
228
|
-
: '
|
|
226
|
+
next_action: myActiveTasks.length > 0
|
|
227
|
+
? `You have ${myActiveTasks.length} active task(s). Continue working, then call listen().`
|
|
228
|
+
: 'Call listen() to receive messages and start working.',
|
|
229
229
|
};
|
|
230
230
|
}
|
|
231
231
|
|
package/tools/messaging.js
CHANGED
|
@@ -56,7 +56,7 @@ module.exports = function (ctx) {
|
|
|
56
56
|
result.preview = `${latest.from}: "${latest.content.substring(0, 80).replace(/\n/g, ' ')}..."`;
|
|
57
57
|
const oldestAge = Math.round((Date.now() - new Date(unconsumed[0].timestamp).getTime()) / 1000);
|
|
58
58
|
result.urgency = oldestAge > 120 ? 'critical' : oldestAge > 30 ? 'urgent' : 'normal';
|
|
59
|
-
result.
|
|
59
|
+
result.next_action = 'Call listen() to receive and process these messages.';
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
return result;
|
package/tools/safety.js
CHANGED
|
@@ -34,7 +34,7 @@ module.exports = function (ctx) {
|
|
|
34
34
|
locks[normalized] = { agent: state.registeredName, since: new Date().toISOString() };
|
|
35
35
|
writeJsonFile(LOCKS_FILE, locks);
|
|
36
36
|
touchActivity();
|
|
37
|
-
return { success: true, file: normalized,
|
|
37
|
+
return { success: true, file: normalized, next_action: 'Edit the file, then call unlock_file() when done.' };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function toolUnlockFile(filePath) {
|
package/tools/tasks.js
CHANGED
|
@@ -81,7 +81,7 @@ module.exports = function (ctx) {
|
|
|
81
81
|
saveTasks(tasks);
|
|
82
82
|
touchActivity();
|
|
83
83
|
|
|
84
|
-
const result = { success: true, task_id: task.id, assignee: task.assignee };
|
|
84
|
+
const result = { success: true, task_id: task.id, assignee: task.assignee, next_action: 'Call listen() to receive updates.' };
|
|
85
85
|
if (taskChannel) result.channel = taskChannel;
|
|
86
86
|
return result;
|
|
87
87
|
}
|
|
@@ -160,6 +160,7 @@ module.exports = function (ctx) {
|
|
|
160
160
|
task_id: task.id,
|
|
161
161
|
status: 'in_review',
|
|
162
162
|
review_id: reviewId,
|
|
163
|
+
next_action: 'Call listen() to wait for the reviewer to approve.',
|
|
163
164
|
message: `Cannot mark done — a reviewer is online and no approval exists. Review ${reviewId} auto-created. Wait for approval, then try again.`,
|
|
164
165
|
};
|
|
165
166
|
}
|
|
@@ -291,7 +292,11 @@ module.exports = function (ctx) {
|
|
|
291
292
|
for (const n of notifications) { helpers.sendSystemMessage(n.agent, n.message); }
|
|
292
293
|
} catch (e) { /* hooks not available */ }
|
|
293
294
|
|
|
294
|
-
|
|
295
|
+
const nextAction = status === 'done' ? 'Send a summary of what you did via send_message(), then call listen().'
|
|
296
|
+
: status === 'in_progress' ? `Do the work on "${task.title}", then call update_task("${task.id}", "done") when finished.`
|
|
297
|
+
: status === 'blocked' ? 'Send a message explaining the blocker, then call listen().'
|
|
298
|
+
: 'Call listen() to receive updates.';
|
|
299
|
+
return { success: true, task_id: task.id, status: task.status, title: task.title, next_action: nextAction };
|
|
295
300
|
}
|
|
296
301
|
|
|
297
302
|
// --- List Tasks ---
|
package/tools/workflows.js
CHANGED
|
@@ -102,7 +102,7 @@ module.exports = function (ctx) {
|
|
|
102
102
|
autonomous,
|
|
103
103
|
parallel,
|
|
104
104
|
started_steps: startedSteps.map(s => ({ id: s.id, description: s.description, assignee: s.assignee })),
|
|
105
|
-
|
|
105
|
+
next_action: autonomous ? 'Call get_work() for your assignment.' : 'Call listen() to receive updates.',
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -179,6 +179,7 @@ module.exports = function (ctx) {
|
|
|
179
179
|
next_steps: nextSteps.length > 0 ? nextSteps.map(s => ({ id: s.id, description: s.description, assignee: s.assignee })) : null,
|
|
180
180
|
progress: `${doneCount}/${wf.steps.length} (${pct}%)`,
|
|
181
181
|
workflow_status: wf.status,
|
|
182
|
+
next_action: wf.autonomous ? 'Call get_work() for your next assignment.' : 'Call listen() to receive the next step.',
|
|
182
183
|
};
|
|
183
184
|
}
|
|
184
185
|
|