@cgh567/agent 2.4.3 → 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.
- 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-wrapper.sh +4 -1
- 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 +310 -58
- package/daemon/helios-company-daemon.js +179 -53
- 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 +319 -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 +466 -10
- 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' });
|
|
@@ -439,19 +441,24 @@ async function handleCreateTask(req, res, ctx) {
|
|
|
439
441
|
const resolvedPriority = Number.isFinite(Number(priority)) ? Number(priority) : 2;
|
|
440
442
|
|
|
441
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
|
+
})`;
|
|
442
460
|
await mgQuery(
|
|
443
|
-
|
|
444
|
-
id: $id,
|
|
445
|
-
companyId: $cid,
|
|
446
|
-
title: $title,
|
|
447
|
-
status: 'todo',
|
|
448
|
-
priority: toInteger($priority),
|
|
449
|
-
assigneeAgentId: $assigneeAgentId,
|
|
450
|
-
body: $body,
|
|
451
|
-
executionPolicyJson: $policyJson,
|
|
452
|
-
executionStateJson: $stateJson,
|
|
453
|
-
createdAt: datetime()
|
|
454
|
-
})`,
|
|
461
|
+
_taskCypher,
|
|
455
462
|
{
|
|
456
463
|
id: taskId,
|
|
457
464
|
cid,
|
|
@@ -461,8 +468,26 @@ async function handleCreateTask(req, res, ctx) {
|
|
|
461
468
|
body: taskBody ? String(taskBody) : null,
|
|
462
469
|
policyJson: validatedPolicyJson,
|
|
463
470
|
stateJson: initialStateJson,
|
|
471
|
+
sourceType: sourceType ? String(sourceType) : null,
|
|
472
|
+
sourceId: sourceId ? String(sourceId) : null,
|
|
473
|
+
originKind: originKind ? String(originKind) : null,
|
|
474
|
+
projectId: projectId ? String(projectId) : null,
|
|
464
475
|
}
|
|
465
|
-
);
|
|
476
|
+
));
|
|
477
|
+
|
|
478
|
+
// Phase 2.5-B: OI-P3 fix — write BELONGS_TO_PROJECT edge when projectId is provided
|
|
479
|
+
if (projectId) {
|
|
480
|
+
try {
|
|
481
|
+
await mgQuery(
|
|
482
|
+
`MATCH (t:Task {id: $taskId}), (p:HeliosProject {id: $projectId, companyId: $cid})
|
|
483
|
+
MERGE (t)-[:BELONGS_TO_PROJECT]->(p)`,
|
|
484
|
+
{ taskId, projectId: String(projectId), cid }
|
|
485
|
+
);
|
|
486
|
+
} catch (_edgeErr) {
|
|
487
|
+
// Edge write is best-effort — task is already created; log but don't fail
|
|
488
|
+
log('warn', 'BELONGS_TO_PROJECT edge write failed (OI-P3)', { taskId, projectId });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
466
491
|
|
|
467
492
|
mirrorHboStore('task.create', () => hboStore.createTask?.({
|
|
468
493
|
id: taskId,
|
|
@@ -479,6 +504,17 @@ async function handleCreateTask(req, res, ctx) {
|
|
|
479
504
|
|
|
480
505
|
broadcast({ type: 'task.created', taskId, status: 'todo', companyId: cid });
|
|
481
506
|
jsonResponse(res, 201, { task: { id: taskId, title, status: 'todo', priority: resolvedPriority, assigneeAgentId, executionPolicyJson: validatedPolicyJson } });
|
|
507
|
+
|
|
508
|
+
// P_EXPLAIN-01: fire-and-forget interpretation explanation background job
|
|
509
|
+
const { rawWrite: _interpRawWrite } = require('../lib/safe-memgraph.js');
|
|
510
|
+
setImmediate(() => {
|
|
511
|
+
try {
|
|
512
|
+
const { createInterpretationExplanation } = require('./lib/interpretation-engine.js');
|
|
513
|
+
createInterpretationExplanation(taskId, cid, _interpRawWrite).catch(e =>
|
|
514
|
+
log('warn', 'P_EXPLAIN-01 failed', { e: e.message })
|
|
515
|
+
);
|
|
516
|
+
} catch (_) { /* interpretation is non-blocking */ }
|
|
517
|
+
});
|
|
482
518
|
} catch (err) {
|
|
483
519
|
jsonResponse(res, 500, { error: `Create failed: ${err.message}` });
|
|
484
520
|
}
|
|
@@ -491,7 +527,9 @@ async function handleUpdateTask(req, res, ctx, taskId) {
|
|
|
491
527
|
const cid = ctx.cid;
|
|
492
528
|
|
|
493
529
|
const body = await parseBody(req);
|
|
494
|
-
|
|
530
|
+
// P6-06a: workspacePath added — desktop sets this after acquiring a SessionWorktree
|
|
531
|
+
// for isolated execution; daemon reads it into adapterContext.workspacePath
|
|
532
|
+
const { status, priority, workspacePath, blockedReason, clearBlocked, hillPosition } = body;
|
|
495
533
|
|
|
496
534
|
// P4-01: 'in_review' added — allows execution policy gate to hold a task for review
|
|
497
535
|
const VALID_STATUSES = new Set(['todo', 'in_progress', 'done', 'failed', 'cancelled', 'blocked', 'escalated', 'in_review']);
|
|
@@ -504,8 +542,8 @@ async function handleUpdateTask(req, res, ctx, taskId) {
|
|
|
504
542
|
return;
|
|
505
543
|
}
|
|
506
544
|
|
|
507
|
-
if (status === undefined && priority === undefined) {
|
|
508
|
-
jsonResponse(res, 400, { error: 'Provide at least one field to update: status, priority' });
|
|
545
|
+
if (status === undefined && priority === undefined && workspacePath === undefined && blockedReason === undefined && clearBlocked === undefined && hillPosition === undefined) {
|
|
546
|
+
jsonResponse(res, 400, { error: 'Provide at least one field to update: status, priority, workspacePath, blockedReason, clearBlocked, hillPosition' });
|
|
509
547
|
return;
|
|
510
548
|
}
|
|
511
549
|
|
|
@@ -767,6 +805,17 @@ async function handleUpdateTask(req, res, ctx, taskId) {
|
|
|
767
805
|
const params = { id: taskId, cid };
|
|
768
806
|
if (status !== undefined) { setClauses.push('t.status = $status'); params.status = status; }
|
|
769
807
|
if (priority !== undefined) { setClauses.push('t.priority = toInteger($priority)'); params.priority = Number(priority); }
|
|
808
|
+
// P6-06a: persist workspacePath on Task node so daemon can read it into adapterContext
|
|
809
|
+
if (workspacePath !== undefined) { setClauses.push('t.workspacePath = $workspacePath'); params.workspacePath = workspacePath ? String(workspacePath) : null; }
|
|
810
|
+
// P1-G: blockedReason + blockedAt — clearBlocked=true nulls both fields and resets status to todo.
|
|
811
|
+
// COALESCE preserves existing reason if not re-set; blockedAt only set once on initial block.
|
|
812
|
+
if (blockedReason !== undefined || clearBlocked !== undefined) {
|
|
813
|
+
params.clearBlocked = clearBlocked === true;
|
|
814
|
+
params.blockedReason = (blockedReason !== undefined && blockedReason !== null) ? String(blockedReason) : null;
|
|
815
|
+
setClauses.push('t.blockedReason = CASE WHEN $clearBlocked = true THEN null ELSE COALESCE($blockedReason, t.blockedReason) END');
|
|
816
|
+
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');
|
|
817
|
+
setClauses.push('t.status = CASE WHEN $clearBlocked = true AND t.status = \'blocked\' THEN \'todo\' ELSE t.status END');
|
|
818
|
+
}
|
|
770
819
|
setClauses.push('t.updatedAt = datetime()');
|
|
771
820
|
|
|
772
821
|
// B-09: Single atomic check+update — no TOCTOU window between existence check and SET
|
|
@@ -779,9 +828,25 @@ async function handleUpdateTask(req, res, ctx, taskId) {
|
|
|
779
828
|
return;
|
|
780
829
|
}
|
|
781
830
|
|
|
831
|
+
// T4-02: HillChart write — track task work-cycle position for hill chart visualisation
|
|
832
|
+
if (hillPosition !== undefined && Number.isFinite(Number(hillPosition))) {
|
|
833
|
+
const hPos = Math.max(0, Math.min(1, Number(hillPosition)));
|
|
834
|
+
const hPhase = hPos < 0.5 ? 'uphill' : hPos < 1.0 ? 'downhill' : 'complete';
|
|
835
|
+
mgQuery(
|
|
836
|
+
`MERGE (h:HillChart {id: 'hill:' + $taskId})
|
|
837
|
+
SET h.taskId = $taskId, h.companyId = $cid,
|
|
838
|
+
h.position = $pos, h.phase = $phase, h.updatedAt = datetime()
|
|
839
|
+
WITH h
|
|
840
|
+
MATCH (t:Task {id: $taskId, companyId: $cid})
|
|
841
|
+
MERGE (t)-[:HAS_HILL_CHART]->(h)`,
|
|
842
|
+
{ taskId, cid, pos: hPos, phase: hPhase }
|
|
843
|
+
).catch(e => log('warn', `HillChart MERGE failed for task ${taskId}: ${e.message}`));
|
|
844
|
+
}
|
|
845
|
+
|
|
782
846
|
const storeUpdate = {};
|
|
783
847
|
if (status !== undefined) storeUpdate.status = status;
|
|
784
848
|
if (priority !== undefined) storeUpdate.priority = Number(priority);
|
|
849
|
+
if (workspacePath !== undefined) storeUpdate.workspacePath = workspacePath ? String(workspacePath) : null;
|
|
785
850
|
mirrorHboStore('task.update', () => hboStore.updateTask?.(taskId, cid, storeUpdate));
|
|
786
851
|
|
|
787
852
|
broadcast({ type: 'task.updated', taskId, ...(status !== undefined ? { status } : {}), companyId: cid });
|
|
@@ -907,6 +972,7 @@ async function handleGetApprovals(req, res, ctx) {
|
|
|
907
972
|
a.status AS status, a.requestedBy AS requestedBy,
|
|
908
973
|
a.agentId AS agentId, a.taskId AS taskId,
|
|
909
974
|
a.createdAt AS createdAt, a.followUpTaskCreated AS followUpTaskCreated,
|
|
975
|
+
a.followUpTaskId AS followUpTaskId,
|
|
910
976
|
a.expiresAt AS expiresAt,
|
|
911
977
|
a.kaizenProposalId AS kaizenProposalId,
|
|
912
978
|
a.description AS description,
|
|
@@ -954,7 +1020,7 @@ async function handleGetApprovals(req, res, ctx) {
|
|
|
954
1020
|
const rows = parseRows(result);
|
|
955
1021
|
const keys = [
|
|
956
1022
|
'id', 'type', 'title', 'status', 'requestedBy', 'agentId', 'taskId',
|
|
957
|
-
'createdAt', 'followUpTaskCreated', 'expiresAt', 'kaizenProposalId',
|
|
1023
|
+
'createdAt', 'followUpTaskCreated', 'followUpTaskId', 'expiresAt', 'kaizenProposalId',
|
|
958
1024
|
'description', 'payload', 'body', 'question', 'pillarId',
|
|
959
1025
|
'sourceAgent', 'resolvedAt', 'department', 'templateKey',
|
|
960
1026
|
'inferredAnswer', 'defaultAnswer',
|
|
@@ -1089,6 +1155,8 @@ async function handleApproveApproval(req, res, ctx, approvalId) {
|
|
|
1089
1155
|
}
|
|
1090
1156
|
|
|
1091
1157
|
broadcast({ type: 'approval.approved', approvalId, companyId: cid });
|
|
1158
|
+
// H-01: record approval.approve in ActivityLogger
|
|
1159
|
+
ctx.activityLogger?.record({ action: 'approval.approve', actor: cid, entityId: approvalId, companyId: cid });
|
|
1092
1160
|
jsonResponse(res, 200, { approved: true, approvalId });
|
|
1093
1161
|
} catch (err) {
|
|
1094
1162
|
jsonResponse(res, 500, { error: `Approve failed: ${err.message}` });
|
|
@@ -1118,6 +1186,8 @@ async function handleRejectApproval(req, res, ctx, approvalId) {
|
|
|
1118
1186
|
}
|
|
1119
1187
|
mirrorHboStore('approval.reject', () => hboStore.updateApproval?.(approvalId, cid, { status: 'rejected', rejectReason: reason, rejectedAt: Date.now() }));
|
|
1120
1188
|
broadcast({ type: 'approval.rejected', approvalId, reason, companyId: cid });
|
|
1189
|
+
// H-01: record approval.reject in ActivityLogger
|
|
1190
|
+
ctx.activityLogger?.record({ action: 'approval.reject', actor: cid, entityId: approvalId, companyId: cid });
|
|
1121
1191
|
jsonResponse(res, 200, { rejected: true, approvalId, reason });
|
|
1122
1192
|
} catch (err) {
|
|
1123
1193
|
jsonResponse(res, 500, { error: `Reject failed: ${err.message}` });
|
|
@@ -1396,16 +1466,39 @@ async function handleGetAgents(req, res, ctx) {
|
|
|
1396
1466
|
|
|
1397
1467
|
async function handleGetAgent(req, res, ctx, agentId) {
|
|
1398
1468
|
const { mgQuery } = ctx;
|
|
1399
|
-
if (!mgQuery) { jsonResponse(res, 503, { error: 'Memgraph not connected' }); return; }
|
|
1400
1469
|
if (!agentId) { jsonResponse(res, 400, { error: 'Missing agent ID' }); return; }
|
|
1401
1470
|
const cid = ctx.cid;
|
|
1402
1471
|
|
|
1472
|
+
// P7-A1: SQL parity — fall back to SQLite when Memgraph unavailable
|
|
1473
|
+
if (!mgQuery) {
|
|
1474
|
+
try {
|
|
1475
|
+
const a = hboStore.getBusinessAgent ? hboStore.getBusinessAgent(agentId, cid) : null;
|
|
1476
|
+
if (!a) { jsonResponse(res, 404, { error: `Agent not found: ${agentId}` }); return; }
|
|
1477
|
+
// H-1 fix: also include recentTasks from SQLite so shape matches Memgraph path
|
|
1478
|
+
const sqliteTasks = hboStore.getTasksByCompanyStatus
|
|
1479
|
+
? hboStore.getTasksByCompanyStatus(cid, ['todo', 'in_progress', 'done']).filter(t => t.assigneeAgentId === agentId || t.assignee_agent_id === agentId).slice(0, 10)
|
|
1480
|
+
: [];
|
|
1481
|
+
a.recentTasks = sqliteTasks.map(t => ({ id: t.id, title: t.title ?? null, status: t.status ?? null, priority: t.priority ?? null }));
|
|
1482
|
+
return jsonResponse(res, 200, { agent: a, _source: 'sqlite' });
|
|
1483
|
+
} catch (storeErr) {
|
|
1484
|
+
return jsonResponse(res, 503, { error: 'Agent unavailable: Memgraph not connected', agent: null });
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1403
1488
|
try {
|
|
1404
1489
|
const [agentResult, tasksResult] = await Promise.all([
|
|
1405
1490
|
mgQuery(
|
|
1491
|
+
// P7-A1: extended RETURN to include all required fields for agent detail page
|
|
1406
1492
|
`MATCH (a:BusinessAgent {id: $id, companyId: $cid})
|
|
1407
|
-
RETURN a.id AS id, a.title AS title, a.status AS status,
|
|
1408
|
-
a.
|
|
1493
|
+
RETURN a.id AS id, a.title AS title, a.role AS role, a.status AS status,
|
|
1494
|
+
a.skill AS skill, a.adapter AS adapter,
|
|
1495
|
+
a.budgetMonthlyCents AS budgetMonthlyCents,
|
|
1496
|
+
a.heartbeatIntervalMs AS heartbeatIntervalMs,
|
|
1497
|
+
a.capabilities AS capabilities,
|
|
1498
|
+
a.defaultSkills AS defaultSkills,
|
|
1499
|
+
a.lastHeartbeatAt AS lastHeartbeatAt,
|
|
1500
|
+
a.pauseReason AS pauseReason,
|
|
1501
|
+
a.companyId AS companyId`,
|
|
1409
1502
|
{ id: agentId, cid }
|
|
1410
1503
|
),
|
|
1411
1504
|
mgQuery(
|
|
@@ -1422,7 +1515,9 @@ async function handleGetAgent(req, res, ctx, agentId) {
|
|
|
1422
1515
|
return;
|
|
1423
1516
|
}
|
|
1424
1517
|
|
|
1425
|
-
const agentKeys = ['id', 'title', 'status', '
|
|
1518
|
+
const agentKeys = ['id', 'title', 'role', 'status', 'skill', 'adapter',
|
|
1519
|
+
'budgetMonthlyCents', 'heartbeatIntervalMs', 'capabilities', 'defaultSkills',
|
|
1520
|
+
'lastHeartbeatAt', 'pauseReason', 'companyId'];
|
|
1426
1521
|
const taskKeys = ['id', 'title', 'status', 'priority'];
|
|
1427
1522
|
const agent = rowToObj(agentRows[0], agentKeys);
|
|
1428
1523
|
agent.recentTasks = parseRows(tasksResult).map(r => rowToObj(r, taskKeys));
|
|
@@ -1433,6 +1528,37 @@ async function handleGetAgent(req, res, ctx, agentId) {
|
|
|
1433
1528
|
}
|
|
1434
1529
|
}
|
|
1435
1530
|
|
|
1531
|
+
// P7-A2: GET /api/agents/:id/runs — HeartbeatRun history for an agent
|
|
1532
|
+
async function handleGetAgentRuns(req, res, ctx, agentId) {
|
|
1533
|
+
const { mgQuery } = ctx;
|
|
1534
|
+
if (!agentId) { jsonResponse(res, 400, { error: 'Missing agent ID' }); return; }
|
|
1535
|
+
const cid = ctx.cid;
|
|
1536
|
+
|
|
1537
|
+
// SQL parity: HeartbeatRun nodes are Memgraph-only (no SQLite mirror yet)
|
|
1538
|
+
if (!mgQuery) {
|
|
1539
|
+
return jsonResponse(res, 200, { runs: [], _source: 'sqlite' });
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
try {
|
|
1543
|
+
const result = await mgQuery(
|
|
1544
|
+
// H-2 fix: removed h.runId (field doesn't exist on HeartbeatRun nodes — they use h.id)
|
|
1545
|
+
// M-4 fix: use toString() for consistent datetime ordering (matches all other ORDER BY in codebase)
|
|
1546
|
+
`MATCH (h:HeartbeatRun {agentId: $agentId, companyId: $cid})
|
|
1547
|
+
RETURN h.id AS id, h.agentId AS agentId, h.companyId AS companyId,
|
|
1548
|
+
h.status AS status, h.startedAt AS startedAt, h.endedAt AS endedAt,
|
|
1549
|
+
h.taskId AS taskId
|
|
1550
|
+
ORDER BY toString(h.startedAt) DESC LIMIT toInteger(50)`,
|
|
1551
|
+
{ agentId, cid }
|
|
1552
|
+
);
|
|
1553
|
+
const rows = parseRows(result);
|
|
1554
|
+
const keys = ['id', 'agentId', 'companyId', 'status', 'startedAt', 'endedAt', 'taskId'];
|
|
1555
|
+
const runs = rows.map(r => rowToObj(r, keys));
|
|
1556
|
+
return jsonResponse(res, 200, { runs, count: runs.length });
|
|
1557
|
+
} catch (err) {
|
|
1558
|
+
return jsonResponse(res, 500, { error: `Query failed: ${err.message}` });
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1436
1562
|
// ── Activity ──────────────────────────────────────────────────────────────────
|
|
1437
1563
|
|
|
1438
1564
|
async function handleGetActivity(req, res, ctx) {
|
|
@@ -1821,6 +1947,38 @@ async function handleReviseApproval(req, res, ctx, approvalId) {
|
|
|
1821
1947
|
return;
|
|
1822
1948
|
}
|
|
1823
1949
|
broadcast({ type: 'approval.revised', approvalId, companyId: cid });
|
|
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
|
+
|
|
1954
|
+
// B-19 fix: create AgentReadySignal so the assigned agent wakes and processes the revision.
|
|
1955
|
+
// Without this, the agent that owns the approval's task never re-evaluates after revision.
|
|
1956
|
+
setImmediate(async () => {
|
|
1957
|
+
try {
|
|
1958
|
+
const taskRows = await mgQuery(
|
|
1959
|
+
`MATCH (a:Approval {id: $id, companyId: $cid})-[:FOR_TASK]->(t:Task)
|
|
1960
|
+
RETURN t.id AS taskId, t.assigneeAgentId AS agentId`,
|
|
1961
|
+
{ id: approvalId, cid }
|
|
1962
|
+
);
|
|
1963
|
+
const taskRow = parseRows(taskRows)[0];
|
|
1964
|
+
if (taskRow && taskRow.taskId && taskRow.agentId) {
|
|
1965
|
+
await mgQuery(
|
|
1966
|
+
`MERGE (s:AgentReadySignal {id: 'signal:revise:' + $taskId})
|
|
1967
|
+
ON CREATE SET s.agentId = $agentId, s.companyId = $cid, s.status = 'pending',
|
|
1968
|
+
s.origin = 'approval_revision', s.taskId = $taskId,
|
|
1969
|
+
s.approvalId = $approvalId, s.createdAt = localdatetime()
|
|
1970
|
+
ON MATCH SET s.status = 'pending', s.approvalId = $approvalId`,
|
|
1971
|
+
{ taskId: taskRow.taskId, agentId: taskRow.agentId, cid, approvalId }
|
|
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 });
|
|
1976
|
+
}
|
|
1977
|
+
} catch (sigErr) {
|
|
1978
|
+
log('warn', 'B-19: AgentReadySignal creation failed after revision', { approvalId, err: sigErr.message });
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1824
1982
|
jsonResponse(res, 200, { revised: true, approvalId });
|
|
1825
1983
|
} catch (err) {
|
|
1826
1984
|
jsonResponse(res, 500, { error: { code: 'UPDATE_FAILED', message: `Revise failed: ${err.message}` } });
|
|
@@ -2674,6 +2832,10 @@ async function handleGetRoutineTriggers(req, res, ctx, routineId) {
|
|
|
2674
2832
|
|
|
2675
2833
|
async function handleCreateRoutineTrigger(req, res, ctx, routineId) {
|
|
2676
2834
|
const { mgQuery } = ctx;
|
|
2835
|
+
// HIGH-2 fix: guard against Memgraph unavailability
|
|
2836
|
+
if (!mgQuery) { return jsonResponse(res, 503, { error: 'Memgraph not connected' }); }
|
|
2837
|
+
// CRIT-2 fix: declare cid so broadcast() does not throw ReferenceError
|
|
2838
|
+
const cid = ctx.cid;
|
|
2677
2839
|
const body = await parseBody(req);
|
|
2678
2840
|
const kind = body.kind;
|
|
2679
2841
|
if (!kind || !['schedule', 'webhook', 'api'].includes(kind)) {
|
|
@@ -2698,7 +2860,7 @@ async function handleCreateRoutineTrigger(req, res, ctx, routineId) {
|
|
|
2698
2860
|
{
|
|
2699
2861
|
triggerId,
|
|
2700
2862
|
routineId,
|
|
2701
|
-
cid
|
|
2863
|
+
cid,
|
|
2702
2864
|
kind,
|
|
2703
2865
|
publicId,
|
|
2704
2866
|
config: JSON.stringify(body.config || {}),
|
|
@@ -2900,13 +3062,17 @@ async function handleCreateRoutine(req, res, ctx) {
|
|
|
2900
3062
|
|
|
2901
3063
|
async function handlePatchRoutine(req, res, ctx, routineId) {
|
|
2902
3064
|
const { mgQuery } = ctx;
|
|
3065
|
+
// HIGH-2 fix: guard against Memgraph unavailability
|
|
3066
|
+
if (!mgQuery) { return jsonResponse(res, 503, { error: 'Memgraph not connected' }); }
|
|
3067
|
+
// CRIT-2 fix: declare cid so broadcast() does not throw ReferenceError
|
|
3068
|
+
const cid = ctx.cid;
|
|
2903
3069
|
const body = await parseBody(req);
|
|
2904
3070
|
|
|
2905
3071
|
try {
|
|
2906
|
-
// Get current routine
|
|
3072
|
+
// Get current routine — CRIT-5 fix: use r.name (not r.title) consistently
|
|
2907
3073
|
const existing = await mgQuery(
|
|
2908
|
-
`MATCH (r:Routine {id: $routineId, companyId: $cid}) RETURN r.
|
|
2909
|
-
{ routineId, cid
|
|
3074
|
+
`MATCH (r:Routine {id: $routineId, companyId: $cid}) RETURN r.name AS name, r.config AS config`,
|
|
3075
|
+
{ routineId, cid }
|
|
2910
3076
|
);
|
|
2911
3077
|
const rows = parseRows(existing);
|
|
2912
3078
|
if (rows.length === 0) return jsonResponse(res, 404, { error: 'Routine not found' });
|
|
@@ -2914,48 +3080,69 @@ async function handlePatchRoutine(req, res, ctx, routineId) {
|
|
|
2914
3080
|
// Count existing revisions to determine revision number
|
|
2915
3081
|
const revCount = await mgQuery(
|
|
2916
3082
|
`MATCH (rv:RoutineRevision {routineId: $routineId, companyId: $cid}) RETURN count(rv) AS cnt`,
|
|
2917
|
-
{ routineId, cid
|
|
3083
|
+
{ routineId, cid }
|
|
2918
3084
|
);
|
|
2919
3085
|
const revRows = parseRows(revCount);
|
|
2920
3086
|
const revisionNumber = (revRows[0]?.[0] || 0) + 1;
|
|
2921
3087
|
|
|
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
3088
|
// Update routine with new values
|
|
3089
|
+
// CRIT-3 fix: handle body.status (for soft-delete via 'inactive' + other status changes)
|
|
3090
|
+
// CRIT-5 fix: use r.name not r.title
|
|
2943
3091
|
const sets = [];
|
|
2944
|
-
const params = { routineId, cid
|
|
2945
|
-
if (body.
|
|
2946
|
-
if (body.
|
|
3092
|
+
const params = { routineId, cid };
|
|
3093
|
+
if (body.name) { sets.push('r.name = $name'); params.name = String(body.name); }
|
|
3094
|
+
if (body.title) { sets.push('r.name = $name'); params.name = String(body.title); } // legacy alias
|
|
3095
|
+
if (body.status) { sets.push('r.status = $status'); params.status = String(body.status); }
|
|
3096
|
+
if (body.catchUpPolicy) { sets.push('r.catchUpPolicy = $catchUpPolicy'); params.catchUpPolicy = body.catchUpPolicy; }
|
|
2947
3097
|
if (body.concurrencyPolicy) { sets.push('r.concurrencyPolicy = $concurrencyPolicy'); params.concurrencyPolicy = body.concurrencyPolicy; }
|
|
2948
|
-
if (body.
|
|
3098
|
+
if (body.cronExpr) { sets.push('r.cronExpr = $cronExpr'); params.cronExpr = body.cronExpr; }
|
|
3099
|
+
if (body.config) { sets.push('r.config = $config'); params.config = JSON.stringify(body.config); }
|
|
3100
|
+
sets.push('r.updatedAt = datetime()');
|
|
3101
|
+
|
|
3102
|
+
// MED-3 fix: only create a revision if there are actual field changes (not on no-op patches)
|
|
3103
|
+
const hasChanges = sets.length > 1; // always has updatedAt, so check > 1
|
|
3104
|
+
if (hasChanges) {
|
|
3105
|
+
const revisionId = `revision:${crypto.randomUUID()}`;
|
|
3106
|
+
await mgQuery(
|
|
3107
|
+
`CREATE (rv:RoutineRevision {
|
|
3108
|
+
id: $revisionId,
|
|
3109
|
+
routineId: $routineId,
|
|
3110
|
+
companyId: $cid,
|
|
3111
|
+
revisionNumber: $revisionNumber,
|
|
3112
|
+
config: $config,
|
|
3113
|
+
createdAt: datetime()
|
|
3114
|
+
})`,
|
|
3115
|
+
{
|
|
3116
|
+
revisionId,
|
|
3117
|
+
routineId,
|
|
3118
|
+
cid,
|
|
3119
|
+
revisionNumber,
|
|
3120
|
+
config: JSON.stringify(body)
|
|
3121
|
+
}
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
2949
3124
|
|
|
2950
|
-
if (sets.length >
|
|
3125
|
+
if (sets.length > 1) {
|
|
2951
3126
|
await mgQuery(
|
|
2952
3127
|
`MATCH (r:Routine {id: $routineId, companyId: $cid}) SET ${sets.join(', ')}`,
|
|
2953
3128
|
params
|
|
2954
3129
|
);
|
|
2955
3130
|
}
|
|
2956
3131
|
|
|
2957
|
-
|
|
2958
|
-
|
|
3132
|
+
// HIGH-3 fix: SQLite write-through after successful Memgraph update
|
|
3133
|
+
if (sets.length > 1 && hboStore?.updateRoutine) {
|
|
3134
|
+
const storeUpdate = {};
|
|
3135
|
+
if (body.name || body.title) storeUpdate.name = body.name ?? body.title;
|
|
3136
|
+
if (body.status) storeUpdate.status = body.status;
|
|
3137
|
+
if (body.catchUpPolicy) storeUpdate.catch_up_policy = body.catchUpPolicy;
|
|
3138
|
+
if (body.concurrencyPolicy) storeUpdate.concurrency_policy = body.concurrencyPolicy;
|
|
3139
|
+
if (body.cronExpr) storeUpdate.cron_expr = body.cronExpr;
|
|
3140
|
+
try { hboStore.updateRoutine(routineId, cid, storeUpdate); }
|
|
3141
|
+
catch (storeErr) { log('error', 'hboStore.updateRoutine failed', { err: storeErr.message, routineId }); }
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
broadcast({ type: 'routine.updated', routineId, revisionNumber: hasChanges ? revisionNumber : null, companyId: cid });
|
|
3145
|
+
return jsonResponse(res, 200, { updated: true, routineId, revisionNumber: hasChanges ? revisionNumber : null });
|
|
2959
3146
|
} catch (err) {
|
|
2960
3147
|
return jsonResponse(res, 500, { error: err.message });
|
|
2961
3148
|
}
|
|
@@ -2981,6 +3168,12 @@ async function handleGetRoutineRevisions(req, res, ctx, routineId) {
|
|
|
2981
3168
|
|
|
2982
3169
|
async function handleGetRoutineRuns(req, res, ctx, routineId) {
|
|
2983
3170
|
const { mgQuery } = ctx;
|
|
3171
|
+
const cid = ctx.cid;
|
|
3172
|
+
// MED-5 fix: SQL parity — fallback to SQLite when Memgraph unavailable
|
|
3173
|
+
if (!mgQuery) {
|
|
3174
|
+
// No RoutineRun SQLite table exists yet — return empty gracefully
|
|
3175
|
+
return jsonResponse(res, 200, { runs: [], _source: 'sqlite' });
|
|
3176
|
+
}
|
|
2984
3177
|
try {
|
|
2985
3178
|
const result = await mgQuery(
|
|
2986
3179
|
`MATCH (rr:RoutineRun {routineId: $routineId, companyId: $cid})
|
|
@@ -2988,7 +3181,7 @@ async function handleGetRoutineRuns(req, res, ctx, routineId) {
|
|
|
2988
3181
|
rr.startedAt AS startedAt, rr.completedAt AS completedAt
|
|
2989
3182
|
ORDER BY rr.startedAt DESC
|
|
2990
3183
|
LIMIT toInteger($lim)`,
|
|
2991
|
-
{ routineId, cid
|
|
3184
|
+
{ routineId, cid, lim: 50 }
|
|
2992
3185
|
);
|
|
2993
3186
|
const rows = parseRows(result);
|
|
2994
3187
|
const keys = ['id', 'status', 'taskId', 'startedAt', 'completedAt'];
|
|
@@ -3348,9 +3541,14 @@ const approvalsRoute = require('./routes/approvals')({
|
|
|
3348
3541
|
|
|
3349
3542
|
const agentsRoute = require('./routes/agents')({
|
|
3350
3543
|
handleGetAgents,
|
|
3544
|
+
handleGetAgent, // P7-A1: wire single-agent GET (was dead code — not passed before)
|
|
3545
|
+
handleGetAgentRuns, // P7-A2: HeartbeatRun history
|
|
3351
3546
|
handleSyncSkills,
|
|
3352
3547
|
handleApproveAgent,
|
|
3353
3548
|
handleTerminateAgent,
|
|
3549
|
+
handlePauseAgent, // Bug fix: was missing, caused TypeError in agents.js
|
|
3550
|
+
handleResumeAgent, // Bug fix: was missing
|
|
3551
|
+
handlePauseAll: null, // pause-all is handled in hbo.js; stub null to prevent TypeError
|
|
3354
3552
|
});
|
|
3355
3553
|
|
|
3356
3554
|
const skillsRoute = require('./routes/skills')({
|
|
@@ -3405,7 +3603,7 @@ const inboxRoute = require('./routes/inbox')({ broadcast });
|
|
|
3405
3603
|
const queueRoute = require('./routes/queue')({ broadcast });
|
|
3406
3604
|
const { handleDraftsRoute } = require('./routes/drafts');
|
|
3407
3605
|
const goalsRoute = require('./routes/goals');
|
|
3408
|
-
|
|
3606
|
+
let hboRoute = require('./routes/hbo')({ broadcast });
|
|
3409
3607
|
const emailTriageRoute = require('./routes/email-triage')();
|
|
3410
3608
|
const andonRoute = require('./routes/andon')({ handleGetAndonBoard });
|
|
3411
3609
|
const wizardRoute = require('./routes/wizard')({});
|
|
@@ -3435,6 +3633,9 @@ let mandalaRoute;
|
|
|
3435
3633
|
try { mandalaRoute = require('./routes/mandala').mandalaRoute; } catch (_) { mandalaRoute = () => false; }
|
|
3436
3634
|
let personRoute;
|
|
3437
3635
|
try { personRoute = require('./routes/person')({ handleGetPersonProfile, handleGetPersonByEmail, handleGetPersonEpisodes }); } catch (_) { personRoute = () => false; }
|
|
3636
|
+
// P5-S5c: CRM dual-write route — accepts POST /api/crm/contacts from desktop crmSyncService
|
|
3637
|
+
let crmRoute;
|
|
3638
|
+
try { crmRoute = require('./routes/crm')({ parseBody, jsonResponse }); } catch (_) { crmRoute = () => false; }
|
|
3438
3639
|
|
|
3439
3640
|
|
|
3440
3641
|
// ── WS-nonce store (single-use, 60-second TTL) ───────────────────────────────
|
|
@@ -3499,6 +3700,25 @@ async function route(req, res, ctx) {
|
|
|
3499
3700
|
return;
|
|
3500
3701
|
}
|
|
3501
3702
|
|
|
3703
|
+
// SP3: Read receipt tracking pixel — must be before auth middleware, no auth required
|
|
3704
|
+
if (req.method === 'GET' && /^\/t\/[a-zA-Z0-9_-]+\.png$/.test(req.url || '')) {
|
|
3705
|
+
const trackingId = (req.url || '').slice(3, -4); // strip leading /t/ and trailing .png
|
|
3706
|
+
const gif1x1 = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
|
|
3707
|
+
res.writeHead(200, {
|
|
3708
|
+
'Content-Type': 'image/gif',
|
|
3709
|
+
'Content-Length': gif1x1.length,
|
|
3710
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
3711
|
+
'Pragma': 'no-cache',
|
|
3712
|
+
});
|
|
3713
|
+
res.end(gif1x1);
|
|
3714
|
+
// Fire-and-forget: log open event to Memgraph
|
|
3715
|
+
try {
|
|
3716
|
+
const { rawWrite: rw } = require('../lib/safe-memgraph.js');
|
|
3717
|
+
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(() => {});
|
|
3718
|
+
} catch {}
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3502
3722
|
// ── Bearer token auth ────────────────────────────────────────────────────
|
|
3503
3723
|
// Public routes that skip auth
|
|
3504
3724
|
const PUBLIC_PATHS = new Set(['/api/health', '/api/auth/ws-nonce', '/login', '/signup', '/api/auth/signup', '/api/auth/login', '/api/auth/logout']);
|
|
@@ -3875,6 +4095,7 @@ async function route(req, res, ctx) {
|
|
|
3875
4095
|
if (await suggestionsRoute(req, res, reqCtx, pathname, method)) return;
|
|
3876
4096
|
if (await mandalaRoute(req, res, reqCtx, pathname, method)) return;
|
|
3877
4097
|
if (await personRoute(req, res, reqCtx, pathname, method)) return;
|
|
4098
|
+
if (await crmRoute(req, res, reqCtx, pathname, method)) return;
|
|
3878
4099
|
|
|
3879
4100
|
// ── Phase 3: SystemAim endpoints ─────────────────────────────────────────
|
|
3880
4101
|
if (pathname === '/api/system-aim') {
|
|
@@ -4151,11 +4372,26 @@ async function handleGetDepartmentPageRoute(req, res, ctx, department) {
|
|
|
4151
4372
|
if (narrativeStr && lastGenStr) {
|
|
4152
4373
|
const lastGen = new Date(String(lastGenStr)).getTime();
|
|
4153
4374
|
if (!isNaN(lastGen) && Date.now() - lastGen < CACHE_TTL_MS) {
|
|
4154
|
-
// Cache hit — return as-is
|
|
4375
|
+
// Cache hit — return as-is, also include tasks created from this page
|
|
4155
4376
|
let narrative;
|
|
4156
4377
|
try { narrative = JSON.parse(String(narrativeStr)); }
|
|
4157
4378
|
catch (_) { narrative = { governingThought: String(narrativeStr) }; }
|
|
4158
|
-
|
|
4379
|
+
|
|
4380
|
+
// P8E-01: Include tasks created from this department page's decisionsStructured
|
|
4381
|
+
const tasksResult = await mg(
|
|
4382
|
+
`MATCH (dp:DepartmentPage {companyId: $cid, department: $dept})
|
|
4383
|
+
WHERE dp.narrative IS NOT NULL
|
|
4384
|
+
OPTIONAL MATCH (dp)-[:CREATED_TASK]->(t:Task)
|
|
4385
|
+
RETURN collect({id: t.id, title: t.title, status: t.status, originKind: t.originKind}) AS tasks
|
|
4386
|
+
ORDER BY dp.lastGeneratedAt DESC LIMIT 1`,
|
|
4387
|
+
{ cid, dept: department }
|
|
4388
|
+
).catch(() => null);
|
|
4389
|
+
const tasksRow = tasksResult && tasksResult.rows ? tasksResult.rows[0] : null;
|
|
4390
|
+
const tasks = tasksRow
|
|
4391
|
+
? (Array.isArray(tasksRow) ? (tasksRow[0] || []) : (tasksRow.tasks || []))
|
|
4392
|
+
: [];
|
|
4393
|
+
|
|
4394
|
+
jsonResponse(res, 200, { department, narrative, generatedAt: lastGenStr, cached: true, tasks });
|
|
4159
4395
|
return;
|
|
4160
4396
|
}
|
|
4161
4397
|
}
|
|
@@ -4407,6 +4643,21 @@ function startApi(mgQuery, config = {}, state = {}) {
|
|
|
4407
4643
|
deptRoute = require('./routes/dept')({ mgQuery, broadcast: state.broadcast ?? (() => {}) });
|
|
4408
4644
|
}
|
|
4409
4645
|
|
|
4646
|
+
// Reinitialize hboRoute with triggerGoalDecompose hook so POST /api/hbo/goals immediately
|
|
4647
|
+
// fires tickGoalDecompose instead of waiting up to 150s for the next 5th-tick.
|
|
4648
|
+
// state.daemon._mods is the per-company module registry built by buildForCompany().
|
|
4649
|
+
if (state.daemon && typeof state.broadcast === 'function') {
|
|
4650
|
+
const _triggerFn = (cid) => {
|
|
4651
|
+
try {
|
|
4652
|
+
const mods = state.daemon._mods && state.daemon._mods.get ? state.daemon._mods.get(cid) : (state.daemon._mods?.[cid]);
|
|
4653
|
+
if (mods && mods.hboBridge && typeof mods.hboBridge.tickGoalDecompose === 'function') {
|
|
4654
|
+
mods.hboBridge.tickGoalDecompose({ fromGoalCreate: true }).catch(() => {});
|
|
4655
|
+
}
|
|
4656
|
+
} catch (_) {}
|
|
4657
|
+
};
|
|
4658
|
+
hboRoute = require('./routes/hbo')({ broadcast: state.broadcast, mgQuery, triggerGoalDecompose: _triggerFn });
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4410
4661
|
// HED routes — requires mgQuery for HEDEngine
|
|
4411
4662
|
try {
|
|
4412
4663
|
const { createHedRoutes } = require('./routes/hed');
|
|
@@ -4439,6 +4690,7 @@ function startApi(mgQuery, config = {}, state = {}) {
|
|
|
4439
4690
|
draining: false,
|
|
4440
4691
|
apiToken: config.apiToken || null, // null = no auth (backward compat)
|
|
4441
4692
|
daemon: state.daemon ?? null, // daemon instance ref — used by wizard route for onCompanyReady
|
|
4693
|
+
activityLogger: state.activityLogger ?? null, // H-01: for recording approval events
|
|
4442
4694
|
modules: [
|
|
4443
4695
|
'RoutineEvaluator', 'BudgetEnforcer', 'LivenessWatchdog',
|
|
4444
4696
|
'AgentDispatcher', 'ActivityLogger', 'TaskCompletionWatchdog',
|