neohive 6.2.2 → 6.4.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/README.md +3 -0
- package/cli.js +277 -55
- package/dashboard.html +108 -10
- package/dashboard.js +33 -2
- package/package.json +1 -1
- package/server.js +455 -508
- package/tools/governance.js +33 -8
- package/tools/knowledge.js +4 -4
- package/tools/messaging.js +3 -1
- package/tools/safety.js +4 -4
- package/tools/tasks.js +12 -7
- package/tools/workflows.js +2 -1
package/server.js
CHANGED
|
@@ -82,7 +82,7 @@ const SERVER_CONFIG = {
|
|
|
82
82
|
// Polling / Heartbeat intervals (ms)
|
|
83
83
|
HEARTBEAT_INTERVAL_MS: 15000, // how often agents write heartbeat files
|
|
84
84
|
POLL_INTERVAL_MS: 2000, // message polling cycle
|
|
85
|
-
AUTONOMOUS_LISTEN_MS:
|
|
85
|
+
AUTONOMOUS_LISTEN_MS: 90000, // max listen timeout in autonomous mode
|
|
86
86
|
CODEX_LISTEN_MS: 90000, // max listen timeout for Codex agents
|
|
87
87
|
|
|
88
88
|
// Agent health thresholds (ms)
|
|
@@ -124,6 +124,7 @@ let currentBranch = 'main'; // which branch this agent is on
|
|
|
124
124
|
let lastSentAt = 0; // timestamp of last sent message (for group cooldown)
|
|
125
125
|
let sendsSinceLastListen = 0; // enforced: must listen between sends in group mode
|
|
126
126
|
let consecutiveNonListenCalls = 0; // escalating listen() enforcement counter
|
|
127
|
+
let pendingUserReply = false; // true when __user__ message received but not yet replied to
|
|
127
128
|
let _isCurrentlyListening = false; // true when agent is in a listen() call
|
|
128
129
|
let sendLimit = 1; // default: 1 send per listen cycle (2 if addressed)
|
|
129
130
|
let unaddressedSends = 0; // response budget: unaddressed sends counter
|
|
@@ -584,14 +585,32 @@ function buildMessageResponse(msg, consumedIds) {
|
|
|
584
585
|
}
|
|
585
586
|
} catch (e) { log.debug('task reminder in listen failed:', e.message); }
|
|
586
587
|
|
|
587
|
-
// Append report-back protocol reminder to all non-system messages
|
|
588
588
|
const isSystemMsg = msg.from === '__system__' || msg.system === true;
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
589
|
+
if (msg.from === '__user__') pendingUserReply = true;
|
|
590
|
+
|
|
591
|
+
// Generate a specific next_action for review requests so reviewers know to read the file
|
|
592
|
+
let nextAction;
|
|
593
|
+
if (isSystemMsg && msg.content) {
|
|
594
|
+
const reviewMatch = msg.content.match(/submit_review\("(rev_[a-z0-9]+)"/);
|
|
595
|
+
const fileMatch = msg.content.match(/read(?:ing)?(?: the)? (?:file )?"([^"]+)"/i) ||
|
|
596
|
+
msg.content.match(/review of "([^"]+)"/i);
|
|
597
|
+
if (reviewMatch) {
|
|
598
|
+
const reviewId = reviewMatch[1];
|
|
599
|
+
const filePath = fileMatch ? fileMatch[1] : null;
|
|
600
|
+
nextAction = filePath
|
|
601
|
+
? `REVIEW REQUIRED: Read "${filePath}" first, then call submit_review("${reviewId}", "approved"/"changes_requested", "<your findings — min 50 chars>"). Do NOT submit without reading the file.`
|
|
602
|
+
: `REVIEW REQUIRED: Read the relevant files for this review, then call submit_review("${reviewId}", "approved"/"changes_requested", "<your findings — min 50 chars>"). Feedback is required.`;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (!nextAction) {
|
|
606
|
+
nextAction = isSystemMsg
|
|
607
|
+
? 'Process this message, then call listen().'
|
|
608
|
+
: `Do what this message asks. When finished, send_message(to="${msg.from}") with what you did and files changed, then call listen().`;
|
|
609
|
+
}
|
|
592
610
|
|
|
593
611
|
return {
|
|
594
612
|
success: true,
|
|
613
|
+
next_action: nextAction,
|
|
595
614
|
message: {
|
|
596
615
|
id: msg.id,
|
|
597
616
|
from: msg.from,
|
|
@@ -600,11 +619,9 @@ function buildMessageResponse(msg, consumedIds) {
|
|
|
600
619
|
priority: classifyPriority(msg),
|
|
601
620
|
...(msg.reply_to && { reply_to: msg.reply_to }),
|
|
602
621
|
...(msg.thread_id && { thread_id: msg.thread_id }),
|
|
603
|
-
...(reportBackReminder && { _protocol: reportBackReminder }),
|
|
604
622
|
},
|
|
605
623
|
pending_count: pendingCount,
|
|
606
624
|
agents_online: agentsOnline,
|
|
607
|
-
coordinator_mode: getConfig().coordinator_mode || 'responsive',
|
|
608
625
|
...(taskReminder && { task_reminder: taskReminder }),
|
|
609
626
|
};
|
|
610
627
|
}
|
|
@@ -1159,57 +1176,39 @@ function buildGuide(level = 'standard') {
|
|
|
1159
1176
|
const managed = getManagedConfig();
|
|
1160
1177
|
const isManager = managed.manager === registeredName;
|
|
1161
1178
|
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.');
|
|
1179
|
+
rules.push('ROLE: You are the Manager. You assign work and control who speaks.');
|
|
1180
|
+
rules.push('LOOP: create_task/create_workflow → yield_floor(agent) → listen() → process results → repeat. Never stop.');
|
|
1181
|
+
rules.push('Use update_task(id, status="done") and advance_workflow() as agents finish.');
|
|
1182
|
+
rules.push('Use set_phase() to move through discussion → planning → execution → review.');
|
|
1168
1183
|
} 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.');
|
|
1184
|
+
rules.push('ROLE: Managed agent. The manager controls your turn.');
|
|
1185
|
+
rules.push('LOOP: listen() → receive work → update_task(id, "in_progress") → do work → update_task(id, "done") → send_message(manager, summary) → listen(). Never stop.');
|
|
1186
|
+
rules.push('Never call get_work() or check_messages() in managed mode.');
|
|
1173
1187
|
}
|
|
1174
|
-
rules.push('Keep messages
|
|
1175
|
-
rules.push('When you finish work, report what you did and what files you changed.');
|
|
1188
|
+
rules.push('Keep messages short (2-3 paragraphs). Report what you did and what files changed.');
|
|
1176
1189
|
}
|
|
1177
1190
|
// === AUTONOMOUS MODE: completely different guide ===
|
|
1178
1191
|
else if (autonomousActive) {
|
|
1179
1192
|
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.');
|
|
1193
|
+
rules.push('ROLE: Advisor. You do NOT write code. You observe the team and suggest improvements.');
|
|
1194
|
+
rules.push('LOOP: get_work() → analyze messages/tasks/decisions → send_message(agent, insight) → get_work(). Never stop.');
|
|
1195
|
+
rules.push('Focus on: missed patterns, better approaches, edge cases, architectural issues.');
|
|
1186
1196
|
} 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.');
|
|
1197
|
+
rules.push('ROLE: System Monitor. You do NOT write code. You keep the team functioning.');
|
|
1198
|
+
rules.push('LOOP: get_work() → analyze health report → intervene (reassign stuck tasks, nudge idle agents) → get_work(). Never stop.');
|
|
1199
|
+
rules.push('Watch for: idle agents (>2min), circular escalations, queue buildup, stuck workflows (>15min).');
|
|
1193
1200
|
} 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.');
|
|
1201
|
+
rules.push('ROLE: Quality Lead. You review ALL team work. Never approve without reading the actual code.');
|
|
1202
|
+
rules.push('LOOP: get_work() → review code → submit_review()/verify_and_advance() or submit_review(status="changes_requested", feedback) → get_work(). Never stop.');
|
|
1198
1203
|
} else {
|
|
1199
|
-
rules.push('
|
|
1204
|
+
rules.push('LOOP: get_work() → do the work → verify_and_advance() → get_work(). Never stop. Never wait for approval.');
|
|
1200
1205
|
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.');
|
|
1206
|
+
? 'After completing work, report to ' + qualityLeadName + ' (Quality Lead) via send_message, then get_work().'
|
|
1207
|
+
: 'After completing work, call get_work() for your next task.');
|
|
1208
|
+
}
|
|
1209
|
+
rules.push('If stuck: retry up to 3 times, then ask team via send_message, then move to next task.');
|
|
1210
|
+
rules.push('Lock files before editing (lock_file/unlock_file). Log decisions with log_decision().');
|
|
1211
|
+
rules.push('Keep messages short (2-3 paragraphs). Report what you did and what files changed.');
|
|
1213
1212
|
|
|
1214
1213
|
// User-customizable project-specific rules
|
|
1215
1214
|
const guideFile = path.join(DATA_DIR, 'guide.md');
|
|
@@ -1239,101 +1238,66 @@ function buildGuide(level = 'standard') {
|
|
|
1239
1238
|
}
|
|
1240
1239
|
}
|
|
1241
1240
|
|
|
1242
|
-
|
|
1241
|
+
const result = {
|
|
1243
1242
|
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.',
|
|
1243
|
+
first_steps: 'Call get_work() to get your next assignment.',
|
|
1253
1244
|
autonomous_mode: true,
|
|
1254
1245
|
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
1246
|
};
|
|
1247
|
+
if (projectRules.length > 0) result.project_rules = projectRules;
|
|
1248
|
+
return result;
|
|
1265
1249
|
}
|
|
1266
1250
|
|
|
1267
1251
|
// === STANDARD MODE (non-autonomous) ===
|
|
1268
|
-
// Self-continuation rules apply in standard mode too (for 2+ agent teams)
|
|
1269
1252
|
if (aliveCount >= 2 && (mode === 'group' || mode === 'managed')) {
|
|
1270
1253
|
if (isQualityLead) {
|
|
1271
|
-
rules.push('
|
|
1254
|
+
rules.push('ROLE: Quality Lead. Review all team work via submit_review(). You are the approval gate.');
|
|
1272
1255
|
} else if (qualityLeadName) {
|
|
1273
|
-
rules.push('
|
|
1256
|
+
rules.push('After completing work, report to ' + qualityLeadName + ' (Quality Lead), then continue.');
|
|
1274
1257
|
}
|
|
1275
1258
|
}
|
|
1276
1259
|
|
|
1277
|
-
|
|
1278
|
-
if (isLeadRole && aliveCount >= 2) {
|
|
1260
|
+
if (isLeadRole) {
|
|
1279
1261
|
const coordinatorMode = getConfig().coordinator_mode || 'responsive';
|
|
1280
1262
|
if (coordinatorMode === 'responsive') {
|
|
1281
|
-
rules.push('
|
|
1263
|
+
rules.push('COORDINATOR: Use consume_messages() to check updates non-blockingly. Do NOT block in listen() — stay responsive to the user.');
|
|
1282
1264
|
} else {
|
|
1283
|
-
rules.push('
|
|
1265
|
+
rules.push('COORDINATOR: Use listen() to wait for agent results. Only return to human when all tasks are done or blocked.');
|
|
1284
1266
|
}
|
|
1285
|
-
rules.push('
|
|
1267
|
+
rules.push('Coordinators do NOT edit files or write code. Delegate ALL code work to other agents.');
|
|
1286
1268
|
}
|
|
1287
1269
|
|
|
1288
|
-
// Tier 0 — THE one rule (always included at every level)
|
|
1289
1270
|
const listenCmd = isManagedMode() ? 'listen()' : (mode === 'group' ? 'listen_group()' : 'listen()');
|
|
1290
|
-
|
|
1271
|
+
if (!isLeadRole) {
|
|
1272
|
+
rules.push(`After EVERY action, call ${listenCmd}. Never use sleep() or poll with check_messages().`);
|
|
1273
|
+
}
|
|
1291
1274
|
|
|
1292
|
-
// Minimal level: Tier 0 only — for experienced agents refreshing rules
|
|
1293
1275
|
if (level === 'minimal') {
|
|
1294
|
-
rules.push('
|
|
1295
|
-
rules.push('Lock files before editing shared code (lock_file / unlock_file).');
|
|
1276
|
+
rules.push('Lock files before editing (lock_file/unlock_file).');
|
|
1296
1277
|
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.');
|
|
1278
|
+
rules.push('Use reply_to for threading. Ignore messages with should_respond: false unless relevant.');
|
|
1299
1279
|
}
|
|
1300
1280
|
return {
|
|
1301
1281
|
rules,
|
|
1302
|
-
tier_info: `${rules.length} rules (minimal level, ${aliveCount} agents)`,
|
|
1303
1282
|
first_steps: mode === 'direct'
|
|
1304
|
-
? '
|
|
1283
|
+
? 'Call list_agents() to see who is online, then send a message or listen().'
|
|
1305
1284
|
: mode === 'managed'
|
|
1306
|
-
?
|
|
1307
|
-
:
|
|
1285
|
+
? 'Call get_briefing(), then listen() to wait for the manager.'
|
|
1286
|
+
: 'Call get_briefing(), then listen() to join.',
|
|
1308
1287
|
};
|
|
1309
1288
|
}
|
|
1310
1289
|
|
|
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.');
|
|
1290
|
+
rules.push('Keep messages short (2-3 paragraphs). Report what you did and what files changed.');
|
|
1291
|
+
rules.push('Lock files before editing (lock_file/unlock_file). Log decisions with log_decision().');
|
|
1318
1292
|
|
|
1319
|
-
// Tier 2 — group mode features (shown when group or managed mode)
|
|
1320
1293
|
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.');
|
|
1294
|
+
rules.push('Use reply_to for threading (faster cooldown). Ignore should_respond: false unless relevant.');
|
|
1324
1295
|
}
|
|
1325
|
-
|
|
1326
|
-
// Tier 2b — channels (shown when channels exist beyond #general)
|
|
1327
1296
|
if (hasChannels) {
|
|
1328
|
-
rules.push('
|
|
1329
|
-
rules.push('Use channel parameter on send_message to keep discussions focused.');
|
|
1297
|
+
rules.push('Use join_channel() and channel param on send_message to keep discussions focused.');
|
|
1330
1298
|
}
|
|
1331
|
-
|
|
1332
|
-
// Tier 3 — large teams (shown when 5+ agents)
|
|
1333
1299
|
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.');
|
|
1300
|
+
rules.push('Use task channels (#task-xxx) for focused discussion instead of #general.');
|
|
1337
1301
|
}
|
|
1338
1302
|
|
|
1339
1303
|
// User-customizable project-specific rules from .neohive/guide.md
|
|
@@ -1364,51 +1328,25 @@ function buildGuide(level = 'standard') {
|
|
|
1364
1328
|
}
|
|
1365
1329
|
}
|
|
1366
1330
|
|
|
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
|
|
1331
|
+
// Task reminder
|
|
1402
1332
|
if (registeredName) {
|
|
1403
1333
|
try {
|
|
1404
1334
|
const myTasks = getTasks().filter(t => t.assignee === registeredName && (t.status === 'pending' || t.status === 'in_progress'));
|
|
1405
1335
|
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('; ')}`);
|
|
1336
|
+
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
1337
|
}
|
|
1409
1338
|
} catch (e) { log.debug('task reminder in guide failed:', e.message); }
|
|
1410
1339
|
}
|
|
1411
1340
|
|
|
1341
|
+
const result = {
|
|
1342
|
+
rules,
|
|
1343
|
+
first_steps: mode === 'direct'
|
|
1344
|
+
? 'Call list_agents() to see who is online, then send a message or listen().'
|
|
1345
|
+
: 'Call get_briefing(), then listen() to join.',
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
if (projectRules.length > 0) result.project_rules = projectRules;
|
|
1349
|
+
|
|
1412
1350
|
// Cache the result for subsequent calls with same params
|
|
1413
1351
|
_guideCache = { key: cacheKey, result };
|
|
1414
1352
|
return result;
|
|
@@ -1567,79 +1505,91 @@ function toolRegister(name, provider = null, skills = null) {
|
|
|
1567
1505
|
const config = getConfig();
|
|
1568
1506
|
const mode = config.conversation_mode || 'direct';
|
|
1569
1507
|
const otherAgents = Object.keys(getAgents()).filter(n => n !== name);
|
|
1508
|
+
const guide = buildGuide();
|
|
1570
1509
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1510
|
+
// Auto-assign roles when 2+ agents are online
|
|
1511
|
+
const aliveCount = Object.values(getAgents()).filter(a => isPidAlive(a.pid, a.last_activity)).length;
|
|
1512
|
+
let assignedRole = guide.your_role || undefined;
|
|
1513
|
+
if (aliveCount >= 2) {
|
|
1514
|
+
try {
|
|
1515
|
+
const roleAssignments = autoAssignRoles();
|
|
1516
|
+
if (roleAssignments && roleAssignments[name]) assignedRole = roleAssignments[name];
|
|
1517
|
+
} catch (e) { log.debug("role assignment failed:", e.message); }
|
|
1518
|
+
}
|
|
1578
1519
|
|
|
1579
|
-
// Recovery
|
|
1520
|
+
// --- Recovery detection ---
|
|
1580
1521
|
const myTasks = getTasks().filter(t => t.assignee === name && t.status !== 'done');
|
|
1581
1522
|
const myWorkspace = getWorkspace(name);
|
|
1582
|
-
// Scale fix: tail-read last 30 messages instead of entire history
|
|
1583
1523
|
const recentHistory = tailReadJsonl(getHistoryFile(currentBranch), 30);
|
|
1584
1524
|
const myRecentMsgs = recentHistory.filter(m => m.to === name || m.from === name).slice(-5);
|
|
1525
|
+
let isResuming = false;
|
|
1526
|
+
let resumeContext = null;
|
|
1585
1527
|
|
|
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
1528
|
const recoveryFile = path.join(DATA_DIR, `recovery-${name}.json`);
|
|
1596
1529
|
if (fs.existsSync(recoveryFile)) {
|
|
1597
1530
|
try {
|
|
1598
1531
|
const snapshot = JSON.parse(fs.readFileSync(recoveryFile, 'utf8'));
|
|
1599
1532
|
const snapshotAge = Date.now() - new Date(snapshot.died_at).getTime();
|
|
1600
1533
|
if (snapshotAge > 3600000) {
|
|
1601
|
-
// Stale snapshot (>1 hour) — discard
|
|
1602
1534
|
try { fs.unlinkSync(recoveryFile); } catch {}
|
|
1603
1535
|
} 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
|
|
1536
|
+
isResuming = true;
|
|
1537
|
+
resumeContext = {
|
|
1538
|
+
crashed_ago: Math.round(snapshotAge / 1000) + 's',
|
|
1539
|
+
was_graceful: !!snapshot.graceful,
|
|
1540
|
+
};
|
|
1541
|
+
if (snapshot.active_tasks && snapshot.active_tasks.length > 0) resumeContext.active_tasks = snapshot.active_tasks;
|
|
1542
|
+
if (snapshot.locked_files && snapshot.locked_files.length > 0) resumeContext.files_to_relock = snapshot.locked_files;
|
|
1543
|
+
if (snapshot.decisions_made && snapshot.decisions_made.length > 0) resumeContext.decisions_made = snapshot.decisions_made;
|
|
1544
|
+
if (snapshot.tasks_completed && snapshot.tasks_completed.length > 0) resumeContext.tasks_completed = snapshot.tasks_completed;
|
|
1624
1545
|
try { fs.unlinkSync(recoveryFile); } catch {}
|
|
1625
1546
|
}
|
|
1626
1547
|
} catch (e) { log.debug("recovery file parse failed:", e.message); }
|
|
1627
1548
|
}
|
|
1628
1549
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1550
|
+
if (!isResuming && (myTasks.length > 0 || myRecentMsgs.length > 0)) {
|
|
1551
|
+
isResuming = true;
|
|
1552
|
+
resumeContext = {};
|
|
1553
|
+
if (myTasks.length > 0) resumeContext.active_tasks = myTasks.map(t => ({ id: t.id, title: t.title, status: t.status }));
|
|
1554
|
+
}
|
|
1631
1555
|
|
|
1632
|
-
//
|
|
1633
|
-
|
|
1634
|
-
if (
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1556
|
+
// --- Build next_action: the ONE thing the agent should do right now ---
|
|
1557
|
+
let nextAction;
|
|
1558
|
+
if (isResuming && resumeContext && resumeContext.active_tasks && resumeContext.active_tasks.length > 0) {
|
|
1559
|
+
nextAction = `You have ${resumeContext.active_tasks.length} unfinished task(s). Call list_tasks() to see them and resume work.`;
|
|
1560
|
+
} else if (guide.first_steps) {
|
|
1561
|
+
const firstStep = guide.first_steps.split(/\d+\.\s+/).filter(Boolean)[0];
|
|
1562
|
+
nextAction = firstStep ? firstStep.trim().replace(/\.$/, '') : guide.first_steps;
|
|
1563
|
+
} else {
|
|
1564
|
+
nextAction = 'Call get_briefing() to load project context';
|
|
1641
1565
|
}
|
|
1642
1566
|
|
|
1567
|
+
// Lead/coordinator gets role-specific next_action regardless of agent count
|
|
1568
|
+
const myRoleStr = (guide.your_role || '').toLowerCase();
|
|
1569
|
+
if (myRoleStr === 'lead' || myRoleStr === 'manager' || myRoleStr === 'coordinator') {
|
|
1570
|
+
const coordinatorMode = getConfig().coordinator_mode || 'responsive';
|
|
1571
|
+
nextAction = coordinatorMode === 'autonomous'
|
|
1572
|
+
? 'Call get_briefing() to load project context, then listen() to coordinate your team.'
|
|
1573
|
+
: 'Call get_briefing() to load project context, then consume_messages() to check for pending work.';
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// --- Build the result: next_action FIRST, then context ---
|
|
1577
|
+
const result = {
|
|
1578
|
+
success: true,
|
|
1579
|
+
registered: name,
|
|
1580
|
+
next_action: nextAction,
|
|
1581
|
+
mode,
|
|
1582
|
+
agents_online: otherAgents,
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
if (assignedRole) result.your_role = assignedRole;
|
|
1586
|
+
if (isResuming) result.resuming = resumeContext;
|
|
1587
|
+
|
|
1588
|
+
result.guide = guide;
|
|
1589
|
+
|
|
1590
|
+
// Notify other agents
|
|
1591
|
+
fireEvent('agent_join', { agent: name });
|
|
1592
|
+
|
|
1643
1593
|
return result;
|
|
1644
1594
|
} finally {
|
|
1645
1595
|
unlockAgentsFile();
|
|
@@ -1659,9 +1609,8 @@ function setListening(isListening) {
|
|
|
1659
1609
|
if (!registeredName) return;
|
|
1660
1610
|
_isCurrentlyListening = !!isListening;
|
|
1661
1611
|
|
|
1662
|
-
// Track listen calls in heartbeat for auto-nudge system
|
|
1663
1612
|
if (isListening) {
|
|
1664
|
-
touchActivity(true);
|
|
1613
|
+
touchActivity(true);
|
|
1665
1614
|
}
|
|
1666
1615
|
|
|
1667
1616
|
try {
|
|
@@ -2006,6 +1955,9 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
|
|
|
2006
1955
|
sendsSinceLastListen++;
|
|
2007
1956
|
if (isGroupMode() && !msg.addressed_to) { unaddressedSends++; }
|
|
2008
1957
|
|
|
1958
|
+
// Clear pending user reply flag when agent successfully replies to __user__
|
|
1959
|
+
if (to === '__user__') pendingUserReply = false;
|
|
1960
|
+
|
|
2009
1961
|
const result = { success: true, messageId: msg.id, from: msg.from, to: msg.to };
|
|
2010
1962
|
|
|
2011
1963
|
// Decision overlap hint: warn if message content overlaps with existing decisions
|
|
@@ -2044,19 +1996,6 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
|
|
|
2044
1996
|
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
1997
|
}
|
|
2046
1998
|
|
|
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
1999
|
// Coordinator enforcement: warn if sending work assignment without creating a task first
|
|
2061
2000
|
const senderProfile = getProfiles()[registeredName];
|
|
2062
2001
|
const senderRole = senderProfile && senderProfile.role ? senderProfile.role.toLowerCase() : '';
|
|
@@ -2067,7 +2006,7 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
|
|
|
2067
2006
|
const recentTasks = getTasks().filter(t => {
|
|
2068
2007
|
if (t.assignee !== to) return false;
|
|
2069
2008
|
const age = Date.now() - new Date(t.created_at).getTime();
|
|
2070
|
-
return age < 60000;
|
|
2009
|
+
return age < 60000;
|
|
2071
2010
|
});
|
|
2072
2011
|
if (recentTasks.length === 0) {
|
|
2073
2012
|
result.task_warning = `No task created for this assignment to ${to}. Use create_task(title, description, "${to}") to formally track this work.`;
|
|
@@ -2075,6 +2014,8 @@ async function toolSendMessage(content, to = null, reply_to = null, channel = nu
|
|
|
2075
2014
|
}
|
|
2076
2015
|
}
|
|
2077
2016
|
|
|
2017
|
+
result.next_action = 'Call listen() to receive replies.';
|
|
2018
|
+
|
|
2078
2019
|
return result;
|
|
2079
2020
|
}
|
|
2080
2021
|
|
|
@@ -2132,10 +2073,7 @@ function toolBroadcast(content) {
|
|
|
2132
2073
|
sendsSinceLastListen++;
|
|
2133
2074
|
unaddressedSends++; // broadcasts are always unaddressed
|
|
2134
2075
|
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.`; }
|
|
2076
|
+
const result = { success: true, messageId: msg.id, recipient_count: aliveOthers.length, next_action: 'Call listen() to receive replies.' };
|
|
2139
2077
|
return result;
|
|
2140
2078
|
}
|
|
2141
2079
|
|
|
@@ -2161,21 +2099,8 @@ function toolBroadcast(content) {
|
|
|
2161
2099
|
touchActivity();
|
|
2162
2100
|
lastSentAt = Date.now();
|
|
2163
2101
|
|
|
2164
|
-
const result = { success: true, sent_to: ids, recipient_count: ids.length };
|
|
2102
|
+
const result = { success: true, sent_to: ids, recipient_count: ids.length, next_action: 'Call listen() to receive replies.' };
|
|
2165
2103
|
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
2104
|
return result;
|
|
2180
2105
|
}
|
|
2181
2106
|
|
|
@@ -2242,104 +2167,11 @@ async function toolWaitForReply(timeoutSeconds = 300, from = null) {
|
|
|
2242
2167
|
};
|
|
2243
2168
|
}
|
|
2244
2169
|
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
const unconsumed = getUnconsumedMessages(registeredName, from);
|
|
2251
|
-
|
|
2252
|
-
// Rich summary: senders, addressed count, urgency — same as enhanced nudge
|
|
2253
|
-
const senders = {};
|
|
2254
|
-
let addressedCount = 0;
|
|
2255
|
-
for (const m of unconsumed) {
|
|
2256
|
-
senders[m.from] = (senders[m.from] || 0) + 1;
|
|
2257
|
-
if (m.addressed_to && m.addressed_to.includes(registeredName)) addressedCount++;
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
// Include pending notification count
|
|
2261
|
-
const allNotifs = getNotifications();
|
|
2262
|
-
const unreadNotifs = allNotifs.filter(n => !n.read_by.includes(registeredName));
|
|
2263
|
-
|
|
2264
|
-
const result = {
|
|
2265
|
-
count: unconsumed.length,
|
|
2266
|
-
pending_notifications: unreadNotifs.length,
|
|
2267
|
-
// Scale fix: return previews not full content — agent gets full content via listen_group()
|
|
2268
|
-
messages: unconsumed.map(m => ({
|
|
2269
|
-
id: m.id,
|
|
2270
|
-
from: m.from,
|
|
2271
|
-
preview: m.content.substring(0, 120),
|
|
2272
|
-
timestamp: m.timestamp,
|
|
2273
|
-
...(m.addressed_to && { addressed_to: m.addressed_to }),
|
|
2274
|
-
})),
|
|
2275
|
-
};
|
|
2276
|
-
|
|
2277
|
-
if (unconsumed.length > 0) {
|
|
2278
|
-
result.senders = senders;
|
|
2279
|
-
result.addressed_to_you = addressedCount;
|
|
2280
|
-
const latest = unconsumed[unconsumed.length - 1];
|
|
2281
|
-
result.preview = `${latest.from}: "${latest.content.substring(0, 80).replace(/\n/g, ' ')}..."`;
|
|
2282
|
-
const oldestAge = Math.round((Date.now() - new Date(unconsumed[0].timestamp).getTime()) / 1000);
|
|
2283
|
-
result.urgency = oldestAge > 120 ? 'critical' : oldestAge > 30 ? 'urgent' : 'normal';
|
|
2284
|
-
result.action_required = 'You have unread messages. Call listen() to receive and process them. Do NOT call check_messages() again — it does not consume messages and you will see the same messages repeatedly.';
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
return result;
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
function toolConsumeMessages(from = null, limit = null) {
|
|
2291
|
-
if (!registeredName) {
|
|
2292
|
-
return { error: 'You must call register() first' };
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
let unconsumed = getUnconsumedMessages(registeredName, from);
|
|
2296
|
-
if (limit && limit > 0 && unconsumed.length > limit) {
|
|
2297
|
-
unconsumed = unconsumed.slice(0, limit);
|
|
2298
|
-
}
|
|
2170
|
+
// toolCheckMessages and toolConsumeMessages removed — dead code.
|
|
2171
|
+
// Routing goes through: case 'messages' → messaging.handlers['check_messages' | 'consume_messages']
|
|
2172
|
+
// Source of truth: agent-bridge/tools/messaging.js
|
|
2299
2173
|
|
|
2300
|
-
|
|
2301
|
-
return { success: true, count: 0, messages: [] };
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
// Mark all as consumed
|
|
2305
|
-
const consumed = getConsumedIds(registeredName);
|
|
2306
|
-
for (const msg of unconsumed) {
|
|
2307
|
-
consumed.add(msg.id);
|
|
2308
|
-
markAsRead(registeredName, msg.id);
|
|
2309
|
-
}
|
|
2310
|
-
saveConsumedIds(registeredName, consumed);
|
|
2311
|
-
|
|
2312
|
-
// Update read offset
|
|
2313
|
-
const msgFile = getMessagesFile(currentBranch);
|
|
2314
|
-
if (fs.existsSync(msgFile)) {
|
|
2315
|
-
lastReadOffset = fs.statSync(msgFile).size;
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2318
|
-
touchActivity();
|
|
2319
|
-
|
|
2320
|
-
// Count remaining unconsumed after this batch
|
|
2321
|
-
const remaining = getUnconsumedMessages(registeredName, null);
|
|
2322
|
-
|
|
2323
|
-
const agents = getAgents();
|
|
2324
|
-
const agentsOnline = Object.entries(agents).filter(([, info]) => isPidAlive(info.pid, info.last_activity)).length;
|
|
2325
|
-
|
|
2326
|
-
return {
|
|
2327
|
-
success: true,
|
|
2328
|
-
count: unconsumed.length,
|
|
2329
|
-
messages: unconsumed.map(m => ({
|
|
2330
|
-
id: m.id,
|
|
2331
|
-
from: m.from,
|
|
2332
|
-
content: m.content,
|
|
2333
|
-
timestamp: m.timestamp,
|
|
2334
|
-
...(m.reply_to && { reply_to: m.reply_to }),
|
|
2335
|
-
...(m.thread_id && { thread_id: m.thread_id }),
|
|
2336
|
-
...(m.addressed_to && { addressed_to: m.addressed_to }),
|
|
2337
|
-
})),
|
|
2338
|
-
remaining: remaining.length,
|
|
2339
|
-
agents_online: agentsOnline,
|
|
2340
|
-
coordinator_mode: getConfig().coordinator_mode || 'responsive',
|
|
2341
|
-
};
|
|
2342
|
-
}
|
|
2174
|
+
// toolConsumeMessages removed — dead code. See agent-bridge/tools/messaging.js
|
|
2343
2175
|
|
|
2344
2176
|
function toolAckMessage(messageId) {
|
|
2345
2177
|
if (!registeredName) {
|
|
@@ -2390,6 +2222,9 @@ async function toolListen(from = null, outcome = null, task_id = null, summary =
|
|
|
2390
2222
|
if (newStatus) toolUpdateTask(task_id, newStatus, summary || '');
|
|
2391
2223
|
}
|
|
2392
2224
|
|
|
2225
|
+
// Clear pending user reply flag — warning was shown, agent is now entering the listen loop
|
|
2226
|
+
pendingUserReply = false;
|
|
2227
|
+
|
|
2393
2228
|
// Auto-detect group/managed mode and delegate to toolListenGroup
|
|
2394
2229
|
// This prevents agents from calling the "wrong" listen function
|
|
2395
2230
|
if (isGroupMode() || isManagedMode()) {
|
|
@@ -2442,8 +2277,9 @@ async function toolListen(from = null, outcome = null, task_id = null, summary =
|
|
|
2442
2277
|
|
|
2443
2278
|
let watcher;
|
|
2444
2279
|
let fallbackInterval;
|
|
2280
|
+
let timer;
|
|
2281
|
+
let heartbeatTimer;
|
|
2445
2282
|
|
|
2446
|
-
// Helper: check for new messages
|
|
2447
2283
|
const checkMessages = () => {
|
|
2448
2284
|
const { messages: newMsgs, newOffset } = readNewMessages(lastReadOffset);
|
|
2449
2285
|
lastReadOffset = newOffset;
|
|
@@ -2463,34 +2299,47 @@ async function toolListen(from = null, outcome = null, task_id = null, summary =
|
|
|
2463
2299
|
return false;
|
|
2464
2300
|
};
|
|
2465
2301
|
|
|
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
|
-
}
|
|
2302
|
+
// Auto-restart: instead of returning retry:true and hoping the agent
|
|
2303
|
+
// calls listen() again, loop internally. The agent stays blocked here
|
|
2304
|
+
// until a real message arrives or the IDE/user interrupts the tool call.
|
|
2305
|
+
function setupWatcher() {
|
|
2306
|
+
try { if (watcher) watcher.close(); } catch {}
|
|
2307
|
+
if (fallbackInterval) clearInterval(fallbackInterval);
|
|
2308
|
+
clearTimeout(timer);
|
|
2309
|
+
clearTimeout(heartbeatTimer);
|
|
2484
2310
|
|
|
2485
|
-
|
|
2486
|
-
|
|
2311
|
+
try {
|
|
2312
|
+
const msgFile = getMessagesFile(currentBranch);
|
|
2313
|
+
watcher = fs.watch(msgFile, () => { checkMessages(); });
|
|
2314
|
+
watcher.on('error', () => {});
|
|
2315
|
+
} catch {
|
|
2316
|
+
let pollCount = 0;
|
|
2317
|
+
fallbackInterval = setInterval(() => {
|
|
2318
|
+
if (checkMessages()) { clearInterval(fallbackInterval); return; }
|
|
2319
|
+
pollCount++;
|
|
2320
|
+
if (pollCount === 10) {
|
|
2321
|
+
clearInterval(fallbackInterval);
|
|
2322
|
+
fallbackInterval = setInterval(() => {
|
|
2323
|
+
if (checkMessages()) clearInterval(fallbackInterval);
|
|
2324
|
+
}, 2000);
|
|
2325
|
+
}
|
|
2326
|
+
}, 500);
|
|
2327
|
+
}
|
|
2487
2328
|
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2329
|
+
heartbeatTimer = setInterval(() => { touchHeartbeat(registeredName); }, 15000);
|
|
2330
|
+
|
|
2331
|
+
const listenTimeoutMs = (getConfig().listen_poll_interval || 120) * 1000;
|
|
2332
|
+
timer = setTimeout(() => {
|
|
2333
|
+
touchActivity();
|
|
2334
|
+
autoCompact();
|
|
2335
|
+
if (checkMessages()) return;
|
|
2336
|
+
// Return cleanly so Claude sees retry:true rather than a client-side MCP timeout
|
|
2337
|
+
setListening(false);
|
|
2338
|
+
done({ retry: true, next_action: 'No messages. Call listen() again immediately.' });
|
|
2339
|
+
}, listenTimeoutMs);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
setupWatcher();
|
|
2494
2343
|
});
|
|
2495
2344
|
}
|
|
2496
2345
|
|
|
@@ -2596,7 +2445,7 @@ async function toolListenCodex(from = null, outcome = null, task_id = null, summ
|
|
|
2596
2445
|
|
|
2597
2446
|
const timer = setTimeout(() => {
|
|
2598
2447
|
setListening(false);
|
|
2599
|
-
done({ retry: true,
|
|
2448
|
+
done({ retry: true, next_action: 'Call listen() again.', message: 'No messages yet.' });
|
|
2600
2449
|
}, 45000);
|
|
2601
2450
|
});
|
|
2602
2451
|
}
|
|
@@ -2819,9 +2668,12 @@ async function toolListenGroup(outcome = null, task_id = null, summary = null) {
|
|
|
2819
2668
|
|
|
2820
2669
|
const consumed = getConsumedIds(registeredName);
|
|
2821
2670
|
|
|
2822
|
-
// Autonomous mode: cap listen at
|
|
2823
|
-
|
|
2824
|
-
const
|
|
2671
|
+
// Autonomous mode: cap listen at 90s — agents should use get_work() instead
|
|
2672
|
+
// Responsive mode (Stay with me) overrides autonomous timeout — always uses configured listen interval
|
|
2673
|
+
const coordinatorMode = getConfig().coordinator_mode || 'responsive';
|
|
2674
|
+
const autonomousTimeout = (coordinatorMode !== 'responsive' && isAutonomousMode()) ? SERVER_CONFIG.AUTONOMOUS_LISTEN_MS : null;
|
|
2675
|
+
const configuredListenMs = (getConfig().listen_poll_interval || 120) * 1000;
|
|
2676
|
+
const MAX_LISTEN_MS = configuredListenMs; // configurable via dashboard settings (default 2 min)
|
|
2825
2677
|
const listenStart = Date.now();
|
|
2826
2678
|
|
|
2827
2679
|
// Helper: collect unconsumed messages from all sources (general + channels)
|
|
@@ -2888,8 +2740,8 @@ async function toolListenGroup(outcome = null, task_id = null, summary = null) {
|
|
|
2888
2740
|
if (fallbackInterval) clearInterval(fallbackInterval);
|
|
2889
2741
|
if (batch && batch.length > 0) {
|
|
2890
2742
|
resolve(buildListenGroupResponse(batch, consumed, registeredName, listenStart));
|
|
2891
|
-
} else {
|
|
2892
|
-
//
|
|
2743
|
+
} else if (autonomousTimeout) {
|
|
2744
|
+
// Autonomous mode: return so agent goes back to get_work() loop
|
|
2893
2745
|
setListening(false);
|
|
2894
2746
|
sendsSinceLastListen = 0;
|
|
2895
2747
|
sendLimit = 2;
|
|
@@ -2897,74 +2749,94 @@ async function toolListenGroup(outcome = null, task_id = null, summary = null) {
|
|
|
2897
2749
|
resolve({
|
|
2898
2750
|
messages: [],
|
|
2899
2751
|
message_count: 0,
|
|
2900
|
-
|
|
2901
|
-
batch_summary:
|
|
2752
|
+
next_action: 'Call get_work() for your next assignment.',
|
|
2753
|
+
batch_summary: 'No new messages.',
|
|
2902
2754
|
});
|
|
2755
|
+
} else {
|
|
2756
|
+
// Standard/group mode: don't return — re-enter the wait loop internally.
|
|
2757
|
+
// This prevents the agent from breaking out of listen mode.
|
|
2758
|
+
resolved = false;
|
|
2759
|
+
touchHeartbeat(registeredName);
|
|
2760
|
+
autoCompact();
|
|
2761
|
+
const freshBatch = collectBatch();
|
|
2762
|
+
if (freshBatch.length > 0) {
|
|
2763
|
+
resolved = true;
|
|
2764
|
+
resolve(buildListenGroupResponse(freshBatch, consumed, registeredName, listenStart));
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
setupWatchers();
|
|
2903
2768
|
}
|
|
2904
2769
|
};
|
|
2905
2770
|
|
|
2906
2771
|
let watcher;
|
|
2907
2772
|
let channelWatchers = [];
|
|
2908
2773
|
let fallbackInterval;
|
|
2774
|
+
let timer;
|
|
2775
|
+
let heartbeatTimer;
|
|
2909
2776
|
|
|
2910
|
-
|
|
2911
|
-
//
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2777
|
+
function setupWatchers() {
|
|
2778
|
+
// Clean up previous watchers/timers if re-entering
|
|
2779
|
+
try { if (watcher) watcher.close(); } catch {}
|
|
2780
|
+
try { if (channelWatchers.length) channelWatchers.forEach(w => { try { w.close(); } catch {} }); } catch {}
|
|
2781
|
+
if (fallbackInterval) clearInterval(fallbackInterval);
|
|
2782
|
+
clearTimeout(timer);
|
|
2783
|
+
clearTimeout(heartbeatTimer);
|
|
2784
|
+
channelWatchers = [];
|
|
2918
2785
|
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2786
|
+
try {
|
|
2787
|
+
const msgFile = getMessagesFile(currentBranch);
|
|
2788
|
+
watcher = fs.watch(msgFile, () => {
|
|
2789
|
+
const batch = collectBatch();
|
|
2790
|
+
if (batch.length > 0) done(batch);
|
|
2791
|
+
});
|
|
2792
|
+
watcher.on('error', () => {});
|
|
2793
|
+
|
|
2794
|
+
const myChannels = getAgentChannels(registeredName);
|
|
2795
|
+
for (const ch of myChannels) {
|
|
2796
|
+
if (ch === 'general') continue;
|
|
2797
|
+
const chFile = getChannelMessagesFile(ch);
|
|
2798
|
+
if (fs.existsSync(chFile)) {
|
|
2799
|
+
try {
|
|
2800
|
+
const chWatcher = fs.watch(chFile, () => {
|
|
2801
|
+
const batch = collectBatch();
|
|
2802
|
+
if (batch.length > 0) done(batch);
|
|
2803
|
+
});
|
|
2804
|
+
chWatcher.on('error', () => {});
|
|
2805
|
+
channelWatchers.push(chWatcher);
|
|
2806
|
+
} catch (e) { log.debug("channel watcher setup failed:", e.message); }
|
|
2807
|
+
}
|
|
2933
2808
|
}
|
|
2809
|
+
} catch {
|
|
2810
|
+
let pollCount = 0;
|
|
2811
|
+
fallbackInterval = setInterval(() => {
|
|
2812
|
+
const batch = collectBatch();
|
|
2813
|
+
if (batch.length > 0) {
|
|
2814
|
+
clearInterval(fallbackInterval);
|
|
2815
|
+
done(batch);
|
|
2816
|
+
}
|
|
2817
|
+
pollCount++;
|
|
2818
|
+
if (pollCount === 10) {
|
|
2819
|
+
clearInterval(fallbackInterval);
|
|
2820
|
+
fallbackInterval = setInterval(() => {
|
|
2821
|
+
const batch = collectBatch();
|
|
2822
|
+
if (batch.length > 0) { clearInterval(fallbackInterval); done(batch); }
|
|
2823
|
+
}, 2000);
|
|
2824
|
+
}
|
|
2825
|
+
}, 500);
|
|
2934
2826
|
}
|
|
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
2827
|
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
}, 15000);
|
|
2828
|
+
heartbeatTimer = setInterval(() => {
|
|
2829
|
+
touchHeartbeat(registeredName);
|
|
2830
|
+
}, 15000);
|
|
2960
2831
|
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
: MAX_LISTEN_MS;
|
|
2832
|
+
const effectiveTimeout = autonomousTimeout
|
|
2833
|
+
? Math.min(autonomousTimeout, MAX_LISTEN_MS)
|
|
2834
|
+
: MAX_LISTEN_MS;
|
|
2965
2835
|
|
|
2966
|
-
|
|
2967
|
-
|
|
2836
|
+
timer = setTimeout(() => done([]), effectiveTimeout);
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
setupWatchers();
|
|
2968
2840
|
});
|
|
2969
2841
|
}
|
|
2970
2842
|
|
|
@@ -3142,10 +3014,19 @@ function buildListenGroupResponse(batch, consumed, agentName, listenStart) {
|
|
|
3142
3014
|
}
|
|
3143
3015
|
}
|
|
3144
3016
|
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3017
|
+
if (batch.some(m => m.from === '__user__')) {
|
|
3018
|
+
pendingUserReply = true;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
if (isAutonomousMode()) {
|
|
3022
|
+
result.next_action = 'Process these messages, then call get_work().';
|
|
3023
|
+
} else if (result.should_respond === false) {
|
|
3024
|
+
result.next_action = 'Read these messages but do NOT respond. Call listen() to wait for your turn.';
|
|
3025
|
+
} else if (result.instructions && result.instructions.includes('MANAGER')) {
|
|
3026
|
+
result.next_action = 'You are the manager. Decide who speaks next, then call listen().';
|
|
3027
|
+
} else {
|
|
3028
|
+
result.next_action = 'Process these messages. Reply via send_message(), then call listen() again.';
|
|
3029
|
+
}
|
|
3149
3030
|
|
|
3150
3031
|
// Task reminder: remind agent of their outstanding tasks
|
|
3151
3032
|
try {
|
|
@@ -3511,7 +3392,7 @@ function toolUpdateTask(taskId, status, notes = null) {
|
|
|
3511
3392
|
task.status = 'in_review';
|
|
3512
3393
|
task.updated_at = new Date().toISOString();
|
|
3513
3394
|
saveTasks(tasks);
|
|
3514
|
-
broadcastSystemMessage(`[REVIEW GATE] ${registeredName} tried to mark "${task.title}" done but no review exists. Auto-created review ${reviewId}.
|
|
3395
|
+
broadcastSystemMessage(`[REVIEW GATE] ${registeredName} tried to mark "${task.title}" done but no review exists. Auto-created review ${reviewId}. To review: (1) read the relevant files for "${task.title}", (2) call submit_review("${reviewId}", "approved"/"changes_requested", "<your findings — min 50 chars>"). Feedback is required.`, registeredName);
|
|
3515
3396
|
logViolation('review_gate_blocked', registeredName, `Task "${task.title}" (${task.id}) blocked — no approved review. Auto-created ${reviewId}.`);
|
|
3516
3397
|
touchActivity();
|
|
3517
3398
|
return {
|
|
@@ -4203,6 +4084,7 @@ async function toolGetWork(params = {}) {
|
|
|
4203
4084
|
if (myStep) {
|
|
4204
4085
|
const result = {
|
|
4205
4086
|
type: 'workflow_step', priority: 'assigned', step: myStep,
|
|
4087
|
+
next_action: 'Do this work now. When done, call verify_and_advance().',
|
|
4206
4088
|
instruction: `You have assigned work: "${myStep.description}" (Workflow: "${myStep.workflow_name}"). Do this NOW. When done, call verify_and_advance().`
|
|
4207
4089
|
};
|
|
4208
4090
|
// Attach relevant KB skills for this task
|
|
@@ -4228,6 +4110,7 @@ async function toolGetWork(params = {}) {
|
|
|
4228
4110
|
return {
|
|
4229
4111
|
type: 'messages', priority: 'respond',
|
|
4230
4112
|
messages: pending.slice(0, 10), total: pending.length,
|
|
4113
|
+
next_action: 'Process these messages, then call get_work() again.',
|
|
4231
4114
|
instruction: 'Process these messages first, then call get_work() again.'
|
|
4232
4115
|
};
|
|
4233
4116
|
}
|
|
@@ -4252,6 +4135,7 @@ async function toolGetWork(params = {}) {
|
|
|
4252
4135
|
if (claimed) {
|
|
4253
4136
|
const claimResult = {
|
|
4254
4137
|
type: 'claimed_task', priority: 'self_assigned', task: best,
|
|
4138
|
+
next_action: 'Start working on this task now. Call verify_and_advance() when done.',
|
|
4255
4139
|
instruction: `No one was working on "${best.title}". I've assigned it to you. Start working on it now.`
|
|
4256
4140
|
};
|
|
4257
4141
|
const taskSkills = searchKBForTask(best.title + ' ' + (best.description || ''));
|
|
@@ -4269,6 +4153,7 @@ async function toolGetWork(params = {}) {
|
|
|
4269
4153
|
if (helpReqs.length > 0) {
|
|
4270
4154
|
return {
|
|
4271
4155
|
type: 'help_teammate', priority: 'assist', request: helpReqs[0],
|
|
4156
|
+
next_action: `Help ${helpReqs[0].from || 'your teammate'}, then call get_work() again.`,
|
|
4272
4157
|
instruction: `${helpReqs[0].from || 'A teammate'} needs help: "${helpReqs[0].content.substring(0, 200)}". Assist them.`
|
|
4273
4158
|
};
|
|
4274
4159
|
}
|
|
@@ -4278,6 +4163,7 @@ async function toolGetWork(params = {}) {
|
|
|
4278
4163
|
if (reviews.length > 0) {
|
|
4279
4164
|
return {
|
|
4280
4165
|
type: 'review', priority: 'review', review: reviews[0],
|
|
4166
|
+
next_action: 'Review this work, then call submit_review().',
|
|
4281
4167
|
instruction: `Review request from ${reviews[0].requested_by}: "${reviews[0].file}". Review their work and submit_review().`
|
|
4282
4168
|
};
|
|
4283
4169
|
}
|
|
@@ -4287,6 +4173,7 @@ async function toolGetWork(params = {}) {
|
|
|
4287
4173
|
if (blocked.length > 0) {
|
|
4288
4174
|
return {
|
|
4289
4175
|
type: 'unblock', priority: 'unblock', task: blocked[0],
|
|
4176
|
+
next_action: 'Try to unblock this task, then call get_work() again.',
|
|
4290
4177
|
instruction: `"${blocked[0].title}" is blocked. See if you can help unblock it.`
|
|
4291
4178
|
};
|
|
4292
4179
|
}
|
|
@@ -4310,6 +4197,7 @@ async function toolGetWork(params = {}) {
|
|
|
4310
4197
|
return {
|
|
4311
4198
|
type: 'stolen_task', priority: 'work_steal', task: stealable.task,
|
|
4312
4199
|
from_agent: stealable.from_agent,
|
|
4200
|
+
next_action: 'Start working on this task now. Call verify_and_advance() when done.',
|
|
4313
4201
|
instruction: stealable.message + ' Start working on it now.',
|
|
4314
4202
|
};
|
|
4315
4203
|
}
|
|
@@ -4322,6 +4210,7 @@ async function toolGetWork(params = {}) {
|
|
|
4322
4210
|
return {
|
|
4323
4211
|
type: 'messages', priority: 'respond',
|
|
4324
4212
|
messages: newMsgs.slice(0, 10), total: newMsgs.length,
|
|
4213
|
+
next_action: 'Process these messages, then call get_work() again.',
|
|
4325
4214
|
instruction: 'New messages arrived. Process them, then call get_work() again.'
|
|
4326
4215
|
};
|
|
4327
4216
|
}
|
|
@@ -4331,6 +4220,7 @@ async function toolGetWork(params = {}) {
|
|
|
4331
4220
|
if (upcoming) {
|
|
4332
4221
|
return {
|
|
4333
4222
|
type: 'prep_work', priority: 'proactive', step: upcoming,
|
|
4223
|
+
next_action: 'Prepare for this upcoming step, then call get_work() again.',
|
|
4334
4224
|
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
4225
|
};
|
|
4336
4226
|
}
|
|
@@ -4338,11 +4228,14 @@ async function toolGetWork(params = {}) {
|
|
|
4338
4228
|
// 9. Truly idle — try role rebalancing before returning
|
|
4339
4229
|
rebalanceRoles(); // Item 5: check if workload requires role changes
|
|
4340
4230
|
touchActivity();
|
|
4231
|
+
const config = getConfig();
|
|
4232
|
+
const idleInterval = config.idle_poll_interval || 90;
|
|
4341
4233
|
const idleResult = {
|
|
4342
4234
|
type: 'idle',
|
|
4235
|
+
next_action: isManagedMode() ? 'Call listen() to wait for work.' : `Call get_work() again in ${idleInterval} seconds.`,
|
|
4343
4236
|
instruction: isManagedMode()
|
|
4344
4237
|
? 'No work available right now. Call listen() to wait for the manager to assign work or give you the floor.'
|
|
4345
|
-
:
|
|
4238
|
+
: `No work available right now. Call get_work() again in ${idleInterval} seconds.`
|
|
4346
4239
|
};
|
|
4347
4240
|
// Item 4: warn demoted agents
|
|
4348
4241
|
const agentRep = getReputation();
|
|
@@ -4399,7 +4292,7 @@ async function toolVerifyAndAdvance(params) {
|
|
|
4399
4292
|
const report = generateCompletionReport(wf);
|
|
4400
4293
|
const retrospective = logRetrospective(wf.id); // Item 9: analyze retry patterns
|
|
4401
4294
|
touchActivity();
|
|
4402
|
-
return { status: flagged ? 'workflow_complete_flagged' : 'workflow_complete', workflow_id: wf.id,
|
|
4295
|
+
return { status: flagged ? 'workflow_complete_flagged' : 'workflow_complete', workflow_id: wf.id, next_action: 'Call get_work() for your next assignment.', report, retrospective };
|
|
4403
4296
|
}
|
|
4404
4297
|
|
|
4405
4298
|
const agents = getAgents();
|
|
@@ -4421,7 +4314,7 @@ async function toolVerifyAndAdvance(params) {
|
|
|
4421
4314
|
status: flagged ? 'advanced_with_flag' : 'advanced', workflow_id: wf.id,
|
|
4422
4315
|
completed_step: currentStep.id,
|
|
4423
4316
|
next_steps: nextSteps.map(s => ({ id: s.id, description: s.description, assignee: s.assignee })),
|
|
4424
|
-
|
|
4317
|
+
next_action: 'Call get_work() for your next assignment.',
|
|
4425
4318
|
};
|
|
4426
4319
|
}
|
|
4427
4320
|
|
|
@@ -4452,7 +4345,7 @@ async function toolVerifyAndAdvance(params) {
|
|
|
4452
4345
|
touchActivity();
|
|
4453
4346
|
return {
|
|
4454
4347
|
status: 'needs_help', workflow_id: wf.id,
|
|
4455
|
-
|
|
4348
|
+
next_action: 'Call get_work() for other work while waiting for help.',
|
|
4456
4349
|
};
|
|
4457
4350
|
}
|
|
4458
4351
|
|
|
@@ -5881,6 +5774,59 @@ function toolListChannels() {
|
|
|
5881
5774
|
return { channels: result, your_channels: getAgentChannels(registeredName) };
|
|
5882
5775
|
}
|
|
5883
5776
|
|
|
5777
|
+
// --- Self-healing Watchdog: reclaim tasks from dead/stale agents ---
|
|
5778
|
+
// Specified in GEMINI.md: runs every 60s; scans in_progress tasks.
|
|
5779
|
+
function runSelfHealingWatchdog() {
|
|
5780
|
+
if (!registeredName) return;
|
|
5781
|
+
try {
|
|
5782
|
+
const tasks = getTasks();
|
|
5783
|
+
const agents = getAgents();
|
|
5784
|
+
let changed = false;
|
|
5785
|
+
const now = Date.now();
|
|
5786
|
+
const STALE_THRESHOLD_MS = 300000; // 5 minutes
|
|
5787
|
+
|
|
5788
|
+
for (const task of tasks) {
|
|
5789
|
+
if (task.status !== 'in_progress' || !task.assignee) continue;
|
|
5790
|
+
|
|
5791
|
+
const assignee = agents[task.assignee];
|
|
5792
|
+
let isStale = false;
|
|
5793
|
+
|
|
5794
|
+
if (!assignee) {
|
|
5795
|
+
isStale = true; // Assignee no longer in registry
|
|
5796
|
+
} else {
|
|
5797
|
+
const lastActivity = assignee.last_activity ? new Date(assignee.last_activity).getTime() : 0;
|
|
5798
|
+
const heartbeatStale = now - lastActivity > STALE_THRESHOLD_MS;
|
|
5799
|
+
const pidDead = !isPidAlive(assignee.pid, assignee.last_activity);
|
|
5800
|
+
|
|
5801
|
+
if (pidDead && heartbeatStale) {
|
|
5802
|
+
isStale = true;
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
|
|
5806
|
+
if (isStale) {
|
|
5807
|
+
const retryCount = (task.retry_count || 0) + 1;
|
|
5808
|
+
task.retry_count = retryCount;
|
|
5809
|
+
task.updated_at = new Date().toISOString();
|
|
5810
|
+
|
|
5811
|
+
if (retryCount >= 3) {
|
|
5812
|
+
task.status = 'blocked_permanent';
|
|
5813
|
+
task.blocked_reason = `Agent "${task.assignee}" failed 3 times (PID dead + heartbeat stale >5min). Coordinator intervention required.`;
|
|
5814
|
+
broadcastSystemMessage(`⛔ [WATCHDOG: POISON PILL] Task "${task.title}" marked as blocked_permanent after 3 failed attempts by ${task.assignee}. Coordinator intervention required.`, registeredName);
|
|
5815
|
+
} else {
|
|
5816
|
+
const oldAssignee = task.assignee;
|
|
5817
|
+
task.status = 'pending';
|
|
5818
|
+
task.assignee = null;
|
|
5819
|
+
changed = true;
|
|
5820
|
+
broadcastSystemMessage(`↺ [WATCHDOG: RECLAIMED] Task "${task.title}" reclaimed from stale agent "${oldAssignee}" (retry ${retryCount}/3). Reset to pending.`, registeredName);
|
|
5821
|
+
}
|
|
5822
|
+
changed = true;
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5826
|
+
if (changed) saveTasks(tasks);
|
|
5827
|
+
} catch (e) { log.warn("Self-healing watchdog failed:", e.message); }
|
|
5828
|
+
}
|
|
5829
|
+
|
|
5884
5830
|
// Auto-escalation: notify team about tasks blocked for >5 minutes
|
|
5885
5831
|
// Uses task.escalated_at field for cross-process dedup (file-based, not in-memory)
|
|
5886
5832
|
function escalateBlockedTasks() {
|
|
@@ -7648,7 +7594,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
7648
7594
|
// Escalating listen() enforcement — block tools after too many non-listen calls
|
|
7649
7595
|
// send_message is exempt so blocked agents can escalate to coordinator before calling listen()
|
|
7650
7596
|
// messages is exempt (unified query tool — replaces check_messages/consume_messages)
|
|
7651
|
-
|
|
7597
|
+
// lock_file and unlock_file are safety housekeeping, not comms — exempt from the listen counter
|
|
7598
|
+
const listenExemptTools = new Set(['register', 'get_briefing', 'get_guide', 'listen', 'wait_for_reply', 'update_profile', 'list_agents', 'add_rule', 'remove_rule', 'toggle_rule', 'list_rules', 'send_message', 'messages', 'lock_file', 'unlock_file']);
|
|
7652
7599
|
if (listenExemptTools.has(name)) {
|
|
7653
7600
|
if (name === 'listen' || name === 'wait_for_reply') {
|
|
7654
7601
|
consecutiveNonListenCalls = 0;
|
|
@@ -7670,7 +7617,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
7670
7617
|
|
|
7671
7618
|
if (!isCoordinatorExempt) {
|
|
7672
7619
|
consecutiveNonListenCalls++;
|
|
7673
|
-
if (consecutiveNonListenCalls >=
|
|
7620
|
+
if (consecutiveNonListenCalls >= 15) {
|
|
7674
7621
|
const coordinator = (() => {
|
|
7675
7622
|
try {
|
|
7676
7623
|
const profs = getProfiles();
|
|
@@ -7679,14 +7626,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
7679
7626
|
} catch { return 'your coordinator'; }
|
|
7680
7627
|
})();
|
|
7681
7628
|
return {
|
|
7682
|
-
content: [{ type: 'text', text:
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
}
|
|
7629
|
+
content: [{ type: 'text', text:
|
|
7630
|
+
`BLOCKED — ${name}() was not executed. You made ${consecutiveNonListenCalls} tool calls without calling listen(). ` +
|
|
7631
|
+
`Do these two steps IN ORDER:\n` +
|
|
7632
|
+
`1. send_message(to="${coordinator}", content="I was blocked after ${consecutiveNonListenCalls} calls without listen(). I need to call ${name}. Should I proceed?")\n` +
|
|
7633
|
+
`2. listen()\n` +
|
|
7634
|
+
`Do NOT skip step 1. Do NOT call any other tool. Start with send_message now.`
|
|
7635
|
+
}],
|
|
7689
7636
|
isError: true,
|
|
7637
|
+
next_action: `Call send_message(to="${coordinator}", content="I was blocked after ${consecutiveNonListenCalls} calls without listen(). I need to call ${name}. Should I proceed?") then immediately call listen().`,
|
|
7690
7638
|
};
|
|
7691
7639
|
}
|
|
7692
7640
|
}
|
|
@@ -7894,54 +7842,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
7894
7842
|
if (last3.length >= 3 && last3.every(c => c.tool === name && c.argsHash === argsHash)) {
|
|
7895
7843
|
result._stuck_hint = `You have called ${name} 3 times with the same error. Consider: broadcasting for help, trying a different approach, or calling suggest_task() to find other work.`;
|
|
7896
7844
|
}
|
|
7845
|
+
result.next_action = 'Fix the error above, then call listen() to continue.';
|
|
7897
7846
|
return {
|
|
7898
7847
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
7899
7848
|
isError: true,
|
|
7900
7849
|
};
|
|
7901
7850
|
}
|
|
7902
7851
|
|
|
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
7852
|
// Global hook: reputation tracking
|
|
7853
|
+
const listenTools = ['listen', 'wait_for_reply'];
|
|
7945
7854
|
if (registeredName && result.success) {
|
|
7946
7855
|
try {
|
|
7947
7856
|
const repMap = {
|
|
@@ -7952,9 +7861,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
7952
7861
|
'submit_review': 'review_submit',
|
|
7953
7862
|
};
|
|
7954
7863
|
if (repMap[name]) trackReputation(registeredName, repMap[name]);
|
|
7955
|
-
// Track task completion specifically
|
|
7956
7864
|
if (name === 'update_task' && args?.status === 'done') {
|
|
7957
|
-
// Calculate task completion time
|
|
7958
7865
|
const tasks = getTasks();
|
|
7959
7866
|
const doneTask = tasks.find(t => t.id === args.task_id);
|
|
7960
7867
|
const taskTimeSec = doneTask ? Math.round((Date.now() - new Date(doneTask.created_at).getTime()) / 1000) : 0;
|
|
@@ -7963,48 +7870,78 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
7963
7870
|
} catch (e) { log.debug('reputation tracking failed:', e.message); }
|
|
7964
7871
|
}
|
|
7965
7872
|
|
|
7966
|
-
// Global hook: auto-compress conversation periodically
|
|
7967
7873
|
if (name === 'send_message' || name === 'broadcast') {
|
|
7968
7874
|
try { autoCompress(); } catch (e) { log.debug('auto-compress failed:', e.message); }
|
|
7969
7875
|
}
|
|
7970
7876
|
|
|
7971
|
-
//
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
const
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7877
|
+
// Unified next_action: the ONE field that tells the agent what to do next.
|
|
7878
|
+
// Tool-specific next_action (set by the handler) takes priority.
|
|
7879
|
+
// Middleware fills in a default if the tool didn't set one.
|
|
7880
|
+
if (registeredName && typeof result === 'object' && result !== null && !listenTools.includes(name)) {
|
|
7881
|
+
const isResponsiveCoordinator = (() => {
|
|
7882
|
+
try {
|
|
7883
|
+
const prof = getProfiles()[registeredName];
|
|
7884
|
+
const role = prof && prof.role ? prof.role.toLowerCase() : '';
|
|
7885
|
+
if (role === 'lead' || role === 'manager' || role === 'coordinator') {
|
|
7886
|
+
return (getConfig().coordinator_mode || 'responsive') === 'responsive';
|
|
7887
|
+
}
|
|
7888
|
+
} catch {}
|
|
7889
|
+
return false;
|
|
7890
|
+
})();
|
|
7891
|
+
|
|
7892
|
+
if (isResponsiveCoordinator) {
|
|
7893
|
+
// Responsive coordinators must NEVER be told to call listen().
|
|
7894
|
+
// Three cases:
|
|
7895
|
+
// 1. No next_action set by tool → inject consume_messages hint if pending, else nothing
|
|
7896
|
+
// 2. Bare listen() directive → replace entirely with coordinator hint
|
|
7897
|
+
// 3. Compound "Do X, then listen()." → strip the listen() tail, keep the lead instruction
|
|
7898
|
+
const na = result.next_action || '';
|
|
7899
|
+
const bareListenRe = /^call listen\(\)/i;
|
|
7900
|
+
const tailListenRe = /,?\s*then call listen\(\)[^.]*\./i;
|
|
7901
|
+
try {
|
|
7902
|
+
const pending = getUnconsumedMessages(registeredName);
|
|
7903
|
+
const pendingHint = pending.length > 0
|
|
7904
|
+
? `${pending.length} agent update(s) waiting. Call consume_messages() to read them.`
|
|
7905
|
+
: null;
|
|
7906
|
+
if (!na || bareListenRe.test(na)) {
|
|
7907
|
+
// No guidance or bare listen() — replace with coordinator hint or nothing
|
|
7908
|
+
if (pendingHint) result.next_action = pendingHint;
|
|
7909
|
+
else delete result.next_action;
|
|
7910
|
+
} else if (tailListenRe.test(na)) {
|
|
7911
|
+
// Compound instruction ending in "then call listen()" — strip just the listen() tail
|
|
7912
|
+
const stripped = na.replace(tailListenRe, '.').replace(/\.\.$/, '.').trim();
|
|
7913
|
+
result.next_action = pendingHint ? `${stripped} Then: ${pendingHint}` : stripped;
|
|
7914
|
+
}
|
|
7915
|
+
// else: next_action has no listen() reference — preserve as-is
|
|
7916
|
+
} catch {
|
|
7917
|
+
if (bareListenRe.test(na)) delete result.next_action;
|
|
7918
|
+
}
|
|
7919
|
+
} else {
|
|
7920
|
+
if (!result.next_action) {
|
|
7921
|
+
try {
|
|
7922
|
+
const pending = getUnconsumedMessages(registeredName);
|
|
7923
|
+
if (pending.length > 0) {
|
|
7924
|
+
const oldest = pending[0];
|
|
7925
|
+
const ageSec = Math.floor((Date.now() - new Date(oldest.timestamp).getTime()) / 1000);
|
|
7926
|
+
if (ageSec > 120) {
|
|
7927
|
+
result.next_action = `URGENT: ${pending.length} message(s) waiting ${Math.round(ageSec / 60)}+ min. Call listen() now.`;
|
|
7928
|
+
} else {
|
|
7929
|
+
result.next_action = `${pending.length} unread message(s). Call listen().`;
|
|
7930
|
+
}
|
|
7931
|
+
} else {
|
|
7932
|
+
result.next_action = 'Call listen() to receive messages.';
|
|
7933
|
+
}
|
|
7934
|
+
} catch {}
|
|
7982
7935
|
}
|
|
7983
|
-
} catch (e) { log.debug('coordinator mode hint failed:', e.message); }
|
|
7984
|
-
}
|
|
7985
7936
|
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
if (registeredName && typeof result === 'object' && result !== null && !listenTools.includes(name)) {
|
|
7989
|
-
try {
|
|
7990
|
-
const unread = getUnconsumedMessages(registeredName);
|
|
7991
|
-
if (unread.length > 0) {
|
|
7992
|
-
const latest = unread[unread.length - 1];
|
|
7993
|
-
result.unread_messages = unread.length;
|
|
7994
|
-
result.unread_preview = `${latest.from}: "${latest.content.substring(0, 100).replace(/\n/g, ' ')}"`;
|
|
7995
|
-
result.unread_action = `You have ${unread.length} unread message(s). Call listen() to receive them.`;
|
|
7937
|
+
if (consecutiveNonListenCalls >= 10) {
|
|
7938
|
+
result.next_action = `WARNING: ${consecutiveNonListenCalls} calls without listen(). Tools BLOCKED at 15. Call listen() NOW.`;
|
|
7996
7939
|
}
|
|
7997
|
-
} catch (e) { log.debug('unread message hint failed:', e.message); }
|
|
7998
|
-
}
|
|
7999
7940
|
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
if (consecutiveNonListenCalls >= 3) {
|
|
8005
|
-
result._listen = `WARNING: You have NOT called listen() in ${consecutiveNonListenCalls} tool calls. Tools will be BLOCKED at 5. Call listen() NOW.`;
|
|
8006
|
-
} else {
|
|
8007
|
-
result._listen = 'After processing this result, call listen() to receive messages. Do NOT skip this.';
|
|
7941
|
+
// Soft-enforce user reply: remind agent they have an unanswered user message
|
|
7942
|
+
if (pendingUserReply && result.next_action && name !== 'send_message') {
|
|
7943
|
+
result.next_action += " NOTE: You have an unanswered user message — call send_message(to='__user__') before your next listen().";
|
|
7944
|
+
}
|
|
8008
7945
|
}
|
|
8009
7946
|
}
|
|
8010
7947
|
|
|
@@ -8126,9 +8063,19 @@ function autoReclaimDeadSeat() {
|
|
|
8126
8063
|
autoReclaimedName = true; // mark as auto-reclaimed so toolRegister() can override it
|
|
8127
8064
|
registeredToken = agents[bestName].token || '';
|
|
8128
8065
|
touchHeartbeat(bestName);
|
|
8129
|
-
// Start 10s heartbeat interval
|
|
8066
|
+
// Start 10s heartbeat interval; watchdog runs every 60s (6 ticks)
|
|
8130
8067
|
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
8131
|
-
|
|
8068
|
+
let watchdogTick = 0;
|
|
8069
|
+
heartbeatInterval = setInterval(() => {
|
|
8070
|
+
touchHeartbeat(registeredName);
|
|
8071
|
+
watchdogTick++;
|
|
8072
|
+
if (watchdogTick >= 6) {
|
|
8073
|
+
watchdogTick = 0;
|
|
8074
|
+
runSelfHealingWatchdog();
|
|
8075
|
+
escalateBlockedTasks();
|
|
8076
|
+
triggerStandupIfDue();
|
|
8077
|
+
}
|
|
8078
|
+
}, 10000);
|
|
8132
8079
|
heartbeatInterval.unref();
|
|
8133
8080
|
console.error(`[neohive] Auto-reclaimed seat "${bestName}" (previous PID dead)`);
|
|
8134
8081
|
} catch (e) {
|