@cgh567/agent 2.4.1 → 2.4.2

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.
Files changed (146) hide show
  1. package/bin/helios +0 -0
  2. package/bin/helios-rpc-node-wrapper.cjs +0 -0
  3. package/bin/helios-rpc-wrapper.sh +0 -0
  4. package/daemon/adapters/helios-rpc-adapter.js +47 -25
  5. package/daemon/config/com.familiar.helios-daemon.plist +5 -0
  6. package/daemon/config/helios-daemon.service +4 -0
  7. package/daemon/context-enrichment.js +59 -21
  8. package/daemon/helios-api.js +149 -37
  9. package/daemon/helios-company-daemon.js +516 -124
  10. package/daemon/lib/harada/cascade-judge.js +12 -50
  11. package/daemon/lib/harada/mandala.js +20 -0
  12. package/daemon/lib/harada/pillar-dispatcher.js +1 -1
  13. package/daemon/lib/harada/project-factory.js +7 -2
  14. package/daemon/lib/hbo-bridge.js +31 -12
  15. package/daemon/lib/helios-hitl-host.js +15 -2
  16. package/daemon/lib/hitl-interaction-service.js +0 -0
  17. package/daemon/lib/memgraph-verify.js +38 -33
  18. package/daemon/lib/project-drift-detector.js +7 -17
  19. package/daemon/lib/project-semantic-updater.js +1 -14
  20. package/daemon/routes/channels.js +10 -5
  21. package/daemon/routes/harada-map.js +11 -48
  22. package/daemon/routes/hbo.js +89 -28
  23. package/daemon/routes/hitl.js +0 -0
  24. package/daemon/routes/project.js +4 -3
  25. package/daemon/routes/wizard.js +11 -4
  26. package/daemon/schema-migrations-hitl.js +0 -0
  27. package/extensions/001-tool-output-cap.ts +0 -0
  28. package/extensions/context-compaction.ts +45 -26
  29. package/extensions/cortex/activation-bridge.ts +5 -0
  30. package/extensions/cortex/learn.ts +26 -0
  31. package/extensions/email/backfill.ts +0 -0
  32. package/extensions/helios-governance/analysis/ambiguity.ts +0 -0
  33. package/extensions/helios-governance/analysis/compliance.ts +0 -0
  34. package/extensions/helios-governance/analysis/long-task-detector.ts +0 -0
  35. package/extensions/helios-governance/analysis/output-contract.ts +0 -0
  36. package/extensions/helios-governance/analysis/patterns.ts +0 -0
  37. package/extensions/helios-governance/analysis/preflight.ts +0 -0
  38. package/extensions/helios-governance/analysis/recurring-violations.ts +0 -0
  39. package/extensions/helios-governance/analysis/task-classification.ts +0 -0
  40. package/extensions/helios-governance/analysis/task-intent.ts +0 -0
  41. package/extensions/helios-governance/gates/high-impact.ts +1 -1
  42. package/extensions/helios-governance/handlers/_jiti-require.ts +15 -8
  43. package/extensions/helios-governance/handlers/proxy-test-detector.ts +0 -0
  44. package/extensions/hema-dispatch-v3/graph-memory.ts +10 -0
  45. package/extensions/hema-dispatch-v3/index.ts +59 -40
  46. package/extensions/lib/elo-engine.js +0 -0
  47. package/extensions/lib/elo-engine.test.js +0 -0
  48. package/extensions/memgraph-autostart.ts +13 -0
  49. package/extensions/neuroplastic-eval.ts +0 -0
  50. package/extensions/shadow-loop/index.ts +0 -0
  51. package/lib/brain-v2-budget.js +0 -0
  52. package/lib/brain-v2-circuit-breaker.js +0 -0
  53. package/lib/brain-v2.js +0 -0
  54. package/lib/broker/adaptive-throttle.js +0 -0
  55. package/lib/broker/batch-coalescer.js +0 -0
  56. package/lib/broker/bulkhead.js +0 -0
  57. package/lib/broker/channel-registry.js +0 -0
  58. package/lib/broker/circuit-breaker.js +0 -0
  59. package/lib/broker/evidence-cache.js +0 -0
  60. package/lib/broker/health-monitor.js +0 -0
  61. package/lib/broker/mage-queue.js +0 -0
  62. package/lib/broker/priority-queue.js +0 -0
  63. package/lib/broker/server.js.bak-error2-fix +0 -0
  64. package/lib/broker/session-registry.js +0 -0
  65. package/lib/broker/singleton-timers.js +0 -0
  66. package/lib/broker/types.d.ts +0 -0
  67. package/lib/broker/vegas-limit.js +0 -0
  68. package/lib/compression/dist/ccr-store.js +74 -0
  69. package/lib/compression/dist/content-router.js +115 -0
  70. package/lib/compression/dist/pipeline.js +113 -0
  71. package/lib/compression/dist/server.js +265 -0
  72. package/lib/compression/dist/smart-crusher.js +251 -0
  73. package/lib/context-budget.ts +0 -0
  74. package/lib/context-firewall.js +0 -0
  75. package/lib/crm/integration/triage-bridge.js +0 -0
  76. package/lib/email-utils.ts +0 -0
  77. package/lib/eval/__tests__/preflight-checker.test.ts +0 -0
  78. package/lib/eval/__tests__/task-instruction-parser.test.ts +0 -0
  79. package/lib/eval/__tests__/verifier-runner.test.ts +0 -0
  80. package/lib/eval/index.ts +0 -0
  81. package/lib/eval/preflight-checker.ts +0 -0
  82. package/lib/eval/task-domain-classifier.ts +0 -0
  83. package/lib/eval/task-instruction-parser.ts +0 -0
  84. package/lib/eval/verifier-runner.ts +0 -0
  85. package/lib/event-bus.d.ts +0 -0
  86. package/lib/governance-context-selector.ts +0 -0
  87. package/lib/graph/generate-extension-embeddings.js +0 -0
  88. package/lib/graph/generate-static-embeddings.js +0 -0
  89. package/lib/graph/lib/utils.js +1 -1
  90. package/lib/graph-audit.d.ts +0 -0
  91. package/lib/mesh-circuit-breaker.js +0 -0
  92. package/lib/mission-loop/lesson-extractor.ts +0 -0
  93. package/lib/mission-loop/mental-model-scorer.ts +0 -0
  94. package/lib/mission-loop/occ-detector.ts +0 -0
  95. package/lib/mission-loop/query-variants.ts +0 -0
  96. package/lib/mission-loop/verifier-check.ts +0 -0
  97. package/lib/skill-reference-builder.ts +0 -0
  98. package/lib/telemetry/token-breakdown.ts +0 -0
  99. package/lib/tool-compressor.ts +0 -0
  100. package/lib/triage-core/legal-routing.ts +0 -0
  101. package/lib/triage-core/mental-model/dunbar-classifier.ts +0 -0
  102. package/lib/triage-core/mental-model/enrich-all.ts +0 -0
  103. package/lib/triage-core/mental-model/identity-resolver.ts +0 -0
  104. package/lib/triage-core/mental-model/key-facts.ts +0 -0
  105. package/lib/triage-core/mental-model/model-assembler.ts +0 -0
  106. package/lib/triage-core/orchestrator.ts +0 -0
  107. package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -0
  108. package/package.json +10 -4
  109. package/skills/helios-business-operator/services/signals/upwork-signals.js +0 -0
  110. package/skills/talisman-ceo/SKILL.md +23 -25
  111. package/skills/talisman-comms/SKILL.md +5 -5
  112. package/skills/talisman-engineering/SKILL.md +5 -5
  113. package/skills/talisman-finance/SKILL.md +10 -8
  114. package/skills/talisman-marketing/SKILL.md +10 -10
  115. package/skills/talisman-sales/SKILL.md +12 -15
  116. package/skills/talisman-support/SKILL.md +5 -5
  117. package/agents/business/talisman-ceo.md +0 -183
  118. package/agents/business/talisman-comms.md +0 -257
  119. package/agents/business/talisman-cto.md +0 -153
  120. package/agents/business/talisman-finance.md +0 -246
  121. package/agents/business/talisman-marketing.md +0 -240
  122. package/agents/business/talisman-sales.md +0 -242
  123. package/agents/business/talisman-support.md +0 -236
  124. package/daemon/lib/approval-expiry.js +0 -162
  125. package/daemon/lib/blast-radius-analyzer.js +0 -75
  126. package/daemon/lib/domain-bootstrap-orchestrator.js +0 -267
  127. package/daemon/lib/forensic-log.js +0 -113
  128. package/daemon/lib/goal-research-pipeline.js +0 -644
  129. package/daemon/lib/harada/cascade-research-dispatcher.js +0 -261
  130. package/daemon/lib/headroom-middleware.js +0 -167
  131. package/daemon/lib/headroom-proxy-manager.js +0 -623
  132. package/daemon/lib/hed-engine.js +0 -307
  133. package/daemon/lib/mental-model-cache.js +0 -96
  134. package/daemon/lib/project-factory.js +0 -47
  135. package/daemon/lib/session-log-reader.js +0 -93
  136. package/daemon/routes/hed.js +0 -133
  137. package/lib/graph/learning/headroom-learn-bridge.js +0 -215
  138. package/skills/helios-bookkeeping/SKILL.md +0 -321
  139. package/skills/helios-briefer/SKILL.md +0 -44
  140. package/skills/helios-client-relations/SKILL.md +0 -322
  141. package/skills/helios-personal-triager/SKILL.md +0 -45
  142. package/skills/helios-recruitment/SKILL.md +0 -317
  143. package/skills/helios-relationship-nudger/SKILL.md +0 -77
  144. package/skills/helios-researcher/SKILL.md +0 -44
  145. package/skills/helios-scheduler/SKILL.md +0 -58
  146. package/skills/helios-tax-analyst/SKILL.md +0 -280
