@cgh567/agent 2.4.3 → 2.4.4
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/agents/business/talisman-ceo.md +183 -0
- package/agents/business/talisman-comms.md +257 -0
- package/agents/business/talisman-cto.md +153 -0
- package/agents/business/talisman-finance.md +246 -0
- package/agents/business/talisman-marketing.md +240 -0
- package/agents/business/talisman-sales.md +242 -0
- package/agents/business/talisman-support.md +236 -0
- package/bin/helios-rpc.js +19 -0
- package/daemon/adapters/helios-rpc-adapter.js +5 -12
- package/daemon/context-enrichment.js +27 -0
- package/daemon/helios-api.js +290 -45
- package/daemon/helios-company-daemon.js +160 -50
- package/daemon/lib/blast-radius-analyzer.js +75 -0
- package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
- package/daemon/lib/forensic-log.js +113 -0
- package/daemon/lib/goal-research-pipeline.js +644 -0
- package/daemon/lib/harada/cascade-judge.js +84 -1
- package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
- package/daemon/lib/harada/pillar-dispatcher.js +23 -2
- package/daemon/lib/hbo-bridge.js +73 -5
- package/daemon/lib/headroom-middleware.js +129 -0
- package/daemon/lib/headroom-proxy-manager.js +309 -0
- package/daemon/lib/intelligence/department-page-generator.js +46 -1
- package/daemon/lib/interpretation-engine.js +92 -0
- package/daemon/lib/mental-model-cache.js +96 -0
- package/daemon/lib/project-factory.js +47 -0
- package/daemon/lib/session-log-reader.js +93 -0
- package/daemon/lib/standard-work-bootstrap.js +87 -1
- package/daemon/lib/task-completion-processor.js +12 -0
- package/daemon/package.json +2 -1
- package/daemon/routes/agents.js +51 -6
- package/daemon/routes/channels.js +116 -2
- package/daemon/routes/crm.js +85 -0
- package/daemon/routes/dashboard.js +62 -16
- package/daemon/routes/dept.js +10 -1
- package/daemon/routes/email-triage.js +19 -10
- package/daemon/routes/hbo.js +367 -13
- package/daemon/routes/hed.js +133 -0
- package/daemon/routes/inbox.js +397 -8
- package/daemon/routes/project.js +392 -9
- package/daemon/schema-definitions.js +10 -0
- package/daemon/schema-migrations-hbo.js +10 -0
- package/daemon/schema-migrations-proj.js +22 -0
- package/extensions/__tests__/codebase-index.test.ts +73 -0
- package/extensions/__tests__/extension-command-registration.test.ts +35 -0
- package/extensions/__tests__/git-push-guard.test.ts +68 -0
- package/extensions/context-compaction.ts +104 -76
- package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
- package/extensions/email/actions/draft-response.ts +21 -1
- package/extensions/email/auth/accounts.ts +5 -11
- package/extensions/email/auth/inbox-dog.ts +5 -2
- package/extensions/email/backfill.ts +20 -13
- package/extensions/email/providers/gmail.ts +164 -0
- package/extensions/email/providers/google-calendar.ts +34 -5
- package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
- package/extensions/helios-browser/backends/playwright.ts +3 -1
- package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
- package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
- package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
- package/extensions/hema-dispatch-v3/index.ts +33 -65
- package/extensions/interview/__tests__/server.test.ts +117 -0
- package/extensions/lib/helios-root.cjs +46 -0
- package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
- package/extensions/warm-tick/warm-tick-maintenance.ts +156 -0
- package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
- package/lib/__tests__/crash-fixes.test.ts +49 -0
- package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
- package/lib/broker/__tests__/jit-subscription.test.js +44 -1
- package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
- package/lib/compression/__tests__/ccr-store.test.js +138 -0
- package/lib/compression/__tests__/pipeline.test.js +280 -0
- package/lib/compression/__tests__/smart-crusher.test.js +242 -0
- package/lib/compression/dist/server.js +34 -1
- package/lib/compression/dist/start-server.js +77 -0
- package/lib/graph/learning/headroom-learn-bridge.js +175 -0
- package/lib/hbo-core-store.ts +71 -0
- package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
- package/lib/skill-sync.js +6 -1
- package/lib/startup-integrity.js +9 -2
- package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
- package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
- package/lib/triage-core/__tests__/classifier.test.ts +45 -7
- package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
- package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
- package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
- package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
- package/lib/triage-core/__tests__/signals.test.ts +357 -0
- package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
- package/lib/triage-core/backfill-cost-estimator.ts +91 -0
- package/lib/triage-core/backfill-orchestrator.ts +119 -0
- package/lib/triage-core/classifier.ts +38 -6
- package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
- package/lib/triage-core/cos/response-debt.ts +2 -2
- package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
- package/lib/triage-core/graph/batch-persistence.ts +66 -2
- package/lib/triage-core/graph/betweenness-worker.js +75 -0
- package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
- package/lib/triage-core/graph/persistence.ts +1 -1
- package/lib/triage-core/graph/schema-v2.ts +2 -0
- package/lib/triage-core/graph/schema.cypher +1 -0
- package/lib/triage-core/graph/triage-query.ts +1 -1
- package/lib/triage-core/learning.ts +15 -20
- package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
- package/lib/triage-core/mental-model/cos-integration.ts +1 -1
- package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
- package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
- package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
- package/lib/triage-core/mental-model/model-assembler.ts +16 -3
- package/lib/triage-core/orchestrator.ts +4 -4
- package/lib/triage-core/scheduled-sends.ts +39 -2
- package/lib/triage-core/signals/comms-style.ts +1 -1
- package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
- package/lib/triage-core/signals/favee-type.ts +6 -1
- package/lib/triage-core/signals/goal-relevance.ts +31 -2
- package/lib/triage-core/signals/personal-importance.ts +1 -1
- package/lib/triage-core/signals/referral-chain.ts +0 -1
- package/lib/triage-core/signals/relationship-decay.ts +4 -0
- package/lib/triage-core/signals/relationship-health.ts +6 -1
- package/lib/triage-core/signals/trajectory-signal.ts +38 -3
- package/lib/triage-core/tournament-runner.js +11 -1
- package/lib/triage-core/triage-llm-factory.ts +110 -0
- package/lib/triage-core/triage-local-llm.ts +145 -0
- package/lib/triage-core/triage-sql-store.ts +337 -0
- package/lib/triage-core/types.ts +2 -2
- package/lib/unified-graph.atomic.test.ts +52 -0
- package/lib/unified-graph.failure-categories.test.ts +55 -0
- package/package.json +10 -3
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/skills/helios-bookkeeping/SKILL.md +321 -0
- package/skills/helios-briefer/SKILL.md +44 -0
- package/skills/helios-client-relations/SKILL.md +322 -0
- package/skills/helios-personal-triager/SKILL.md +45 -0
- package/skills/helios-recruitment/SKILL.md +317 -0
- package/skills/helios-relationship-nudger/SKILL.md +77 -0
- package/skills/helios-researcher/SKILL.md +44 -0
- package/skills/helios-scheduler/SKILL.md +58 -0
- package/skills/helios-tax-analyst/SKILL.md +280 -0
- package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -1823
package/daemon/helios-api.js
CHANGED
|
@@ -383,7 +383,9 @@ async function handleCreateTask(req, res, ctx) {
|
|
|
383
383
|
const cid = ctx.cid;
|
|
384
384
|
|
|
385
385
|
const body = await parseBody(req);
|
|
386
|
-
const { title, assigneeAgentId, priority, body: taskBody, executionPolicy
|
|
386
|
+
const { title, assigneeAgentId, priority, body: taskBody, executionPolicy,
|
|
387
|
+
// Phase 2.5-B additions: sourceType, sourceId, originKind, projectId for email-to-task
|
|
388
|
+
sourceType, sourceId, originKind, projectId } = body;
|
|
387
389
|
|
|
388
390
|
if (!title || typeof title !== 'string' || title.trim() === '') {
|
|
389
391
|
jsonResponse(res, 400, { error: 'title is required and must be a non-empty string' });
|
|
@@ -450,6 +452,10 @@ async function handleCreateTask(req, res, ctx) {
|
|
|
450
452
|
body: $body,
|
|
451
453
|
executionPolicyJson: $policyJson,
|
|
452
454
|
executionStateJson: $stateJson,
|
|
455
|
+
sourceType: $sourceType,
|
|
456
|
+
sourceId: $sourceId,
|
|
457
|
+
originKind: $originKind,
|
|
458
|
+
projectId: $projectId,
|
|
453
459
|
createdAt: datetime()
|
|
454
460
|
})`,
|
|
455
461
|
{
|
|
@@ -461,9 +467,27 @@ async function handleCreateTask(req, res, ctx) {
|
|
|
461
467
|
body: taskBody ? String(taskBody) : null,
|
|
462
468
|
policyJson: validatedPolicyJson,
|
|
463
469
|
stateJson: initialStateJson,
|
|
470
|
+
sourceType: sourceType ? String(sourceType) : null,
|
|
471
|
+
sourceId: sourceId ? String(sourceId) : null,
|
|
472
|
+
originKind: originKind ? String(originKind) : null,
|
|
473
|
+
projectId: projectId ? String(projectId) : null,
|
|
464
474
|
}
|
|
465
475
|
);
|
|
466
476
|
|
|
477
|
+
// Phase 2.5-B: OI-P3 fix — write BELONGS_TO_PROJECT edge when projectId is provided
|
|
478
|
+
if (projectId) {
|
|
479
|
+
try {
|
|
480
|
+
await mgQuery(
|
|
481
|
+
`MATCH (t:Task {id: $taskId}), (p:HeliosProject {id: $projectId, companyId: $cid})
|
|
482
|
+
MERGE (t)-[:BELONGS_TO_PROJECT]->(p)`,
|
|
483
|
+
{ taskId, projectId: String(projectId), cid }
|
|
484
|
+
);
|
|
485
|
+
} catch (_edgeErr) {
|
|
486
|
+
// Edge write is best-effort — task is already created; log but don't fail
|
|
487
|
+
log('warn', 'BELONGS_TO_PROJECT edge write failed (OI-P3)', { taskId, projectId });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
467
491
|
mirrorHboStore('task.create', () => hboStore.createTask?.({
|
|
468
492
|
id: taskId,
|
|
469
493
|
companyId: cid,
|
|
@@ -479,6 +503,17 @@ async function handleCreateTask(req, res, ctx) {
|
|
|
479
503
|
|
|
480
504
|
broadcast({ type: 'task.created', taskId, status: 'todo', companyId: cid });
|
|
481
505
|
jsonResponse(res, 201, { task: { id: taskId, title, status: 'todo', priority: resolvedPriority, assigneeAgentId, executionPolicyJson: validatedPolicyJson } });
|
|
506
|
+
|
|
507
|
+
// P_EXPLAIN-01: fire-and-forget interpretation explanation background job
|
|
508
|
+
const { rawWrite: _interpRawWrite } = require('../lib/safe-memgraph.js');
|
|
509
|
+
setImmediate(() => {
|
|
510
|
+
try {
|
|
511
|
+
const { createInterpretationExplanation } = require('./lib/interpretation-engine.js');
|
|
512
|
+
createInterpretationExplanation(taskId, cid, _interpRawWrite).catch(e =>
|
|
513
|
+
log('warn', 'P_EXPLAIN-01 failed', { e: e.message })
|
|
514
|
+
);
|
|
515
|
+
} catch (_) { /* interpretation is non-blocking */ }
|
|
516
|
+
});
|
|
482
517
|
} catch (err) {
|
|
483
518
|
jsonResponse(res, 500, { error: `Create failed: ${err.message}` });
|
|
484
519
|
}
|
|
@@ -491,7 +526,9 @@ async function handleUpdateTask(req, res, ctx, taskId) {
|
|
|
491
526
|
const cid = ctx.cid;
|
|
492
527
|
|
|
493
528
|
const body = await parseBody(req);
|
|
494
|
-
|
|
529
|
+
// P6-06a: workspacePath added — desktop sets this after acquiring a SessionWorktree
|
|
530
|
+
// for isolated execution; daemon reads it into adapterContext.workspacePath
|
|
531
|
+
const { status, priority, workspacePath, blockedReason, clearBlocked, hillPosition } = body;
|
|
495
532
|
|
|
496
533
|
// P4-01: 'in_review' added — allows execution policy gate to hold a task for review
|
|
497
534
|
const VALID_STATUSES = new Set(['todo', 'in_progress', 'done', 'failed', 'cancelled', 'blocked', 'escalated', 'in_review']);
|
|
@@ -504,8 +541,8 @@ async function handleUpdateTask(req, res, ctx, taskId) {
|
|
|
504
541
|
return;
|
|
505
542
|
}
|
|
506
543
|
|
|
507
|
-
if (status === undefined && priority === undefined) {
|
|
508
|
-
jsonResponse(res, 400, { error: 'Provide at least one field to update: status, priority' });
|
|
544
|
+
if (status === undefined && priority === undefined && workspacePath === undefined && blockedReason === undefined && clearBlocked === undefined && hillPosition === undefined) {
|
|
545
|
+
jsonResponse(res, 400, { error: 'Provide at least one field to update: status, priority, workspacePath, blockedReason, clearBlocked, hillPosition' });
|
|
509
546
|
return;
|
|
510
547
|
}
|
|
511
548
|
|
|
@@ -767,6 +804,17 @@ async function handleUpdateTask(req, res, ctx, taskId) {
|
|
|
767
804
|
const params = { id: taskId, cid };
|
|
768
805
|
if (status !== undefined) { setClauses.push('t.status = $status'); params.status = status; }
|
|
769
806
|
if (priority !== undefined) { setClauses.push('t.priority = toInteger($priority)'); params.priority = Number(priority); }
|
|
807
|
+
// P6-06a: persist workspacePath on Task node so daemon can read it into adapterContext
|
|
808
|
+
if (workspacePath !== undefined) { setClauses.push('t.workspacePath = $workspacePath'); params.workspacePath = workspacePath ? String(workspacePath) : null; }
|
|
809
|
+
// P1-G: blockedReason + blockedAt — clearBlocked=true nulls both fields and resets status to todo.
|
|
810
|
+
// COALESCE preserves existing reason if not re-set; blockedAt only set once on initial block.
|
|
811
|
+
if (blockedReason !== undefined || clearBlocked !== undefined) {
|
|
812
|
+
params.clearBlocked = clearBlocked === true;
|
|
813
|
+
params.blockedReason = (blockedReason !== undefined && blockedReason !== null) ? String(blockedReason) : null;
|
|
814
|
+
setClauses.push('t.blockedReason = CASE WHEN $clearBlocked = true THEN null ELSE COALESCE($blockedReason, t.blockedReason) END');
|
|
815
|
+
setClauses.push('t.blockedAt = CASE WHEN $clearBlocked = true THEN null WHEN $blockedReason IS NOT NULL AND t.blockedAt IS NULL THEN datetime() ELSE t.blockedAt END');
|
|
816
|
+
setClauses.push('t.status = CASE WHEN $clearBlocked = true AND t.status = \'blocked\' THEN \'todo\' ELSE t.status END');
|
|
817
|
+
}
|
|
770
818
|
setClauses.push('t.updatedAt = datetime()');
|
|
771
819
|
|
|
772
820
|
// B-09: Single atomic check+update — no TOCTOU window between existence check and SET
|
|
@@ -779,9 +827,25 @@ async function handleUpdateTask(req, res, ctx, taskId) {
|
|
|
779
827
|
return;
|
|
780
828
|
}
|
|
781
829
|
|
|
830
|
+
// T4-02: HillChart write — track task work-cycle position for hill chart visualisation
|
|
831
|
+
if (hillPosition !== undefined && Number.isFinite(Number(hillPosition))) {
|
|
832
|
+
const hPos = Math.max(0, Math.min(1, Number(hillPosition)));
|
|
833
|
+
const hPhase = hPos < 0.5 ? 'uphill' : hPos < 1.0 ? 'downhill' : 'complete';
|
|
834
|
+
mgQuery(
|
|
835
|
+
`MERGE (h:HillChart {id: 'hill:' + $taskId})
|
|
836
|
+
SET h.taskId = $taskId, h.companyId = $cid,
|
|
837
|
+
h.position = $pos, h.phase = $phase, h.updatedAt = datetime()
|
|
838
|
+
WITH h
|
|
839
|
+
MATCH (t:Task {id: $taskId, companyId: $cid})
|
|
840
|
+
MERGE (t)-[:HAS_HILL_CHART]->(h)`,
|
|
841
|
+
{ taskId, cid, pos: hPos, phase: hPhase }
|
|
842
|
+
).catch(e => log('warn', `HillChart MERGE failed for task ${taskId}: ${e.message}`));
|
|
843
|
+
}
|
|
844
|
+
|
|
782
845
|
const storeUpdate = {};
|
|
783
846
|
if (status !== undefined) storeUpdate.status = status;
|
|
784
847
|
if (priority !== undefined) storeUpdate.priority = Number(priority);
|
|
848
|
+
if (workspacePath !== undefined) storeUpdate.workspacePath = workspacePath ? String(workspacePath) : null;
|
|
785
849
|
mirrorHboStore('task.update', () => hboStore.updateTask?.(taskId, cid, storeUpdate));
|
|
786
850
|
|
|
787
851
|
broadcast({ type: 'task.updated', taskId, ...(status !== undefined ? { status } : {}), companyId: cid });
|
|
@@ -907,6 +971,7 @@ async function handleGetApprovals(req, res, ctx) {
|
|
|
907
971
|
a.status AS status, a.requestedBy AS requestedBy,
|
|
908
972
|
a.agentId AS agentId, a.taskId AS taskId,
|
|
909
973
|
a.createdAt AS createdAt, a.followUpTaskCreated AS followUpTaskCreated,
|
|
974
|
+
a.followUpTaskId AS followUpTaskId,
|
|
910
975
|
a.expiresAt AS expiresAt,
|
|
911
976
|
a.kaizenProposalId AS kaizenProposalId,
|
|
912
977
|
a.description AS description,
|
|
@@ -954,7 +1019,7 @@ async function handleGetApprovals(req, res, ctx) {
|
|
|
954
1019
|
const rows = parseRows(result);
|
|
955
1020
|
const keys = [
|
|
956
1021
|
'id', 'type', 'title', 'status', 'requestedBy', 'agentId', 'taskId',
|
|
957
|
-
'createdAt', 'followUpTaskCreated', 'expiresAt', 'kaizenProposalId',
|
|
1022
|
+
'createdAt', 'followUpTaskCreated', 'followUpTaskId', 'expiresAt', 'kaizenProposalId',
|
|
958
1023
|
'description', 'payload', 'body', 'question', 'pillarId',
|
|
959
1024
|
'sourceAgent', 'resolvedAt', 'department', 'templateKey',
|
|
960
1025
|
'inferredAnswer', 'defaultAnswer',
|
|
@@ -1089,6 +1154,8 @@ async function handleApproveApproval(req, res, ctx, approvalId) {
|
|
|
1089
1154
|
}
|
|
1090
1155
|
|
|
1091
1156
|
broadcast({ type: 'approval.approved', approvalId, companyId: cid });
|
|
1157
|
+
// H-01: record approval.approve in ActivityLogger
|
|
1158
|
+
ctx.activityLogger?.record({ action: 'approval.approve', actor: cid, entityId: approvalId, companyId: cid });
|
|
1092
1159
|
jsonResponse(res, 200, { approved: true, approvalId });
|
|
1093
1160
|
} catch (err) {
|
|
1094
1161
|
jsonResponse(res, 500, { error: `Approve failed: ${err.message}` });
|
|
@@ -1118,6 +1185,8 @@ async function handleRejectApproval(req, res, ctx, approvalId) {
|
|
|
1118
1185
|
}
|
|
1119
1186
|
mirrorHboStore('approval.reject', () => hboStore.updateApproval?.(approvalId, cid, { status: 'rejected', rejectReason: reason, rejectedAt: Date.now() }));
|
|
1120
1187
|
broadcast({ type: 'approval.rejected', approvalId, reason, companyId: cid });
|
|
1188
|
+
// H-01: record approval.reject in ActivityLogger
|
|
1189
|
+
ctx.activityLogger?.record({ action: 'approval.reject', actor: cid, entityId: approvalId, companyId: cid });
|
|
1121
1190
|
jsonResponse(res, 200, { rejected: true, approvalId, reason });
|
|
1122
1191
|
} catch (err) {
|
|
1123
1192
|
jsonResponse(res, 500, { error: `Reject failed: ${err.message}` });
|
|
@@ -1396,16 +1465,39 @@ async function handleGetAgents(req, res, ctx) {
|
|
|
1396
1465
|
|
|
1397
1466
|
async function handleGetAgent(req, res, ctx, agentId) {
|
|
1398
1467
|
const { mgQuery } = ctx;
|
|
1399
|
-
if (!mgQuery) { jsonResponse(res, 503, { error: 'Memgraph not connected' }); return; }
|
|
1400
1468
|
if (!agentId) { jsonResponse(res, 400, { error: 'Missing agent ID' }); return; }
|
|
1401
1469
|
const cid = ctx.cid;
|
|
1402
1470
|
|
|
1471
|
+
// P7-A1: SQL parity — fall back to SQLite when Memgraph unavailable
|
|
1472
|
+
if (!mgQuery) {
|
|
1473
|
+
try {
|
|
1474
|
+
const a = hboStore.getBusinessAgent ? hboStore.getBusinessAgent(agentId, cid) : null;
|
|
1475
|
+
if (!a) { jsonResponse(res, 404, { error: `Agent not found: ${agentId}` }); return; }
|
|
1476
|
+
// H-1 fix: also include recentTasks from SQLite so shape matches Memgraph path
|
|
1477
|
+
const sqliteTasks = hboStore.getTasksByCompanyStatus
|
|
1478
|
+
? hboStore.getTasksByCompanyStatus(cid, ['todo', 'in_progress', 'done']).filter(t => t.assigneeAgentId === agentId || t.assignee_agent_id === agentId).slice(0, 10)
|
|
1479
|
+
: [];
|
|
1480
|
+
a.recentTasks = sqliteTasks.map(t => ({ id: t.id, title: t.title ?? null, status: t.status ?? null, priority: t.priority ?? null }));
|
|
1481
|
+
return jsonResponse(res, 200, { agent: a, _source: 'sqlite' });
|
|
1482
|
+
} catch (storeErr) {
|
|
1483
|
+
return jsonResponse(res, 503, { error: 'Agent unavailable: Memgraph not connected', agent: null });
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1403
1487
|
try {
|
|
1404
1488
|
const [agentResult, tasksResult] = await Promise.all([
|
|
1405
1489
|
mgQuery(
|
|
1490
|
+
// P7-A1: extended RETURN to include all required fields for agent detail page
|
|
1406
1491
|
`MATCH (a:BusinessAgent {id: $id, companyId: $cid})
|
|
1407
|
-
RETURN a.id AS id, a.title AS title, a.status AS status,
|
|
1408
|
-
a.
|
|
1492
|
+
RETURN a.id AS id, a.title AS title, a.role AS role, a.status AS status,
|
|
1493
|
+
a.skill AS skill, a.adapter AS adapter,
|
|
1494
|
+
a.budgetMonthlyCents AS budgetMonthlyCents,
|
|
1495
|
+
a.heartbeatIntervalMs AS heartbeatIntervalMs,
|
|
1496
|
+
a.capabilities AS capabilities,
|
|
1497
|
+
a.defaultSkills AS defaultSkills,
|
|
1498
|
+
a.lastHeartbeatAt AS lastHeartbeatAt,
|
|
1499
|
+
a.pauseReason AS pauseReason,
|
|
1500
|
+
a.companyId AS companyId`,
|
|
1409
1501
|
{ id: agentId, cid }
|
|
1410
1502
|
),
|
|
1411
1503
|
mgQuery(
|
|
@@ -1422,7 +1514,9 @@ async function handleGetAgent(req, res, ctx, agentId) {
|
|
|
1422
1514
|
return;
|
|
1423
1515
|
}
|
|
1424
1516
|
|
|
1425
|
-
const agentKeys = ['id', 'title', 'status', '
|
|
1517
|
+
const agentKeys = ['id', 'title', 'role', 'status', 'skill', 'adapter',
|
|
1518
|
+
'budgetMonthlyCents', 'heartbeatIntervalMs', 'capabilities', 'defaultSkills',
|
|
1519
|
+
'lastHeartbeatAt', 'pauseReason', 'companyId'];
|
|
1426
1520
|
const taskKeys = ['id', 'title', 'status', 'priority'];
|
|
1427
1521
|
const agent = rowToObj(agentRows[0], agentKeys);
|
|
1428
1522
|
agent.recentTasks = parseRows(tasksResult).map(r => rowToObj(r, taskKeys));
|
|
@@ -1433,6 +1527,37 @@ async function handleGetAgent(req, res, ctx, agentId) {
|
|
|
1433
1527
|
}
|
|
1434
1528
|
}
|
|
1435
1529
|
|
|
1530
|
+
// P7-A2: GET /api/agents/:id/runs — HeartbeatRun history for an agent
|
|
1531
|
+
async function handleGetAgentRuns(req, res, ctx, agentId) {
|
|
1532
|
+
const { mgQuery } = ctx;
|
|
1533
|
+
if (!agentId) { jsonResponse(res, 400, { error: 'Missing agent ID' }); return; }
|
|
1534
|
+
const cid = ctx.cid;
|
|
1535
|
+
|
|
1536
|
+
// SQL parity: HeartbeatRun nodes are Memgraph-only (no SQLite mirror yet)
|
|
1537
|
+
if (!mgQuery) {
|
|
1538
|
+
return jsonResponse(res, 200, { runs: [], _source: 'sqlite' });
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
try {
|
|
1542
|
+
const result = await mgQuery(
|
|
1543
|
+
// H-2 fix: removed h.runId (field doesn't exist on HeartbeatRun nodes — they use h.id)
|
|
1544
|
+
// M-4 fix: use toString() for consistent datetime ordering (matches all other ORDER BY in codebase)
|
|
1545
|
+
`MATCH (h:HeartbeatRun {agentId: $agentId, companyId: $cid})
|
|
1546
|
+
RETURN h.id AS id, h.agentId AS agentId, h.companyId AS companyId,
|
|
1547
|
+
h.status AS status, h.startedAt AS startedAt, h.endedAt AS endedAt,
|
|
1548
|
+
h.taskId AS taskId
|
|
1549
|
+
ORDER BY toString(h.startedAt) DESC LIMIT toInteger(50)`,
|
|
1550
|
+
{ agentId, cid }
|
|
1551
|
+
);
|
|
1552
|
+
const rows = parseRows(result);
|
|
1553
|
+
const keys = ['id', 'agentId', 'companyId', 'status', 'startedAt', 'endedAt', 'taskId'];
|
|
1554
|
+
const runs = rows.map(r => rowToObj(r, keys));
|
|
1555
|
+
return jsonResponse(res, 200, { runs, count: runs.length });
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
return jsonResponse(res, 500, { error: `Query failed: ${err.message}` });
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1436
1561
|
// ── Activity ──────────────────────────────────────────────────────────────────
|
|
1437
1562
|
|
|
1438
1563
|
async function handleGetActivity(req, res, ctx) {
|
|
@@ -1821,6 +1946,32 @@ async function handleReviseApproval(req, res, ctx, approvalId) {
|
|
|
1821
1946
|
return;
|
|
1822
1947
|
}
|
|
1823
1948
|
broadcast({ type: 'approval.revised', approvalId, companyId: cid });
|
|
1949
|
+
|
|
1950
|
+
// B-19 fix: create AgentReadySignal so the assigned agent wakes and processes the revision.
|
|
1951
|
+
// Without this, the agent that owns the approval's task never re-evaluates after revision.
|
|
1952
|
+
setImmediate(async () => {
|
|
1953
|
+
try {
|
|
1954
|
+
const taskRows = await mgQuery(
|
|
1955
|
+
`MATCH (a:Approval {id: $id, companyId: $cid})-[:FOR_TASK]->(t:Task)
|
|
1956
|
+
RETURN t.id AS taskId, t.assigneeAgentId AS agentId`,
|
|
1957
|
+
{ id: approvalId, cid }
|
|
1958
|
+
);
|
|
1959
|
+
const taskRow = parseRows(taskRows)[0];
|
|
1960
|
+
if (taskRow && taskRow.taskId && taskRow.agentId) {
|
|
1961
|
+
await mgQuery(
|
|
1962
|
+
`MERGE (s:AgentReadySignal {id: 'signal:revise:' + $taskId})
|
|
1963
|
+
ON CREATE SET s.agentId = $agentId, s.companyId = $cid, s.status = 'pending',
|
|
1964
|
+
s.origin = 'approval_revision', s.taskId = $taskId,
|
|
1965
|
+
s.approvalId = $approvalId, s.createdAt = localdatetime()
|
|
1966
|
+
ON MATCH SET s.status = 'pending', s.approvalId = $approvalId`,
|
|
1967
|
+
{ taskId: taskRow.taskId, agentId: taskRow.agentId, cid, approvalId }
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
} catch (sigErr) {
|
|
1971
|
+
log('warn', 'B-19: AgentReadySignal creation failed after revision', { approvalId, err: sigErr.message });
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1824
1975
|
jsonResponse(res, 200, { revised: true, approvalId });
|
|
1825
1976
|
} catch (err) {
|
|
1826
1977
|
jsonResponse(res, 500, { error: { code: 'UPDATE_FAILED', message: `Revise failed: ${err.message}` } });
|
|
@@ -2674,6 +2825,10 @@ async function handleGetRoutineTriggers(req, res, ctx, routineId) {
|
|
|
2674
2825
|
|
|
2675
2826
|
async function handleCreateRoutineTrigger(req, res, ctx, routineId) {
|
|
2676
2827
|
const { mgQuery } = ctx;
|
|
2828
|
+
// HIGH-2 fix: guard against Memgraph unavailability
|
|
2829
|
+
if (!mgQuery) { return jsonResponse(res, 503, { error: 'Memgraph not connected' }); }
|
|
2830
|
+
// CRIT-2 fix: declare cid so broadcast() does not throw ReferenceError
|
|
2831
|
+
const cid = ctx.cid;
|
|
2677
2832
|
const body = await parseBody(req);
|
|
2678
2833
|
const kind = body.kind;
|
|
2679
2834
|
if (!kind || !['schedule', 'webhook', 'api'].includes(kind)) {
|
|
@@ -2698,7 +2853,7 @@ async function handleCreateRoutineTrigger(req, res, ctx, routineId) {
|
|
|
2698
2853
|
{
|
|
2699
2854
|
triggerId,
|
|
2700
2855
|
routineId,
|
|
2701
|
-
cid
|
|
2856
|
+
cid,
|
|
2702
2857
|
kind,
|
|
2703
2858
|
publicId,
|
|
2704
2859
|
config: JSON.stringify(body.config || {}),
|
|
@@ -2900,13 +3055,17 @@ async function handleCreateRoutine(req, res, ctx) {
|
|
|
2900
3055
|
|
|
2901
3056
|
async function handlePatchRoutine(req, res, ctx, routineId) {
|
|
2902
3057
|
const { mgQuery } = ctx;
|
|
3058
|
+
// HIGH-2 fix: guard against Memgraph unavailability
|
|
3059
|
+
if (!mgQuery) { return jsonResponse(res, 503, { error: 'Memgraph not connected' }); }
|
|
3060
|
+
// CRIT-2 fix: declare cid so broadcast() does not throw ReferenceError
|
|
3061
|
+
const cid = ctx.cid;
|
|
2903
3062
|
const body = await parseBody(req);
|
|
2904
3063
|
|
|
2905
3064
|
try {
|
|
2906
|
-
// Get current routine
|
|
3065
|
+
// Get current routine — CRIT-5 fix: use r.name (not r.title) consistently
|
|
2907
3066
|
const existing = await mgQuery(
|
|
2908
|
-
`MATCH (r:Routine {id: $routineId, companyId: $cid}) RETURN r.
|
|
2909
|
-
{ routineId, cid
|
|
3067
|
+
`MATCH (r:Routine {id: $routineId, companyId: $cid}) RETURN r.name AS name, r.config AS config`,
|
|
3068
|
+
{ routineId, cid }
|
|
2910
3069
|
);
|
|
2911
3070
|
const rows = parseRows(existing);
|
|
2912
3071
|
if (rows.length === 0) return jsonResponse(res, 404, { error: 'Routine not found' });
|
|
@@ -2914,48 +3073,69 @@ async function handlePatchRoutine(req, res, ctx, routineId) {
|
|
|
2914
3073
|
// Count existing revisions to determine revision number
|
|
2915
3074
|
const revCount = await mgQuery(
|
|
2916
3075
|
`MATCH (rv:RoutineRevision {routineId: $routineId, companyId: $cid}) RETURN count(rv) AS cnt`,
|
|
2917
|
-
{ routineId, cid
|
|
3076
|
+
{ routineId, cid }
|
|
2918
3077
|
);
|
|
2919
3078
|
const revRows = parseRows(revCount);
|
|
2920
3079
|
const revisionNumber = (revRows[0]?.[0] || 0) + 1;
|
|
2921
3080
|
|
|
2922
|
-
// Create revision
|
|
2923
|
-
const revisionId = `revision:${crypto.randomUUID()}`;
|
|
2924
|
-
await mgQuery(
|
|
2925
|
-
`CREATE (rv:RoutineRevision {
|
|
2926
|
-
id: $revisionId,
|
|
2927
|
-
routineId: $routineId,
|
|
2928
|
-
companyId: $cid,
|
|
2929
|
-
revisionNumber: $revisionNumber,
|
|
2930
|
-
config: $config,
|
|
2931
|
-
createdAt: datetime()
|
|
2932
|
-
})`,
|
|
2933
|
-
{
|
|
2934
|
-
revisionId,
|
|
2935
|
-
routineId,
|
|
2936
|
-
cid: ctx.cid,
|
|
2937
|
-
revisionNumber,
|
|
2938
|
-
config: JSON.stringify(body)
|
|
2939
|
-
}
|
|
2940
|
-
);
|
|
2941
|
-
|
|
2942
3081
|
// Update routine with new values
|
|
3082
|
+
// CRIT-3 fix: handle body.status (for soft-delete via 'inactive' + other status changes)
|
|
3083
|
+
// CRIT-5 fix: use r.name not r.title
|
|
2943
3084
|
const sets = [];
|
|
2944
|
-
const params = { routineId, cid
|
|
2945
|
-
if (body.
|
|
2946
|
-
if (body.
|
|
3085
|
+
const params = { routineId, cid };
|
|
3086
|
+
if (body.name) { sets.push('r.name = $name'); params.name = String(body.name); }
|
|
3087
|
+
if (body.title) { sets.push('r.name = $name'); params.name = String(body.title); } // legacy alias
|
|
3088
|
+
if (body.status) { sets.push('r.status = $status'); params.status = String(body.status); }
|
|
3089
|
+
if (body.catchUpPolicy) { sets.push('r.catchUpPolicy = $catchUpPolicy'); params.catchUpPolicy = body.catchUpPolicy; }
|
|
2947
3090
|
if (body.concurrencyPolicy) { sets.push('r.concurrencyPolicy = $concurrencyPolicy'); params.concurrencyPolicy = body.concurrencyPolicy; }
|
|
2948
|
-
if (body.
|
|
3091
|
+
if (body.cronExpr) { sets.push('r.cronExpr = $cronExpr'); params.cronExpr = body.cronExpr; }
|
|
3092
|
+
if (body.config) { sets.push('r.config = $config'); params.config = JSON.stringify(body.config); }
|
|
3093
|
+
sets.push('r.updatedAt = datetime()');
|
|
3094
|
+
|
|
3095
|
+
// MED-3 fix: only create a revision if there are actual field changes (not on no-op patches)
|
|
3096
|
+
const hasChanges = sets.length > 1; // always has updatedAt, so check > 1
|
|
3097
|
+
if (hasChanges) {
|
|
3098
|
+
const revisionId = `revision:${crypto.randomUUID()}`;
|
|
3099
|
+
await mgQuery(
|
|
3100
|
+
`CREATE (rv:RoutineRevision {
|
|
3101
|
+
id: $revisionId,
|
|
3102
|
+
routineId: $routineId,
|
|
3103
|
+
companyId: $cid,
|
|
3104
|
+
revisionNumber: $revisionNumber,
|
|
3105
|
+
config: $config,
|
|
3106
|
+
createdAt: datetime()
|
|
3107
|
+
})`,
|
|
3108
|
+
{
|
|
3109
|
+
revisionId,
|
|
3110
|
+
routineId,
|
|
3111
|
+
cid,
|
|
3112
|
+
revisionNumber,
|
|
3113
|
+
config: JSON.stringify(body)
|
|
3114
|
+
}
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
2949
3117
|
|
|
2950
|
-
if (sets.length >
|
|
3118
|
+
if (sets.length > 1) {
|
|
2951
3119
|
await mgQuery(
|
|
2952
3120
|
`MATCH (r:Routine {id: $routineId, companyId: $cid}) SET ${sets.join(', ')}`,
|
|
2953
3121
|
params
|
|
2954
3122
|
);
|
|
2955
3123
|
}
|
|
2956
3124
|
|
|
2957
|
-
|
|
2958
|
-
|
|
3125
|
+
// HIGH-3 fix: SQLite write-through after successful Memgraph update
|
|
3126
|
+
if (sets.length > 1 && hboStore?.updateRoutine) {
|
|
3127
|
+
const storeUpdate = {};
|
|
3128
|
+
if (body.name || body.title) storeUpdate.name = body.name ?? body.title;
|
|
3129
|
+
if (body.status) storeUpdate.status = body.status;
|
|
3130
|
+
if (body.catchUpPolicy) storeUpdate.catch_up_policy = body.catchUpPolicy;
|
|
3131
|
+
if (body.concurrencyPolicy) storeUpdate.concurrency_policy = body.concurrencyPolicy;
|
|
3132
|
+
if (body.cronExpr) storeUpdate.cron_expr = body.cronExpr;
|
|
3133
|
+
try { hboStore.updateRoutine(routineId, cid, storeUpdate); }
|
|
3134
|
+
catch (storeErr) { log('error', 'hboStore.updateRoutine failed', { err: storeErr.message, routineId }); }
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
broadcast({ type: 'routine.updated', routineId, revisionNumber: hasChanges ? revisionNumber : null, companyId: cid });
|
|
3138
|
+
return jsonResponse(res, 200, { updated: true, routineId, revisionNumber: hasChanges ? revisionNumber : null });
|
|
2959
3139
|
} catch (err) {
|
|
2960
3140
|
return jsonResponse(res, 500, { error: err.message });
|
|
2961
3141
|
}
|
|
@@ -2981,6 +3161,12 @@ async function handleGetRoutineRevisions(req, res, ctx, routineId) {
|
|
|
2981
3161
|
|
|
2982
3162
|
async function handleGetRoutineRuns(req, res, ctx, routineId) {
|
|
2983
3163
|
const { mgQuery } = ctx;
|
|
3164
|
+
const cid = ctx.cid;
|
|
3165
|
+
// MED-5 fix: SQL parity — fallback to SQLite when Memgraph unavailable
|
|
3166
|
+
if (!mgQuery) {
|
|
3167
|
+
// No RoutineRun SQLite table exists yet — return empty gracefully
|
|
3168
|
+
return jsonResponse(res, 200, { runs: [], _source: 'sqlite' });
|
|
3169
|
+
}
|
|
2984
3170
|
try {
|
|
2985
3171
|
const result = await mgQuery(
|
|
2986
3172
|
`MATCH (rr:RoutineRun {routineId: $routineId, companyId: $cid})
|
|
@@ -2988,7 +3174,7 @@ async function handleGetRoutineRuns(req, res, ctx, routineId) {
|
|
|
2988
3174
|
rr.startedAt AS startedAt, rr.completedAt AS completedAt
|
|
2989
3175
|
ORDER BY rr.startedAt DESC
|
|
2990
3176
|
LIMIT toInteger($lim)`,
|
|
2991
|
-
{ routineId, cid
|
|
3177
|
+
{ routineId, cid, lim: 50 }
|
|
2992
3178
|
);
|
|
2993
3179
|
const rows = parseRows(result);
|
|
2994
3180
|
const keys = ['id', 'status', 'taskId', 'startedAt', 'completedAt'];
|
|
@@ -3348,9 +3534,14 @@ const approvalsRoute = require('./routes/approvals')({
|
|
|
3348
3534
|
|
|
3349
3535
|
const agentsRoute = require('./routes/agents')({
|
|
3350
3536
|
handleGetAgents,
|
|
3537
|
+
handleGetAgent, // P7-A1: wire single-agent GET (was dead code — not passed before)
|
|
3538
|
+
handleGetAgentRuns, // P7-A2: HeartbeatRun history
|
|
3351
3539
|
handleSyncSkills,
|
|
3352
3540
|
handleApproveAgent,
|
|
3353
3541
|
handleTerminateAgent,
|
|
3542
|
+
handlePauseAgent, // Bug fix: was missing, caused TypeError in agents.js
|
|
3543
|
+
handleResumeAgent, // Bug fix: was missing
|
|
3544
|
+
handlePauseAll: null, // pause-all is handled in hbo.js; stub null to prevent TypeError
|
|
3354
3545
|
});
|
|
3355
3546
|
|
|
3356
3547
|
const skillsRoute = require('./routes/skills')({
|
|
@@ -3405,7 +3596,7 @@ const inboxRoute = require('./routes/inbox')({ broadcast });
|
|
|
3405
3596
|
const queueRoute = require('./routes/queue')({ broadcast });
|
|
3406
3597
|
const { handleDraftsRoute } = require('./routes/drafts');
|
|
3407
3598
|
const goalsRoute = require('./routes/goals');
|
|
3408
|
-
|
|
3599
|
+
let hboRoute = require('./routes/hbo')({ broadcast });
|
|
3409
3600
|
const emailTriageRoute = require('./routes/email-triage')();
|
|
3410
3601
|
const andonRoute = require('./routes/andon')({ handleGetAndonBoard });
|
|
3411
3602
|
const wizardRoute = require('./routes/wizard')({});
|
|
@@ -3435,6 +3626,9 @@ let mandalaRoute;
|
|
|
3435
3626
|
try { mandalaRoute = require('./routes/mandala').mandalaRoute; } catch (_) { mandalaRoute = () => false; }
|
|
3436
3627
|
let personRoute;
|
|
3437
3628
|
try { personRoute = require('./routes/person')({ handleGetPersonProfile, handleGetPersonByEmail, handleGetPersonEpisodes }); } catch (_) { personRoute = () => false; }
|
|
3629
|
+
// P5-S5c: CRM dual-write route — accepts POST /api/crm/contacts from desktop crmSyncService
|
|
3630
|
+
let crmRoute;
|
|
3631
|
+
try { crmRoute = require('./routes/crm')({ parseBody, jsonResponse }); } catch (_) { crmRoute = () => false; }
|
|
3438
3632
|
|
|
3439
3633
|
|
|
3440
3634
|
// ── WS-nonce store (single-use, 60-second TTL) ───────────────────────────────
|
|
@@ -3499,6 +3693,25 @@ async function route(req, res, ctx) {
|
|
|
3499
3693
|
return;
|
|
3500
3694
|
}
|
|
3501
3695
|
|
|
3696
|
+
// SP3: Read receipt tracking pixel — must be before auth middleware, no auth required
|
|
3697
|
+
if (req.method === 'GET' && /^\/t\/[a-zA-Z0-9_-]+\.png$/.test(req.url || '')) {
|
|
3698
|
+
const trackingId = (req.url || '').slice(3, -4); // strip leading /t/ and trailing .png
|
|
3699
|
+
const gif1x1 = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
|
|
3700
|
+
res.writeHead(200, {
|
|
3701
|
+
'Content-Type': 'image/gif',
|
|
3702
|
+
'Content-Length': gif1x1.length,
|
|
3703
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
3704
|
+
'Pragma': 'no-cache',
|
|
3705
|
+
});
|
|
3706
|
+
res.end(gif1x1);
|
|
3707
|
+
// Fire-and-forget: log open event to Memgraph
|
|
3708
|
+
try {
|
|
3709
|
+
const { rawWrite: rw } = require('../lib/safe-memgraph.js');
|
|
3710
|
+
rw(`MERGE (o:OpenEvent {trackingId: $id}) ON CREATE SET o.count = 1, o.firstOpenedAt = datetime() ON MATCH SET o.count = o.count + 1, o.lastOpenedAt = datetime()`, { id: trackingId }).catch(() => {});
|
|
3711
|
+
} catch {}
|
|
3712
|
+
return;
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3502
3715
|
// ── Bearer token auth ────────────────────────────────────────────────────
|
|
3503
3716
|
// Public routes that skip auth
|
|
3504
3717
|
const PUBLIC_PATHS = new Set(['/api/health', '/api/auth/ws-nonce', '/login', '/signup', '/api/auth/signup', '/api/auth/login', '/api/auth/logout']);
|
|
@@ -3875,6 +4088,7 @@ async function route(req, res, ctx) {
|
|
|
3875
4088
|
if (await suggestionsRoute(req, res, reqCtx, pathname, method)) return;
|
|
3876
4089
|
if (await mandalaRoute(req, res, reqCtx, pathname, method)) return;
|
|
3877
4090
|
if (await personRoute(req, res, reqCtx, pathname, method)) return;
|
|
4091
|
+
if (await crmRoute(req, res, reqCtx, pathname, method)) return;
|
|
3878
4092
|
|
|
3879
4093
|
// ── Phase 3: SystemAim endpoints ─────────────────────────────────────────
|
|
3880
4094
|
if (pathname === '/api/system-aim') {
|
|
@@ -4151,11 +4365,26 @@ async function handleGetDepartmentPageRoute(req, res, ctx, department) {
|
|
|
4151
4365
|
if (narrativeStr && lastGenStr) {
|
|
4152
4366
|
const lastGen = new Date(String(lastGenStr)).getTime();
|
|
4153
4367
|
if (!isNaN(lastGen) && Date.now() - lastGen < CACHE_TTL_MS) {
|
|
4154
|
-
// Cache hit — return as-is
|
|
4368
|
+
// Cache hit — return as-is, also include tasks created from this page
|
|
4155
4369
|
let narrative;
|
|
4156
4370
|
try { narrative = JSON.parse(String(narrativeStr)); }
|
|
4157
4371
|
catch (_) { narrative = { governingThought: String(narrativeStr) }; }
|
|
4158
|
-
|
|
4372
|
+
|
|
4373
|
+
// P8E-01: Include tasks created from this department page's decisionsStructured
|
|
4374
|
+
const tasksResult = await mg(
|
|
4375
|
+
`MATCH (dp:DepartmentPage {companyId: $cid, department: $dept})
|
|
4376
|
+
WHERE dp.narrative IS NOT NULL
|
|
4377
|
+
OPTIONAL MATCH (dp)-[:CREATED_TASK]->(t:Task)
|
|
4378
|
+
RETURN collect({id: t.id, title: t.title, status: t.status, originKind: t.originKind}) AS tasks
|
|
4379
|
+
ORDER BY dp.lastGeneratedAt DESC LIMIT 1`,
|
|
4380
|
+
{ cid, dept: department }
|
|
4381
|
+
).catch(() => null);
|
|
4382
|
+
const tasksRow = tasksResult && tasksResult.rows ? tasksResult.rows[0] : null;
|
|
4383
|
+
const tasks = tasksRow
|
|
4384
|
+
? (Array.isArray(tasksRow) ? (tasksRow[0] || []) : (tasksRow.tasks || []))
|
|
4385
|
+
: [];
|
|
4386
|
+
|
|
4387
|
+
jsonResponse(res, 200, { department, narrative, generatedAt: lastGenStr, cached: true, tasks });
|
|
4159
4388
|
return;
|
|
4160
4389
|
}
|
|
4161
4390
|
}
|
|
@@ -4407,6 +4636,21 @@ function startApi(mgQuery, config = {}, state = {}) {
|
|
|
4407
4636
|
deptRoute = require('./routes/dept')({ mgQuery, broadcast: state.broadcast ?? (() => {}) });
|
|
4408
4637
|
}
|
|
4409
4638
|
|
|
4639
|
+
// Reinitialize hboRoute with triggerGoalDecompose hook so POST /api/hbo/goals immediately
|
|
4640
|
+
// fires tickGoalDecompose instead of waiting up to 150s for the next 5th-tick.
|
|
4641
|
+
// state.daemon._mods is the per-company module registry built by buildForCompany().
|
|
4642
|
+
if (state.daemon && typeof state.broadcast === 'function') {
|
|
4643
|
+
const _triggerFn = (cid) => {
|
|
4644
|
+
try {
|
|
4645
|
+
const mods = state.daemon._mods && state.daemon._mods.get ? state.daemon._mods.get(cid) : (state.daemon._mods?.[cid]);
|
|
4646
|
+
if (mods && mods.hboBridge && typeof mods.hboBridge.tickGoalDecompose === 'function') {
|
|
4647
|
+
mods.hboBridge.tickGoalDecompose({ fromGoalCreate: true }).catch(() => {});
|
|
4648
|
+
}
|
|
4649
|
+
} catch (_) {}
|
|
4650
|
+
};
|
|
4651
|
+
hboRoute = require('./routes/hbo')({ broadcast: state.broadcast, mgQuery, triggerGoalDecompose: _triggerFn });
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4410
4654
|
// HED routes — requires mgQuery for HEDEngine
|
|
4411
4655
|
try {
|
|
4412
4656
|
const { createHedRoutes } = require('./routes/hed');
|
|
@@ -4439,6 +4683,7 @@ function startApi(mgQuery, config = {}, state = {}) {
|
|
|
4439
4683
|
draining: false,
|
|
4440
4684
|
apiToken: config.apiToken || null, // null = no auth (backward compat)
|
|
4441
4685
|
daemon: state.daemon ?? null, // daemon instance ref — used by wizard route for onCompanyReady
|
|
4686
|
+
activityLogger: state.activityLogger ?? null, // H-01: for recording approval events
|
|
4442
4687
|
modules: [
|
|
4443
4688
|
'RoutineEvaluator', 'BudgetEnforcer', 'LivenessWatchdog',
|
|
4444
4689
|
'AgentDispatcher', 'ActivityLogger', 'TaskCompletionWatchdog',
|