@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 ~/helios-agent/bin/helios "$@"
17
+ exec "${HELIOS_AGENT_ROOT}/bin/helios" "$@"
@@ -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
- `CREATE (t:Task {
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', 'ignore', 'pipe'] — stdin/stdout ignored, stderr captured.
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', 'ignore', 'pipe'],
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;
@@ -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.companyId || process.env.HELIOS_COMPANY_ID || '';
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
- const VALID_ACTIONS = ['archive', 'snooze', 'read', 'delegate', 'dismiss', 'addLabel', 'removeLabel', 'assign', 'approve_draft', 'skip', 'send_later', 'cancel_send', 'unsubscribe', 'block_sender', 'send', 'star', 'mute', 'unread'];
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.companyId || process.env.HELIOS_COMPANY_ID || '';
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
- jsonResponse(res, 200, { ok: true, id: itemId, state: newState, action });
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cgh567/agent",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
4
4
  "description": "Helios agent runtime — full stack: extensions, skills, daemon, brain-v2, HEMA, governance",
5
5
  "type": "commonjs",
6
6
  "bin": {