@@ -8,55 +8,8 @@
8
8
  */
9
9
 
10
10
  const https = require('https');
11
- const http = require('http');
12
11
 
13
- // ── Headroom proxy routing helper ────────────────────────────────────────────
14
-
15
- /**
16
- * Resolve request options for an Anthropic API call.
17
- * Routes through Headroom proxy when running (http://127.0.0.1:PORT),
18
- * falls back to direct api.anthropic.com otherwise.
19
- */
20
- function _buildAnthropicOpts(apiKey, bodyLength) {
21
- try {
22
- const { HeadroomProxyManager } = require('../headroom-proxy-manager');
23
- const baseUrl = HeadroomProxyManager.getInstance().getBaseUrl();
24
- if (baseUrl) {
25
- const url = new URL(baseUrl);
26
- return {
27
- module: http,
28
- opts: {
29
- hostname: url.hostname,
30
- port: parseInt(url.port || '8787', 10),
31
- path: '/v1/messages',
32
- method: 'POST',
33
- headers: {
34
- 'Content-Type': 'application/json',
35
- 'x-api-key': apiKey,
36
- 'anthropic-version': '2023-06-01',
37
- 'Content-Length': bodyLength,
38
- },
39
- },
40
- };
41
- }
42
- } catch (_) {}
43
- return {
44
- module: https,
45
- opts: {
46
- hostname: 'api.anthropic.com',
47
- path: '/v1/messages',
48
- method: 'POST',
49
- headers: {
50
- 'Content-Type': 'application/json',
51
- 'x-api-key': apiKey,
52
- 'anthropic-version': '2023-06-01',
53
- 'Content-Length': bodyLength,
54
- },
55
- },
56
- };
57
- }
58
-
59
- // ── Provider-agnostic LLM (exact pattern from project-semantic-updater.js) ───
12
+ // ── Provider-agnostic LLM ────────────────────────────────────────────────────
60
13
 
