@cgh567/agent 2.4.4 → 2.4.5
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.
|
@@ -10,5 +10,8 @@
|
|
|
10
10
|
source ~/.bashrc 2>/dev/null || true
|
|
11
11
|
source ~/.profile 2>/dev/null || true
|
|
12
12
|
|
|
13
|
+
# Canonical repo path — per AGENTS.md, always use Desktop path (not ~/helios-agent stale WSL copy)
|
|
14
|
+
HELIOS_AGENT_ROOT="/mnt/c/Users/chiko/Desktop/Helios/helios-agent-main"
|
|
15
|
+
|
|
13
16
|
# Delegate to bin/helios with all forwarded args
|
|
14
|
-
exec
|
|
17
|
+
exec "${HELIOS_AGENT_ROOT}/bin/helios" "$@"
|
package/daemon/helios-api.js
CHANGED
|
@@ -441,23 +441,24 @@ async function handleCreateTask(req, res, ctx) {
|
|
|
441
441
|
const resolvedPriority = Number.isFinite(Number(priority)) ? Number(priority) : 2;
|
|
442
442
|
|
|
443
443
|
try {
|
|
444
|
+
// HIGH-3: use MERGE when sourceId provided to prevent duplicate tasks on network-error retry
|
|
445
|
+
const _taskCypher = (sourceId && originKind)
|
|
446
|
+
? `MERGE (t:Task {sourceId: \, companyId: \, originKind: \})
|
|
447
|
+
ON CREATE SET
|
|
448
|
+
t.id = \, t.title = \, t.status = 'todo', t.priority = toInteger(\),
|
|
449
|
+
t.assigneeAgentId = \, t.body = \,
|
|
450
|
+
t.executionPolicyJson = \, t.executionStateJson = \,
|
|
451
|
+
t.sourceType = \, t.projectId = \, t.createdAt = datetime()
|
|
452
|
+
ON MATCH SET t.updatedAt = datetime()`
|
|
453
|
+
: `CREATE (t:Task {
|
|
454
|
+
id: \, companyId: \, title: \, status: 'todo',
|
|
455
|
+
priority: toInteger(\), assigneeAgentId: \, body: \,
|
|
456
|
+
executionPolicyJson: \, executionStateJson: \,
|
|
457
|
+
sourceType: \, sourceId: \, originKind: \,
|
|
458
|
+
projectId: \, createdAt: datetime()
|
|
459
|
+
})`;
|
|
444
460
|
await mgQuery(
|
|
445
|
-
|
|
446
|
-
id: $id,
|
|
447
|
-
companyId: $cid,
|
|
448
|
-
title: $title,
|
|
449
|
-
status: 'todo',
|
|
450
|
-
priority: toInteger($priority),
|
|
451
|
-
assigneeAgentId: $assigneeAgentId,
|
|
452
|
-
body: $body,
|
|
453
|
-
executionPolicyJson: $policyJson,
|
|
454
|
-
executionStateJson: $stateJson,
|
|
455
|
-
sourceType: $sourceType,
|
|
456
|
-
sourceId: $sourceId,
|
|
457
|
-
originKind: $originKind,
|
|
458
|
-
projectId: $projectId,
|
|
459
|
-
createdAt: datetime()
|
|
460
|
-
})`,
|
|
461
|
+
_taskCypher,
|
|
461
462
|
{
|
|
462
463
|
id: taskId,
|
|
463
464
|
cid,
|
|
@@ -472,7 +473,7 @@ async function handleCreateTask(req, res, ctx) {
|
|
|
472
473
|
originKind: originKind ? String(originKind) : null,
|
|
473
474
|
projectId: projectId ? String(projectId) : null,
|
|
474
475
|
}
|
|
475
|
-
);
|
|
476
|
+
));
|
|
476
477
|
|
|
477
478
|
// Phase 2.5-B: OI-P3 fix — write BELONGS_TO_PROJECT edge when projectId is provided
|
|
478
479
|
if (projectId) {
|
|
@@ -1947,6 +1948,9 @@ async function handleReviseApproval(req, res, ctx, approvalId) {
|
|
|
1947
1948
|
}
|
|
1948
1949
|
broadcast({ type: 'approval.revised', approvalId, companyId: cid });
|
|
1949
1950
|
|
|
1951
|
+
// Record revision in ActivityLogger so the activity feed shows it
|
|
1952
|
+
ctx.activityLogger?.record({ action: 'approval.revise', actor: cid, entityId: approvalId, companyId: cid });
|
|
1953
|
+
|
|
1950
1954
|
// B-19 fix: create AgentReadySignal so the assigned agent wakes and processes the revision.
|
|
1951
1955
|
// Without this, the agent that owns the approval's task never re-evaluates after revision.
|
|
1952
1956
|
setImmediate(async () => {
|
|
@@ -1966,6 +1970,9 @@ async function handleReviseApproval(req, res, ctx, approvalId) {
|
|
|
1966
1970
|
ON MATCH SET s.status = 'pending', s.approvalId = $approvalId`,
|
|
1967
1971
|
{ taskId: taskRow.taskId, agentId: taskRow.agentId, cid, approvalId }
|
|
1968
1972
|
);
|
|
1973
|
+
} else {
|
|
1974
|
+
// MED-8: B-19 — no [:FOR_TASK] edge found; agent not signaled
|
|
1975
|
+
log('warn', 'B-19: no Task linked to approval via FOR_TASK edge -- agent not signaled', { approvalId, cid });
|
|
1969
1976
|
}
|
|
1970
1977
|
} catch (sigErr) {
|
|
1971
1978
|
log('warn', 'B-19: AgentReadySignal creation failed after revision', { approvalId, err: sigErr.message });
|
|
@@ -2424,7 +2424,8 @@ class AgentDispatcher {
|
|
|
2424
2424
|
};
|
|
2425
2425
|
|
|
2426
2426
|
// E-07: Capture stderr to crash log so silent failures become visible.
|
|
2427
|
-
// Use ['ignore', '
|
|
2427
|
+
// Use ['ignore', 'pipe', 'pipe'] — stdin ignored, stdout+stderr both captured for crash log.
|
|
2428
|
+
// Previously used ['ignore', 'ignore', 'pipe'] — pi process may write errors to stdout.
|
|
2428
2429
|
const transcriptsDir = path.join(__dirname, 'transcripts');
|
|
2429
2430
|
try { fs.mkdirSync(transcriptsDir, { recursive: true }); } catch (_) {}
|
|
2430
2431
|
|
|
@@ -2432,13 +2433,13 @@ class AgentDispatcher {
|
|
|
2432
2433
|
|
|
2433
2434
|
const child = spawnFn(spawnExe, spawnArgs, {
|
|
2434
2435
|
detached: true,
|
|
2435
|
-
stdio: ['ignore', '
|
|
2436
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2436
2437
|
env: spawnEnv,
|
|
2437
2438
|
windowsHide: true,
|
|
2438
2439
|
shell: false,
|
|
2439
2440
|
});
|
|
2440
2441
|
|
|
2441
|
-
// Collect stderr chunks for crash log (64KB cap to prevent OOM from chatty workers)
|
|
2442
|
+
// Collect stderr+stdout chunks for crash log (64KB cap per stream to prevent OOM from chatty workers)
|
|
2442
2443
|
const stderrChunks = [];
|
|
2443
2444
|
let _stderrBytes = 0;
|
|
2444
2445
|
const MAX_STDERR_BYTES = 64 * 1024;
|
|
@@ -2453,6 +2454,18 @@ class AgentDispatcher {
|
|
|
2453
2454
|
// (child.unref() alone is insufficient when a stderr pipe listener is attached)
|
|
2454
2455
|
child.stderr.unref();
|
|
2455
2456
|
}
|
|
2457
|
+
// Also capture stdout (pi process may write errors/traces to stdout)
|
|
2458
|
+
const stdoutChunks = [];
|
|
2459
|
+
let _stdoutBytes = 0;
|
|
2460
|
+
if (child.stdout) {
|
|
2461
|
+
child.stdout.on('data', (chunk) => {
|
|
2462
|
+
if (_stdoutBytes < MAX_STDERR_BYTES) {
|
|
2463
|
+
stdoutChunks.push(chunk);
|
|
2464
|
+
_stdoutBytes += chunk.length;
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
child.stdout.unref();
|
|
2468
|
+
}
|
|
2456
2469
|
|
|
2457
2470
|
child.on('error', (err) => {
|
|
2458
2471
|
if (err.code === 'ENOENT') {
|
|
@@ -2465,6 +2478,7 @@ class AgentDispatcher {
|
|
|
2465
2478
|
// E-07: Write crash log when worker exits with non-zero code
|
|
2466
2479
|
if (code !== 0) {
|
|
2467
2480
|
const stderr = stderrChunks.length > 0 ? Buffer.concat(stderrChunks).toString('utf8') : '(no stderr captured)';
|
|
2481
|
+
const stdout = stdoutChunks.length > 0 ? Buffer.concat(stdoutChunks).toString('utf8') : '(no stdout captured)';
|
|
2468
2482
|
const crashLogPath = path.join(transcriptsDir, `${taskId.replace(/[^a-zA-Z0-9_-]/g, '_')}.crash.log`);
|
|
2469
2483
|
const crashContent = [
|
|
2470
2484
|
`=== CRASH LOG: task ${taskId} ===`,
|
|
@@ -2477,6 +2491,8 @@ class AgentDispatcher {
|
|
|
2477
2491
|
`HELIOS_COMPANY_ID: ${spawnEnv.HELIOS_COMPANY_ID}`,
|
|
2478
2492
|
`--- stderr ---`,
|
|
2479
2493
|
stderr,
|
|
2494
|
+
`--- stdout ---`,
|
|
2495
|
+
stdout,
|
|
2480
2496
|
].join('\n');
|
|
2481
2497
|
try {
|
|
2482
2498
|
fs.writeFileSync(crashLogPath, crashContent, 'utf8');
|
|
@@ -212,6 +212,16 @@ class HeadroomProxyManager {
|
|
|
212
212
|
});
|
|
213
213
|
|
|
214
214
|
child.on('exit', (code, signal) => {
|
|
215
|
+
// On Windows, Node.js reports abnormal OS termination as exit code 4294967295
|
|
216
|
+
// (0xFFFFFFFF — the unsigned 32-bit representation of -1).
|
|
217
|
+
// This indicates an OS-level process termination (e.g. OOM, crash).
|
|
218
|
+
if (code === 4294967295) {
|
|
219
|
+
process.stderr.write(
|
|
220
|
+
`[headroom-proxy-manager] WARN: compression server on port ${port} exited with abnormal code 0xFFFFFFFF (-1) — ` +
|
|
221
|
+
`likely an OOM kill or unhandled exception in the server process. ` +
|
|
222
|
+
`Scheduling restart with backoff.\n`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
215
225
|
appendLog({ event: 'exit', code, signal, port });
|
|
216
226
|
this._child = null;
|
|
217
227
|
this._baseUrl = null;
|
package/daemon/routes/inbox.js
CHANGED
|
@@ -1157,7 +1157,7 @@ module.exports = function({ broadcast } = {}) {
|
|
|
1157
1157
|
if (method === 'GET' && tasksMatch) {
|
|
1158
1158
|
const sourceId = decodeURIComponent(tasksMatch[1]);
|
|
1159
1159
|
try {
|
|
1160
|
-
const companyId = ctx.
|
|
1160
|
+
const companyId = ctx.cid || process.env.HELIOS_COMPANY_ID || ''; // CRIT-2: ctx.cid (ctx.companyId always undefined)
|
|
1161
1161
|
const mg5 = require('../../lib/safe-memgraph');
|
|
1162
1162
|
// Look up tasks by sourceId (email messageId) — scoped to company
|
|
1163
1163
|
const cypher = companyId
|
|
@@ -1192,6 +1192,49 @@ module.exports = function({ broadcast } = {}) {
|
|
|
1192
1192
|
}
|
|
1193
1193
|
}
|
|
1194
1194
|
|
|
1195
|
+
// Phase 2-F: POST /api/inbox/inbound — bridge from helios-mail service
|
|
1196
|
+
// Creates an InboxItem in the main inbox graph so inbound agent emails appear in the unified inbox.
|
|
1197
|
+
// Called fire-and-forget from services/helios-mail after message is inserted into helios-mail DB.
|
|
1198
|
+
if (method === 'POST' && pathname === '/api/inbox/inbound') {
|
|
1199
|
+
let body;
|
|
1200
|
+
try { body = await parseBody(req); } catch { return jsonResponse(res, 400, { error: 'Invalid JSON' }); }
|
|
1201
|
+
const {
|
|
1202
|
+
id, platform = 'helios-mail', senderName = '', senderHandle = '', subject = '',
|
|
1203
|
+
snippet = '', threadId = null, replyChannel = 'helios-mail',
|
|
1204
|
+
priority = 'P2', companyId,
|
|
1205
|
+
} = body;
|
|
1206
|
+
if (!id || typeof id !== 'string') {
|
|
1207
|
+
return jsonResponse(res, 400, { error: 'id is required' });
|
|
1208
|
+
}
|
|
1209
|
+
const scopeCompanyId = companyId || ctx.cid || process.env.HELIOS_COMPANY_ID || '';
|
|
1210
|
+
try {
|
|
1211
|
+
const mg6 = require('../../lib/safe-memgraph');
|
|
1212
|
+
await mg6.rawWrite(
|
|
1213
|
+
MERGE (i:InboxItem {id: })
|
|
1214
|
+
ON CREATE SET
|
|
1215
|
+
i.platform = ,
|
|
1216
|
+
i.senderName = ,
|
|
1217
|
+
i.senderHandle = ,
|
|
1218
|
+
i.subject = ,
|
|
1219
|
+
i.snippet = ,
|
|
1220
|
+
i.threadId = ,
|
|
1221
|
+
i.replyChannel = ,
|
|
1222
|
+
i.priority = ,
|
|
1223
|
+
i.state = 'unread',
|
|
1224
|
+
i.isRead = false,
|
|
1225
|
+
i.companyId = ,
|
|
1226
|
+
i.receivedAt = toString(datetime()),
|
|
1227
|
+
i.createdAt = toString(datetime())
|
|
1228
|
+
ON MATCH SET i.updatedAt = toString(datetime()),
|
|
1229
|
+
{ id, platform, senderName, senderHandle, subject, snippet, threadId,
|
|
1230
|
+
replyChannel, priority, companyId: scopeCompanyId }
|
|
1231
|
+
);
|
|
1232
|
+
return jsonResponse(res, 201, { ok: true, id });
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
return jsonResponse(res, 500, { error: err.message || 'Failed to create inbox item' });
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1195
1238
|
// PATCH /api/inbox/:id — update item state (archive, snooze, delegate, read)
|
|
1196
1239
|
const patchMatch = pathname.match(/^\/api\/inbox\/([^/]+)$/);
|
|
1197
1240
|
if (method === 'PATCH' && patchMatch) {
|
|
@@ -1200,7 +1243,8 @@ module.exports = function({ broadcast } = {}) {
|
|
|
1200
1243
|
try { body = await parseBody(req); } catch { jsonResponse(res, 400, { error: 'Invalid JSON' }); return true; }
|
|
1201
1244
|
|
|
1202
1245
|
const { action, until, assignee } = body;
|
|
1203
|
-
|
|
1246
|
+
if (!ctx.mgQuery) { return jsonResponse(res, 503, { error: 'Memgraph not connected' }); } // MED-9
|
|
1247
|
+
const VALID_ACTIONS = ['archive', 'snooze', 'read', 'delegate', 'dismiss', 'addLabel', 'removeLabel', 'assign', 'approve_draft', 'skip', 'send_later', 'cancel_send', 'unsubscribe', 'block_sender', 'send', 'star', 'unstar', 'mute', 'unmute', 'unread'];
|
|
1204
1248
|
if (!action || !VALID_ACTIONS.includes(action)) {
|
|
1205
1249
|
jsonResponse(res, 400, { error: 'Invalid action. Supported: ' + VALID_ACTIONS.join(', ') });
|
|
1206
1250
|
return true;
|
|
@@ -1363,13 +1407,18 @@ module.exports = function({ broadcast } = {}) {
|
|
|
1363
1407
|
const newState = stateMap[action] || 'read';
|
|
1364
1408
|
|
|
1365
1409
|
// P9B-02: approve_draft — dispatch the DraftAction content before archiving
|
|
1410
|
+
let _draftSent = false;
|
|
1411
|
+
let _draftNotFound = false; // MED-6/7: track when DraftAction not found
|
|
1366
1412
|
// Previous behavior: just archived the item and discarded the draft.
|
|
1367
1413
|
// Fixed behavior: look up DraftAction, dispatch via appropriate channel, mark approved, then archive.
|
|
1368
1414
|
if (action === 'approve_draft') {
|
|
1369
1415
|
try {
|
|
1370
1416
|
// itemId format for draft items is "draft-{draftId}"
|
|
1371
1417
|
const draftId = itemId.startsWith('draft-') ? itemId.slice('draft-'.length) : body.draftId;
|
|
1372
|
-
if (draftId) {
|
|
1418
|
+
if (!draftId) {
|
|
1419
|
+
_draftNotFound = true; // MED-7: prefix-only or missing draftId
|
|
1420
|
+
log('warn', '[approve_draft] no draftId resolvable for itemId: ' + itemId);
|
|
1421
|
+
} else if (draftId) {
|
|
1373
1422
|
const draftRows = await mg.safeRead(
|
|
1374
1423
|
'MATCH (d:DraftAction {id: $draftId}) RETURN d.content AS content, d.type AS type, d.personId AS personId',
|
|
1375
1424
|
{ draftId }
|
|
@@ -1401,7 +1450,7 @@ module.exports = function({ broadcast } = {}) {
|
|
|
1401
1450
|
}
|
|
1402
1451
|
// Mark DraftAction as approved regardless of dispatch result
|
|
1403
1452
|
await mg.rawWrite(
|
|
1404
|
-
'MATCH (d:DraftAction {id: $draftId}) SET d.status = "approved", d.approvedAt = localdatetime()',
|
|
1453
|
+
'MATCH (d:DraftAction {id: $draftId}) WHERE d.status <> "approved" SET d.status = "approved", d.approvedAt = localdatetime()',
|
|
1405
1454
|
{ draftId }
|
|
1406
1455
|
);
|
|
1407
1456
|
}
|
|
@@ -1423,6 +1472,14 @@ module.exports = function({ broadcast } = {}) {
|
|
|
1423
1472
|
jsonResponse(res, 200, { ok: true, id: itemId, action, isStarred: true });
|
|
1424
1473
|
return true;
|
|
1425
1474
|
}
|
|
1475
|
+
if (action === 'unstar') {
|
|
1476
|
+
await write(
|
|
1477
|
+
'MATCH (i:InboxItem {id: $itemId}) SET i.isStarred = false, i.actionedAt = toString(localdatetime())',
|
|
1478
|
+
{ itemId }
|
|
1479
|
+
);
|
|
1480
|
+
jsonResponse(res, 200, { ok: true, id: itemId, action, isStarred: false });
|
|
1481
|
+
return true;
|
|
1482
|
+
}
|
|
1426
1483
|
if (action === 'mute') {
|
|
1427
1484
|
await write(
|
|
1428
1485
|
'MATCH (i:InboxItem {id: $itemId}) SET i.isMuted = true, i.actionedAt = toString(localdatetime())',
|
|
@@ -1431,6 +1488,14 @@ module.exports = function({ broadcast } = {}) {
|
|
|
1431
1488
|
jsonResponse(res, 200, { ok: true, id: itemId, action, isMuted: true });
|
|
1432
1489
|
return true;
|
|
1433
1490
|
}
|
|
1491
|
+
if (action === 'unmute') {
|
|
1492
|
+
await write(
|
|
1493
|
+
'MATCH (i:InboxItem {id: $itemId}) SET i.isMuted = false, i.actionedAt = toString(localdatetime())',
|
|
1494
|
+
{ itemId }
|
|
1495
|
+
);
|
|
1496
|
+
jsonResponse(res, 200, { ok: true, id: itemId, action, isMuted: false });
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1434
1499
|
if (action === 'unread') {
|
|
1435
1500
|
await write(
|
|
1436
1501
|
'MATCH (i:InboxItem {id: $itemId}) SET i.isRead = false, i.actionedAt = toString(localdatetime())',
|
|
@@ -1477,7 +1542,7 @@ module.exports = function({ broadcast } = {}) {
|
|
|
1477
1542
|
if (action === 'delegate') {
|
|
1478
1543
|
try {
|
|
1479
1544
|
// I4-09 fix: include companyId so task is visible in company-scoped queries
|
|
1480
|
-
const delegateCompanyId = ctx.
|
|
1545
|
+
const delegateCompanyId = ctx.cid || process.env.HELIOS_COMPANY_ID || ''; // CRIT-2 fix
|
|
1481
1546
|
const delegateRows = await mg.safeRead('MATCH (i:InboxItem {id: $itemId}) RETURN i.subject AS subject, i.priority AS priority', { itemId });
|
|
1482
1547
|
const subject = (delegateRows && delegateRows[0] && delegateRows[0].subject) || 'inbox item';
|
|
1483
1548
|
const priorityNum = (delegateRows && delegateRows[0] && delegateRows[0].priority === 'P0') ? 0 : 1;
|
|
@@ -1489,7 +1554,7 @@ module.exports = function({ broadcast } = {}) {
|
|
|
1489
1554
|
"t.originKind = 'inbox_delegate', t.progressPropagated = false, t.createdAt = localdatetime() " +
|
|
1490
1555
|
"WITH t " +
|
|
1491
1556
|
"OPTIONAL MATCH (a:BusinessAgent {status: 'active'}) WHERE a.capabilities CONTAINS 'triage' " +
|
|
1492
|
-
"WITH t, a LIMIT 1 " +
|
|
1557
|
+
"WITH t, a LIMIT 1 WHERE a IS NOT NULL " + // HIGH-4
|
|
1493
1558
|
"MERGE (s:AgentReadySignal {id: 'signal:inbox:' + a.id + ':' + t.id}) " +
|
|
1494
1559
|
"ON CREATE SET " +
|
|
1495
1560
|
" s.agentId = a.id, " +
|
|
@@ -1503,7 +1568,9 @@ module.exports = function({ broadcast } = {}) {
|
|
|
1503
1568
|
} catch (_) { /* fail-open: task creation optional */ }
|
|
1504
1569
|
}
|
|
1505
1570
|
|
|
1506
|
-
|
|
1571
|
+
// MED-6/7: include draftNotFound in response for approve_draft
|
|
1572
|
+
const _extraFields = action === 'approve_draft' ? { draftNotFound: _draftNotFound } : {};
|
|
1573
|
+
jsonResponse(res, 200, { ok: true, id: itemId, state: newState, action, ..._extraFields });
|
|
1507
1574
|
} catch (err) {
|
|
1508
1575
|
jsonResponse(res, 500, { error: err.message || 'Failed to update item' });
|
|
1509
1576
|
}
|