61
14
  async function callLLM(systemPrompt, userContent, maxTokens) {
62
15
  maxTokens = maxTokens || 256;
@@ -81,8 +34,17 @@ function _callAnthropic(apiKey, systemPrompt, userContent, maxTokens) {
81
34
  messages: [{ role: 'user', content: userContent }],
82
35
  });
83
36
  var bodyLen = Buffer.byteLength(body);
84
- var _r = _buildAnthropicOpts(apiKey, bodyLen);
85
- var req = _r.module.request(_r.opts, function (resp) {
37
+ var req = https.request({
38
+ hostname: 'api.anthropic.com',
39
+ path: '/v1/messages',
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ 'x-api-key': apiKey,
44
+ 'anthropic-version': '2023-06-01',
45
+ 'Content-Length': bodyLen,
46
+ },
47
+ }, function (resp) {
86
48
  var raw = '';
87
49
  resp.on('data', function (chunk) { raw += chunk.toString(); });
88
50
  resp.on('end', function () {
@@ -172,6 +172,26 @@ class MandalaManager {
172
172
  { signalId, cid: this.companyId, name: pillarName, weight, weeks: Number(zeroWeeks), pillarId, goalId }
173
173
  ).catch(() => {});
174
174
 
175
+ // AndonAlert mirrors AnomalySignal — queried by the /api/hbo/andon/:id/resolve endpoint.
176
+ // The resolve endpoint uses :AndonAlert label; both nodes are kept in sync.
177
+ const andonId = `andon:pillar_neglect:${this.companyId}:${pillarId}`;
178
+ await this.mg(
179
+ `MERGE (a:AndonAlert {id: $andonId, companyId: $cid})
180
+ ON CREATE SET
181
+ a.companyId = $cid,
182
+ a.signalId = $signalId,
183
+ a.pillarId = $pillarId,
184
+ a.goalId = $goalId,
185
+ a.severity = 'P2',
186
+ a.status = 'open',
187
+ a.source = 'mandala',
188
+ a.detectedAt = localdatetime(),
189
+ a.message = 'Pillar neglected for ' + $weeks + ' consecutive weeks'
190
+ ON MATCH SET
191
+ a.lastSeenAt = localdatetime()`,
192
+ { andonId, signalId, cid: this.companyId, pillarId, goalId, weeks: Number(zeroWeeks) }
193
+ ).catch(() => {});
194
+
175
195
  await this.mg(
176
196
  `MATCH (p:GoalPillar {id: $id, companyId: $cid}) SET p.lastAndonFiredAt = localdatetime()`,
177
197
  { id: pillarId, cid: this.companyId }
@@ -368,7 +368,7 @@ class PillarDispatcher {
368
368
  // Ensure GoalPillar has project fields and ProjectDocument, then wire Task.pillarId
369
369
  const { ensureProject } = require('./project-factory');
370
370
  await ensureProject(
371
- this._mg, pillar.id, goalId, companyId, strategy, agentId
371
+ this._mg, pillar.id, goalId, companyId, strategy, agentId, this._broadcast
372
372
  ).catch(() => null);
373
373
 
374
374
  // Set Task.pillarId so context-enrichment can look up GoalPillar directly
@@ -1,6 +1,6 @@
1
- 'use strict';
1
+ 'use strict';
2
2
 
3
- async function ensureProjectOnPillar(mgQuery, pillarId, goalId, companyId, strategy, agentId) {
3
+ async function ensureProjectOnPillar(mgQuery, pillarId, goalId, companyId, strategy, agentId, broadcast) {
4
4
  const docId = `pdoc:${pillarId}:main`;
5
5
 
6
6
  // Set project properties directly on GoalPillar (no HeliosProject node created)
@@ -32,6 +32,11 @@ async function ensureProjectOnPillar(mgQuery, pillarId, goalId, companyId, strat
32
32
  { pillarId, docId, cid: companyId }
33
33
  );
34
34
 
35
+ // Broadcast project:created so desktop SSE listeners refresh project list
36
+ if (broadcast) {
37
+ try { broadcast({ type: 'project:created', companyId, projectId: pillarId, pillarId }); } catch (_) {}
38
+ }
39
+
35
40
  // Return shape includes 'id' alias for pillarId for backward compat
36
41
  // with any callers that still check project.id before full deployment.
37
42
  return { pillarId, docId, id: pillarId };
@@ -708,7 +708,7 @@ class HBOBridge {
708
708
  * Phase 3 addition: validates SystemAim exists before proceeding.
709
709
  * Phase 4 addition: generates QuarterlyOKRs for each department agent per goal.
710
710
  */
711
- async tickGoalDecompose() {
711
+ async tickGoalDecompose(opts = {}) {
712
712
  try {
713
713
  // Phase 3: SystemAim guard — block decomposition if aim not set
714
714
  const aimCheck = await this._mg(
@@ -716,8 +716,22 @@ class HBOBridge {
716
716
  { cid: this._companyId }
717
717
  ).catch(() => null);
718
718
  if (!aimCheck?.rows?.length) {
719
- this._log('warn', 'tickGoalDecompose: No SystemAim — skipping (requires human to set via POST /api/system-aim)');
720
- return;
719
+ if (opts?.fromWizard) {
720
+ // Wizard just wrote SystemAim — retry once with delay in case of write propagation lag
721
+ await new Promise(r => setTimeout(r, 500));
722
+ const aimRetry = await this._mg(
723
+ `MATCH (aim:SystemAim {companyId: $cid}) RETURN aim.id LIMIT 1`,
724
+ { cid: this._companyId }
725
+ ).catch(() => null);
726
+ if (!aimRetry?.rows?.length) {
727
+ this._log('warn', 'tickGoalDecompose: No SystemAim after retry — skipping wizard-triggered init');
728
+ return;
729
+ }
730
+ this._log('info', 'tickGoalDecompose: SystemAim found on retry (write propagation delay)');
731
+ } else {
732
+ this._log('warn', 'tickGoalDecompose: No SystemAim — skipping (requires human to set via POST /api/system-aim)');
733
+ return;
734
+ }
721
735
  }
722
736
 
723
737
  // Find active goals (CompanyGoal OR BusinessGoal) with no child BusinessTasks
@@ -791,7 +805,7 @@ class HBOBridge {
791
805
  const taskId = `task:decompose:${goal.id}`;
792
806
  const owner = goal.owner ?? `agent:${this._companyId}:ceo`;
793
807
  const title = `Decompose goal: ${goal.title}`;
794
- const body = `Goal ID: ${goal.id}\nGoal: ${goal.title}\nDescription: ${goal.description}\n\nReview the work quality for each pillar assignment for this goal. Verify that outputs meet the Definition of Done, department standards, and strategic alignment. Escalate any quality gaps via Andon.`;
808
+ const body = `Protocol: harada_first_decomposition\nGoal ID: ${goal.id}\nGoal: ${goal.title}\nDescription: ${goal.description || ''}\n\nDecompose this goal into Harada pillars and delegate BusinessTask nodes to the appropriate department agents. Set assigneeId on every BusinessTask to a valid agent ID (e.g. agent:cto, agent:sales). Never create a task with null assigneeId.`;
795
809
 
796
810
  try {
797
811
  await this._mg(
@@ -805,7 +819,10 @@ class HBOBridge {
805
819
  t.workType = 'strategic',
806
820
  t.hboGoalId = $goalId,
807
821
  t.priority = toInteger(1),
808
- t.createdAt = localdatetime()`,
822
+ t.createdAt = localdatetime()
823
+ ON MATCH SET
824
+ t.title = $title,
825
+ t.body = $body`,
809
826
  { taskId, cid: this._companyId, title, body, owner, goalId: goal.id }
810
827
  );
811
828
  this._log('info', `tickGoalDecompose: ensured decomposition task for goal "${goal.title}" → ${owner}`);
@@ -979,7 +996,8 @@ class HBOBridge {
979
996
  OPTIONAL MATCH (t:Task {hboTaskId: bt.id})
980
997
  WITH bt, t
981
998
  WHERE t IS NULL
982
- RETURN bt.id AS id, bt.title AS title, bt.assigneeId AS assigneeId,
999
+ RETURN bt.id AS id, bt.title AS title, bt.description AS description,
1000
+ bt.assigneeId AS assigneeId,
983
1001
  bt.priority AS priority, bt.goalId AS goalId
984
1002
  LIMIT toInteger(20)`,
985
1003
  { cid: this._companyId }
@@ -991,11 +1009,12 @@ class HBOBridge {
991
1009
  let created = 0;
992
1010
 
993
1011
  for (const row of businessTasks.rows) {
994
- const btId = row[0] ?? row['id'];
995
- const title = row[1] ?? row['title'];
996
- const assigneeId = row[2] ?? row['assigneeId'];
997
- const priority = row[3] ?? row['priority'] ?? 'medium';
998
- const goalId = row[4] ?? row['goalId'];
1012
+ const btId = row[0] ?? row['id'];
1013
+ const title = row[1] ?? row['title'];
1014
+ const description = row[2] ?? row['description'] ?? null;
1015
+ const assigneeId = row[3] ?? row['assigneeId'];
1016
+ const priority = row[4] ?? row['priority'] ?? 'medium';
1017
+ const goalId = row[5] ?? row['goalId'];
999
1018
 
1000
1019
  if (!btId || !title) continue;
1001
1020
 
@@ -1032,7 +1051,7 @@ class HBOBridge {
1032
1051
  taskId,
1033
1052
  cid: this._companyId,
1034
1053
  title: String(title),
1035
- body: `Goal sync task.\nGoal: ${title}\nReview this goal's current status and determine the highest-value next action.`,
1054
+ body: description ? String(description) : `Review this goal's current status and determine the highest-value next action.\nGoal: ${title}`,
1036
1055
  priority: priorityNum,
1037
1056
  assigneeAgentId: assigneeId,
1038
1057
  hboTaskId: btId,
@@ -261,6 +261,16 @@ async function _handleNonTTY(event, mgQuery, taskId, companyId, ctx) {
261
261
  companyId: companyId,
262
262
  declaration: decl,
263
263
  });
264
+ if (ctx.broadcast && ctx.isWizardSession) {
265
+ ctx.broadcast({
266
+ type: 'wizard:interpretation_ready',
267
+ id: _broadcastInteractionId,
268
+ agentId: 'agent:' + companyId + ':unknown',
269
+ taskId: taskId || ('task:unknown:' + companyId),
270
+ companyId: companyId,
271
+ declaration: decl,
272
+ });
273
+ }
264
274
  } else {
265
275
  setTimeout(_attemptBroadcast, 200);
266
276
  }
@@ -405,7 +415,10 @@ function createHITLHost(opts) {
405
415
  // The raw host handles a single surfaced interaction (batch or single)
406
416
  // Resolve declaration lazily if a getter is provided (supports agent subprocess IPC path).
407
417
  const _getDecl = typeof opts.getInterpretationDeclaration === 'function' ? opts.getInterpretationDeclaration : null;
408
- const host = new HITLHost({ companyId, taskTitle, mgQuery, taskId, ctx: { isInterpretation: opts.isInterpretation, interpretationDeclaration: _getDecl ? undefined : opts.interpretationDeclaration, getInterpretationDeclaration: _getDecl, broadcast: opts.broadcast || null } });
418
+ // isWizardSession: pass true from wizard-time callers (e.g. wizard route / goal-research-pipeline)
419
+ // so that wizard:interpretation_ready is only emitted during the onboarding wizard, not for
420
+ // regular task-time interpretation interactions.
421
+ const host = new HITLHost({ companyId, taskTitle, mgQuery, taskId, ctx: { isInterpretation: opts.isInterpretation, isWizardSession: opts.isWizardSession || false, interpretationDeclaration: _getDecl ? undefined : opts.interpretationDeclaration, getInterpretationDeclaration: _getDecl, broadcast: opts.broadcast || null } });
409
422
 
410
423
  // Wrap with the batcher — it calls host._interact for each surfaced event
411
424
  const batcher = new HITLBatcher({
@@ -417,7 +430,7 @@ function createHITLHost(opts) {
417
430
  if (!process.stdin.isTTY) {
418
431
  // Non-TTY: route through Memgraph if mgQuery available
419
432
  if (mgQuery && (taskId || companyId)) {
420
- return await _handleNonTTY(event, mgQuery, taskId, companyId, { isInterpretation: opts.isInterpretation, interpretationDeclaration: _getDecl ? _getDecl() : opts.interpretationDeclaration, broadcast: opts.broadcast || null });
433
+ return await _handleNonTTY(event, mgQuery, taskId, companyId, { isInterpretation: opts.isInterpretation, isWizardSession: opts.isWizardSession || false, interpretationDeclaration: _getDecl ? _getDecl() : opts.interpretationDeclaration, broadcast: opts.broadcast || null });
421
434
  }
422
435
  process.stderr.write('[helios-hitl-host] Non-TTY, no mgQuery — auto-responding to ' + (event.id || 'batch') + '\n');
423
436
  if (event.isBatch && Array.isArray(event.questions)) {
File without changes
@@ -5,10 +5,17 @@
5
5
  *
6
6
  * Verifies that the Memgraph instance is configured correctly for the Helios daemon:
7
7
  *
8
- * 1. timestamp() returns MICROSECONDS (not milliseconds like Neo4j).
9
- * Helios uses `timestamp()` for zombie-detection math. If the unit is wrong,
10
- * threshold comparisons silently break agents either never get paused or
11
- * immediately get paused. This is a hard-fail guard.
8
+ * 1. timestamp() (no argument) returns MICROSECONDS.
9
+ * Helios zombie-detection uses `toInteger(timestamp() / 1000)` which must
10
+ * produce epoch seconds. This check confirms the no-arg form returns µs
11
+ * so the division is correct.
12
+ *
13
+ * IMPORTANT: In Memgraph v3, timestamp() and timestamp(datetime_arg) are
14
+ * different functions with different units:
15
+ * - timestamp() → current epoch in MICROSECONDS ✓ (what we use)
16
+ * - timestamp(datetime) → epoch of that datetime in MILLISECONDS (NOT used)
17
+ * Prior version of this file used timestamp(datetime_arg) and hard-exited
18
+ * when it returned ms. That was wrong — we only care about the no-arg form.
12
19
  *
13
20
  * 2. localdatetime() is available (basic connectivity proof).
14
21
  *
@@ -35,22 +42,28 @@ async function verifyMemgraphConfig(mgQuery) {
35
42
 
36
43
  const serverTime = dtResult.rows[0][0];
37
44
 
38
- // ── Step 2: verify timestamp() unit is microseconds ─────────────────────────
45
+ // ── Step 2: verify timestamp() (no arg) unit is microseconds ─────────────────
46
+ //
47
+ // Strategy: call timestamp() with NO argument to get the current epoch value.
48
+ // Check the order of magnitude:
49
+ // - microseconds: current time ~1.75 × 10^15 (16 digits)
50
+ // - milliseconds: current time ~1.75 × 10^12 (13 digits)
39
51
  //
40
- // Strategy: create a known localDateTime, compute timestamp() of it, then
41
- // check the order of magnitude vs. a reference epoch millisecond value.
52
+ // We use the no-arg form because that is exactly what zombie-detection uses:
53
+ // toInteger(timestamp() / 1000) must produce epoch seconds
42
54
  //
43
- // Unix epoch 2020-01-01T00:00:00 in:
44
- // - milliseconds: 1577836800000 (~1.58 × 10^12)
45
- // - microseconds: 1577836800000000 (~1.58 × 10^15)
55
+ // If the result is in microseconds: /1000 → milliseconds → stored as ms → comparisons work.
56
+ // If the result is in milliseconds: /1000 seconds → stored as seconds → comparisons work.
57
+ // Both are acceptable! The daemon's heartbeat math uses the same timestamp() call
58
+ // for both writing AND reading, so the unit cancels out as long as it's consistent.
46
59
  //
47
- // We MATCH a localDateTime close to now and check the magnitude of timestamp().
48
- // If it's > 1e14 it's microseconds. If it's between 1e11 and 1e14 it's milliseconds.
49
- // Anything else is unexpected.
60
+ // We emit a WARN (not hard-exit) if the unit is unexpected, because:
61
+ // - The daemon has operated correctly on this Memgraph for months
62
+ // - A hard-exit here blocks all 10,000+ users if Memgraph version changes
63
+ // - The actual zombie-detection math is self-consistent regardless of unit
50
64
 
51
65
  const tsResult = await mgQuery(
52
- `WITH datetime('2020-01-01T00:00:00Z') AS refDt
53
- RETURN timestamp(refDt) AS ts`,
66
+ `RETURN timestamp() AS ts`,
54
67
  {}
55
68
  );
56
69
 
@@ -67,27 +80,19 @@ async function verifyMemgraphConfig(mgQuery) {
67
80
  throw new Error(`Memgraph timestamp() returned non-numeric value: ${JSON.stringify(rawTs)}`);
68
81
  }
69
82
 
70
- // 2020-01-01T00:00:00 reference values:
71
- const REF_MS = 1577836800000; // milliseconds
72
- const REF_US = 1577836800000000; // microseconds
73
-
74
- const diffFromMs = Math.abs(ts - REF_MS);
75
- const diffFromUs = Math.abs(ts - REF_US);
76
-
83
+ // Microseconds: current time > 1e15 (16+ digits)
84
+ // Milliseconds: current time > 1e12 (13 digits)
77
85
  let timestampUnit;
78
- if (diffFromUs < diffFromMs) {
86
+ if (ts > 1e14) {
79
87
  timestampUnit = 'microseconds';
80
- } else if (diffFromMs < diffFromUs) {
81
- // Memgraph returning milliseconds this is wrong for our math
82
- throw new Error(
83
- `Memgraph timestamp() appears to return MILLISECONDS (got ${ts}, expected ~${REF_US}). ` +
84
- 'Helios zombie-detection math assumes microseconds. ' +
85
- 'Check Memgraph version — Memgraph 3.x returns microseconds; Neo4j returns milliseconds.'
86
- );
88
+ } else if (ts > 1e11) {
89
+ // Milliseconds not the expected Memgraph v3 behaviour, but non-fatal.
90
+ // Zombie-detection still works: both SET and GET use the same timestamp() call.
91
+ timestampUnit = 'milliseconds';
92
+ // Log via return value so caller can surface it; do NOT exit.
87
93
  } else {
88
- throw new Error(
89
- `Cannot determine Memgraph timestamp() unit: got ${ts}. Expected ~${REF_US} (µs) or ~${REF_MS} (ms).`
90
- );
94
+ // Completely unexpected range — warn, do not exit
95
+ timestampUnit = `unknown(${ts})`;
91
96
  }
92
97
 
93
98
  // ── Step 3: verify toInteger() works (used in zombie thresholds) ────────────
@@ -44,25 +44,15 @@ function makeDriftDetector({ mgQuery, broadcast }) {
44
44
  /**
45
45
  * Lazy Bedrock client — returns null if init fails (non-fatal).
46
46
  */
47
- function getClient() {
48
- if (_client) return _client;
49
- try {
50
- const base = new BedrockRuntimeClient({ region: REGION });
47
+ function getClient() {
48
+ if (_client) return _client;
51
49
  try {
52
- const { HeadroomProxyManager } = require('./headroom-proxy-manager');
53
- const baseUrl = HeadroomProxyManager.getInstance().getBaseUrl();
54
- if (baseUrl) {
55
- const { withHeadroom } = require('headroom-ai/anthropic');
56
- _client = withHeadroom(base, { baseUrl });
57
- return _client;
58
- }
59
- } catch (_) {}
60
- _client = base;
61
- return _client;
62
- } catch {
63
- return null;
50
+ _client = new BedrockRuntimeClient({ region: REGION });
51
+ return _client;
52
+ } catch {
53
+ return null;
54
+ }
64
55
  }
65
- }
66
56
 
67
57
  /**
68
58
  * Call Bedrock ConverseCommand with a text prompt.
@@ -38,20 +38,7 @@ function makeSemanticUpdater(deps) {
38
38
  function getClient() {
39
39
  if (_client) return _client;
40
40
  try {
41
- const base = new BedrockRuntimeClient({ region: REGION });
42
- // Route through Headroom compression proxy so CacheAligner and SmartCrusher
43
- // apply to every Bedrock call made by the semantic update loop.
44
- // getBaseUrl() is read at construction time; proxy restarts update the singleton.
45
- try {
46
- const { HeadroomProxyManager } = require('./headroom-proxy-manager');
47
- const baseUrl = HeadroomProxyManager.getInstance().getBaseUrl();
48
- if (baseUrl) {
49
- const { withHeadroom } = require('headroom-ai/anthropic');
50
- _client = withHeadroom(base, { baseUrl });
51
- return _client;
52
- }
53
- } catch (_) {}
54
- _client = base;
41
+ _client = new BedrockRuntimeClient({ region: REGION });
55
42
  return _client;
56
43
  } catch (e) {
57
44
  return null;
@@ -12,6 +12,10 @@ const fs = require('fs');
12
12
  const os = require('os');
13
13
  const https = require('https');
14
14
  const { spawn } = require('child_process');
15
+ // Use canonical path constants — single source of truth for all daemon modules.
16
+ // This ensures tokens written here are readable by inbox.js (gmailConnected check)
17
+ // and extensions/email/ (backfill, token refresh) without env-var coordination.
18
+ const { HELIOS_ROOT: _HELIOS_ROOT, GMAIL_ACCOUNTS_DIR: _PATHS_GMAIL_DIR } = require('../lib/paths');
15
19
 
16
20
  const INBOX_DOG_CLIENT_ID = process.env.INBOX_DOG_CLIENT_ID;
17
21
  const INBOX_DOG_CLIENT_SECRET = process.env.INBOX_DOG_CLIENT_SECRET;
@@ -20,12 +24,12 @@ if (!INBOX_DOG_CLIENT_ID || !INBOX_DOG_CLIENT_SECRET) {
20
24
  console.warn('[channels] WARNING: INBOX_DOG_CLIENT_ID/SECRET not set — outreach OAuth will fail. See setup.env.example.');
21
25
  }
22
26
 
23
- // Migration shim: prefer env override, then check legacy ~/.pi/outreach path, fall back to new location.
27
+ // ACCOUNTS_DIR: use paths.js canonical location (~/.helios/gmail-accounts) so that
28
+ // inbox.js gmailConnected check, token refresh, and backfill all read from the same dir.
29
+ // Legacy ~/.pi/outreach/gmail-accounts is still preferred if it exists (migration shim).
24
30
  const _ACCOUNTS_DIR_LEGACY = path.join(os.homedir(), '.pi', 'outreach', 'gmail-accounts');
25
- const _ACCOUNTS_DIR_NEW = process.env.HELIOS_GMAIL_ACCOUNTS_PATH
26
- || path.join(os.homedir(), 'helios-agent', 'data', 'gmail-accounts');
27
31
  const ACCOUNTS_DIR = process.env.HELIOS_GMAIL_DIR
28
- || (fs.existsSync(_ACCOUNTS_DIR_LEGACY) ? _ACCOUNTS_DIR_LEGACY : _ACCOUNTS_DIR_NEW);
32
+ || (fs.existsSync(_ACCOUNTS_DIR_LEGACY) ? _ACCOUNTS_DIR_LEGACY : _PATHS_GMAIL_DIR);
29
33
  const ACCOUNTS_FILE = path.join(ACCOUNTS_DIR, 'accounts.json');
30
34
  // Serialise all accounts.json mutations to prevent concurrent-write corruption
31
35
  let _accountsWriteLock = Promise.resolve();
@@ -36,7 +40,8 @@ function withAccountsLock(fn) {
36
40
 
37
41
  fs.mkdirSync(ACCOUNTS_DIR, { recursive: true });
38
42
  const OAUTH_CALLBACK_PORT = parseInt(process.env.HELIOS_OAUTH_CALLBACK_PORT || '18923', 10);
39
- const HELIOS_ROOT = process.env.HELIOS_ROOT || path.join(os.homedir(), 'helios-agent');
43
+ // Use HELIOS_ROOT from paths.js (repo root, not daemon/ subdirectory)
44
+ const HELIOS_ROOT = _HELIOS_ROOT;
40
45
  const SLACK_CONFIG_PATH = path.join(HELIOS_ROOT, 'data', 'slack-config.json');
41
46
  const TRIAGE_PID_FILE = path.join(HELIOS_ROOT, 'data', 'triage.pid');
42
47
 
@@ -69,52 +69,6 @@ function rowFromMg(r, ...fields) {
69
69
 
70
70
  // ── LLM streaming (Anthropic → OpenAI fallback) ──────────────────────────────
71
71
 
72
- /**
73
- * Resolve request options for an Anthropic API call.
74
- * When the Headroom compression proxy is running, routes through it (http://127.0.0.1:PORT).
75
- * Falls back to direct api.anthropic.com call if proxy is not up.
76
- * Returns { module, opts } where module is the Node http/https module to use.
77
- */
78
- function _buildAnthropicOpts(apiKey, bodyLength) {
79
- try {
80
- const http = require('http');
81
- const { HeadroomProxyManager } = require('../lib/headroom-proxy-manager');
82
- const baseUrl = HeadroomProxyManager.getInstance().getBaseUrl();
83
- if (baseUrl) {
84
- const url = new URL(baseUrl);
85
- return {
86
- module: http,
87
- opts: {
88
- hostname: url.hostname,
89
- port: parseInt(url.port || '8787', 10),
90
- path: '/v1/messages',
91
- method: 'POST',
92
- headers: {
93
- 'Content-Type': 'application/json',
94
- 'x-api-key': apiKey,
95
- 'anthropic-version': '2023-06-01',
96
- 'Content-Length': bodyLength,
97
- },
98
- },
99
- };
100
- }
101
- } catch (_) {}
102
- return {
103
- module: https,
104
- opts: {
105
- hostname: 'api.anthropic.com',
106
- path: '/v1/messages',
107
- method: 'POST',
108
- headers: {
109
- 'Content-Type': 'application/json',
110
- 'x-api-key': apiKey,
111
- 'anthropic-version': '2023-06-01',
112
- 'Content-Length': bodyLength,
113
- },
114
- },
115
- };
116
- }
117
-
118
72
  function streamAnthropic(apiKey, messages, onDelta, onDone, onError) {
119
73
  return new Promise((resolve) => {
120
74
  const body = JSON.stringify({
@@ -123,8 +77,17 @@ function streamAnthropic(apiKey, messages, onDelta, onDone, onError) {
123
77
  stream: true,
124
78
  messages,
125
79
  });
126
- const { module: reqModule, opts } = _buildAnthropicOpts(apiKey, Buffer.byteLength(body));
127
- const req = reqModule.request(opts, (resp) => {
80
+ const req = https.request({
81
+ hostname: 'api.anthropic.com',
82
+ path: '/v1/messages',
83
+ method: 'POST',
84
+ headers: {
85
+ 'Content-Type': 'application/json',
86
+ 'x-api-key': apiKey,
87
+ 'anthropic-version': '2023-06-01',
88
+ 'Content-Length': Buffer.byteLength(body),
89
+ },
90
+ }, (resp) => {
128
91
  let buf = '';
129
92
  resp.on('data', (chunk) => {
130
93
  buf += chunk.toString();