@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
@@ -14,6 +14,7 @@ const { randomUUID } = crypto;
14
14
  const { HELIOS_ROOT } = require('../lib/paths');
15
15
  const { CompanyBeliefService } = require('../lib/company-belief-service');
16
16
  const { CompanyBeliefDiscovery } = require('../lib/company-belief-discovery');
17
+ const hboStore = require('../../lib/hbo-core-store');
17
18
 
18
19
 
19
20
  const HBO_ROOT = path.resolve(__dirname, '../../skills/helios-business-operator');
@@ -648,20 +649,42 @@ async function handleGetMandala(req, res, ctx) {
648
649
  // Build the pillar MATCH differently depending on whether goalId is provided.
649
650
  // Memgraph requires extracting all properties into aliases BEFORE OPTIONAL MATCH;
650
651
  // referencing gp.property AFTER OPTIONAL MATCH causes "Unbound variable: gp."
651
- const pillarQuery = goalId
652
- ? `MATCH (gp:GoalPillar {companyId: $cid, goalId: $goalId})
653
- WITH gp.id AS id, gp.name AS name, gp.pillarIndex AS pillarIndex,
654
- gp.goalId AS goalId, gp.description AS description, gp AS gpNode
655
- OPTIONAL MATCH (gpNode)-[:HAS_CELL]->(ac:ActionCell)
656
- RETURN id, name, pillarIndex, goalId, description,
657
- collect({id: ac.id, action: ac.action, status: ac.status, weekIndex: ac.weekIndex}) AS cells
658
- ORDER BY pillarIndex ASC LIMIT toInteger(8)`
659
- : `MATCH (gp:GoalPillar {companyId: $cid})
660
- WITH gp.id AS id, gp.name AS name, gp.pillarIndex AS pillarIndex,
661
- gp.goalId AS goalId, gp.description AS description, gp AS gpNode
662
- OPTIONAL MATCH (gpNode)-[:HAS_CELL]->(ac:ActionCell)
663
- RETURN id, name, pillarIndex, goalId, description,
664
- collect({id: ac.id, action: ac.action, status: ac.status, weekIndex: ac.weekIndex}) AS cells
652
+ const pillarQuery = goalId
653
+ ? `MATCH (gp:GoalPillar {companyId: $cid, goalId: $goalId})
654
+ WITH gp.id AS id, gp.name AS name, gp.pillarIndex AS pillarIndex,
655
+ gp.goalId AS goalId, gp.description AS description,
656
+ coalesce(gp.l2ReviewStatus, null) AS l2ReviewStatus,
657
+ coalesce(gp.l2Strategy, null) AS l2Strategy,
658
+ gp AS gpNode
659
+ OPTIONAL MATCH (gpNode)-[:HAS_CELL]->(ac:ActionCell)
660
+ OPTIONAL MATCH (gpNode)<-[:FOR_PILLAR]-(approval:Approval)
661
+ WHERE approval.type IN ['harada_l2_review','harada_l3_review'] AND approval.status = 'pending'
662
+ WITH id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy, gpNode,
663
+ collect(DISTINCT {id: ac.id, description: ac.description, status: ac.status, cellIndex: ac.cellIndex}) AS cells,
664
+ count(DISTINCT approval) AS openReviewCount
665
+ WITH id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy, cells, openReviewCount,
666
+ size(cells) AS cellCount,
667
+ reduce(acc = 0, c IN cells | acc + CASE WHEN c.status = 'closed' THEN 1 ELSE 0 END) AS closedCellCount
668
+ RETURN id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy,
669
+ cells, openReviewCount, cellCount, closedCellCount
670
+ ORDER BY pillarIndex ASC LIMIT toInteger(8)`
671
+ : `MATCH (gp:GoalPillar {companyId: $cid})
672
+ WITH gp.id AS id, gp.name AS name, gp.pillarIndex AS pillarIndex,
673
+ gp.goalId AS goalId, gp.description AS description,
674
+ coalesce(gp.l2ReviewStatus, null) AS l2ReviewStatus,
675
+ coalesce(gp.l2Strategy, null) AS l2Strategy,
676
+ gp AS gpNode
677
+ OPTIONAL MATCH (gpNode)-[:HAS_CELL]->(ac:ActionCell)
678
+ OPTIONAL MATCH (gpNode)<-[:FOR_PILLAR]-(approval:Approval)
679
+ WHERE approval.type IN ['harada_l2_review','harada_l3_review'] AND approval.status = 'pending'
680
+ WITH id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy, gpNode,
681
+ collect(DISTINCT {id: ac.id, description: ac.description, status: ac.status, cellIndex: ac.cellIndex}) AS cells,
682
+ count(DISTINCT approval) AS openReviewCount
683
+ WITH id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy, cells, openReviewCount,
684
+ size(cells) AS cellCount,
685
+ reduce(acc = 0, c IN cells | acc + CASE WHEN c.status = 'closed' THEN 1 ELSE 0 END) AS closedCellCount
686
+ RETURN id, name, pillarIndex, goalId, description, l2ReviewStatus, l2Strategy,
687
+ cells, openReviewCount, cellCount, closedCellCount
665
688
  ORDER BY pillarIndex ASC LIMIT toInteger(8)`;
666
689
 
667
690
  const saQuery = agentId
@@ -715,7 +738,20 @@ async function handleGetMandala(req, res, ctx) {
715
738
  mgQuery(deptAgentQuery, { cid }).catch(() => ({ rows: [], keys: [] })),
716
739
  ]);
717
740
 
718
- const pillarKeys = ['id', 'name', 'pillarIndex', 'goalId', 'description', 'cells'];
741
+ // Map pillar names to department slugs GoalPillar nodes never have a .department
742
+ // property written to them, so we derive the slug from the pillar's canonical name.
743
+ const PILLAR_NAME_TO_DEPT_SLUG = {
744
+ 'Revenue & Sales': 'sales',
745
+ 'Product & Delivery': 'engineering',
746
+ 'Customer Success': 'customer_success',
747
+ 'Technology & Infrastructure': 'engineering',
748
+ 'Team & Capability': 'people',
749
+ 'Brand & Positioning': 'marketing',
750
+ 'Finance & Sustainability': 'finance',
751
+ 'Operations & Process': 'operations',
752
+ };
753
+
754
+ const pillarKeys = ['id', 'name', 'pillarIndex', 'goalId', 'description', 'l2ReviewStatus', 'l2Strategy', 'cells', 'openReviewCount', 'cellCount', 'closedCellCount'];
719
755
  const saKeys = ['id', 'agentId', 'strengths', 'weaknesses', 'opportunities', 'createdAt'];
720
756
  const cascadeKeys = ['id', 'agentId', 'weekNumber', 'weeklyGoal', 'dailyTasks', 'completionRate'];
721
757
  const deptAgentKeys = ['id', 'role', 'departmentId', 'status', 'currentTaskId', 'currentTask', 'taskStartedAt'];
@@ -733,7 +769,13 @@ async function handleGetMandala(req, res, ctx) {
733
769
  });
734
770
 
735
771
  jsonOk(res, {
736
- pillars: parseRows(pillarResult).map(r => rowToObj(r, pillarKeys)),
772
+ pillars: parseRows(pillarResult).map(r => {
773
+ const p = rowToObj(r, pillarKeys);
774
+ // Derive department slug from pillar name so the frontend can build dept nav items.
775
+ // GoalPillar.department is indexed in the schema but never written by any daemon path.
776
+ p.department = PILLAR_NAME_TO_DEPT_SLUG[p.name] || null;
777
+ return p;
778
+ }),
737
779
  selfAnalysis: parseRows(selfAnalysisResult).map(r => rowToObj(r, saKeys)),
738
780
  cascades: parseRows(cascadeResult).map(r => rowToObj(r, cascadeKeys)),
739
781
  deptAgents, // live BusinessAgent + Task join for DepartmentPage TeamTab
@@ -1172,13 +1214,20 @@ async function handleCreatePdsa(req, res, ctx) {
1172
1214
  // in every department's decisions tab regardless of which pillar they belong to.
1173
1215
  // Task.pillarId is set by pillar-dispatcher.js when a task is dispatched to a pillar.
1174
1216
  try {
1175
- const taskRows = await mgQuery(
1176
- `MATCH (t:Task {id: $taskId, companyId: $cid}) RETURN t.pillarId AS pillarId LIMIT 1`,
1177
- { taskId: taskId.trim(), cid }
1178
- );
1179
- const pillarId = taskRows && taskRows.rows && taskRows.rows[0]
1180
- ? (Array.isArray(taskRows.rows[0]) ? taskRows.rows[0][0] : taskRows.rows[0].pillarId)
1181
- : null;
1217
+ // SQLite-first task read for pillarId (P2-10)
1218
+ let pillarId = null;
1219
+ const _storeTask = hboStore.getTask ? hboStore.getTask(taskId.trim(), cid) : null;
1220
+ if (_storeTask) {
1221
+ pillarId = _storeTask.pillarId ?? null;
1222
+ } else {
1223
+ const taskRows = await mgQuery(
1224
+ `MATCH (t:Task {id: $taskId, companyId: $cid}) RETURN t.pillarId AS pillarId LIMIT 1`,
1225
+ { taskId: taskId.trim(), cid }
1226
+ );
1227
+ pillarId = taskRows && taskRows.rows && taskRows.rows[0]
1228
+ ? (Array.isArray(taskRows.rows[0]) ? taskRows.rows[0][0] : taskRows.rows[0].pillarId)
1229
+ : null;
1230
+ }
1182
1231
  if (pillarId && typeof pillarId === 'string' && pillarId.trim()) {
1183
1232
  await mgQuery(
1184
1233
  `MATCH (p:PDSACycle {id: $id, companyId: $cid}) SET p.pillarId = $pillarId`,
@@ -2126,6 +2175,7 @@ async function handleResolveEmailInfraApproval(req, res, ctx, approvalId) {
2126
2175
  */
2127
2176
  async function handleGetHboApprovals(req, res, ctx) {
2128
2177
  try {
2178
+ if (!ctx.mgQuery) { jsonErr(res, 503, 'Memgraph not connected'); return; }
2129
2179
  const url = new URL(req.url, 'http://localhost');
2130
2180
  const status = url.searchParams.get('status') || null;
2131
2181
  const type = url.searchParams.get('type') || null;
@@ -2149,6 +2199,17 @@ async function handleGetHboApprovals(req, res, ctx) {
2149
2199
  agentId: p?.agentId ?? null, taskId: p?.taskId ?? null,
2150
2200
  urgency: p?.urgency ?? null, humanAnswer: p?.humanAnswer ?? null,
2151
2201
  answeredVia: p?.answeredVia ?? null, createdAt: p?.createdAt ?? null,
2202
+ description: p?.description ?? null,
2203
+ question: p?.question ?? null,
2204
+ defaultAnswer: p?.defaultAnswer ?? null,
2205
+ ruleStrength: p?.ruleStrength ?? null,
2206
+ sourceAgent: p?.sourceAgent ?? null,
2207
+ pillarId: p?.pillarId ?? null,
2208
+ expiresAt: p?.expiresAt ?? null,
2209
+ kaizenProposalId: p?.kaizenProposalId ?? null,
2210
+ department: p?.department ?? null,
2211
+ templateKey: p?.templateKey ?? null,
2212
+ inferredAnswer: p?.inferredAnswer ?? null,
2152
2213
  };
2153
2214
  });
2154
2215
  jsonOk(res, { approvals, count: approvals.length });
@@ -2338,14 +2399,14 @@ async function handleGetTriageSummary(req, res, ctx) {
2338
2399
  { cid }
2339
2400
  );
2340
2401
  const rows = parseRows(result);
2341
- const counts = { p0: 0, p1: 0, p2: 0, p3: 0, total: 0 };
2402
+ const counts = { p0Count: 0, p1Count: 0, p2Count: 0, p3Count: 0, total: 0, topItems: [], generatedAt: new Date().toISOString() };
2342
2403
  for (const row of rows) {
2343
2404
  const pri = (row[0] ?? row['priority'] ?? '').toString().toUpperCase();
2344
2405
  const cnt = Number(row[1] ?? row['cnt'] ?? 0);
2345
- if (pri === 'P0') counts.p0 = cnt;
2346
- else if (pri === 'P1') counts.p1 = cnt;
2347
- else if (pri === 'P2') counts.p2 = cnt;
2348
- else if (pri === 'P3') counts.p3 = cnt;
2406
+ if (pri === 'P0') counts.p0Count = cnt;
2407
+ else if (pri === 'P1') counts.p1Count = cnt;
2408
+ else if (pri === 'P2') counts.p2Count = cnt;
2409
+ else if (pri === 'P3') counts.p3Count = cnt;
2349
2410
  counts.total += cnt;
2350
2411
  }
2351
2412
  jsonOk(res, counts);
File without changes
@@ -551,13 +551,14 @@ module.exports = function createProjectRoute({ mgQuery, broadcast, invalidateCon
551
551
  const { answer, answeredBy } = body;
552
552
  if (!answer) return jsonErr(res, 400, 'answer required', req);
553
553
  try {
554
- await mgQuery(
554
+ const mgResult = await mgQuery(
555
555
  `MATCH (q:ProjectQuestion {id: $qid, projectId: $projId, companyId: $cid})
556
- SET q.answer=$answer, q.answeredBy=$by, q.answeredAt=datetime(), q.status='answered'`,
556
+ SET q.answer=$answer, q.answeredBy=$by, q.answeredAt=datetime(), q.status='answered'
557
+ RETURN count(q) AS updated`,
557
558
  { qid, projId: projectId, cid: ctx?.cid || "", answer: String(answer), by: answeredBy || null }
558
559
  );
559
560
  // D2: if no row matched, task not found for this company
560
- const _updated = taskResult?.rows?.[0]?.[0] ?? taskResult?.rows?.[0]?.updated ?? 0;
561
+ const _updated = mgResult?.rows?.[0]?.[0] ?? mgResult?.rows?.[0]?.updated ?? 0;
561
562
  if (!Number(_updated)) return jsonErr(res, 404, "Task not found for this company", req);
562
563
  jsonOk(res, { ok: true }, req);
563
564
  } catch (e) {
@@ -184,9 +184,16 @@ async function handlePostWizard(req, res, ctx) {
184
184
  }
185
185
  }
186
186
 
187
- // BUG-21 fix: fire tickGoalSync immediately after wizard completes so the
188
- // first AgentReadySignal dispatch happens within the current tick (~30s)
189
- // instead of waiting up to 2.5 min (5 ticks × 30s) for the next goal sync cycle.
187
+ // tickGoalDecompose (creates GoalPillar × 8) and wizard:pillars_ready broadcast are
188
+ // now handled in helios-company-daemon.js registerCompany(), which runs after the
189
+ // desktop calls POST /api/daemon/register-company. That is the correct point because:
190
+ // (a) _modulesByCompany is guaranteed to have this company's entry
191
+ // (b) daemon._broadcast is wired before setBroadcast() is called
192
+ // (c) No race with the setImmediate firing before registerCompany completes
193
+ //
194
+ // tickGoalSync bridges any BusinessTask→Task nodes created by the wizard (CEO task).
195
+ // It still runs here because it needs to fire as soon as possible after wizard:done,
196
+ // not after the desktop round-trip to /api/daemon/register-company.
190
197
  if (result.companyId && ctx.daemon?._modulesByCompany) {
191
198
  setImmediate(async () => {
192
199
  try {
@@ -194,7 +201,7 @@ async function handlePostWizard(req, res, ctx) {
194
201
  if (mods?.hboBridge?.tickGoalSync) {
195
202
  await mods.hboBridge.tickGoalSync();
196
203
  }
197
- } catch (_) { /* non-fatal — tickGoalSync will fire on next cycle */ }
204
+ } catch (_) { /* non-fatal */ }
198
205
  });
199
206
  }
200
207
 
File without changes
File without changes
@@ -345,35 +345,55 @@ export default function contextCompaction(pi: ExtensionAPI): void {
345
345
  metrics.compactionTriggered++;
346
346
  metrics.lastCompactionAt = Date.now();
347
347
 
348
- // ── Headroom compression (L1 + L2 replacement) ────────────────────────
349
- // Uses the local Headroom proxy (SmartCrusher + IntelligentContext + CCR)
350
- // instead of the hand-rolled L1 tool-result clearing and L2 extractive
351
- // summarization. Headroom's content-routing selects the right compressor
352
- // per content type (JSON arrays → SmartCrusher, prose → Kompress-base).
353
- // CCR stores originals locally so the agent can retrieve them on demand.
348
+ // ── Helios Compression (L1 + L2 replacement) ────────────────────────
349
+ // Calls the Helios Compression Server (lib/compression/server.ts) via
350
+ // HTTP. The server runs as a sidecar process managed by HeadroomProxyManager.
351
+ // Its URL is injected into the Pi subprocess env as HEADROOM_PROXY_URL.
354
352
  //
355
- // This fires on the same threshold as the old L1/L2 — no behaviour change
356
- // for users, but compression quality is dramatically better:
357
- // - SmartCrusher preserves statistical distribution (30% start, 55%
358
- // importance-scored, 15% end) and always keeps errors/anomalies.
359
- // - IntelligentContext scores on 6 dimensions instead of oldest-first.
360
- // - CacheAligner stabilizes Bedrock KV-cache prefix on every call.
353
+ // No npm package required uses Node's built-in http module.
354
+ // Works identically on Windows and macOS (the server is pure TypeScript).
355
+ //
356
+ // SmartCrusher preserves statistical distribution:
357
+ // Lossless: CSV format for homogeneous arrays (51–84% savings)
358
+ // Lossy: 30% start + 55% importance-scored + 15% end kept;
359
+ // dropped rows stored in CCR and retrievable on demand.
361
360
  let headroomApplied = false;
362
361
  let headroomTokensSaved = 0;
363
362
 
364
363
  try {
365
- // Dynamic import headroom-ai is a regular npm dep, not a Pi module.
366
- // eslint-disable-next-line @typescript-eslint/no-require-imports
367
- const headroomAi = require('headroom-ai');
368
- // eslint-disable-next-line @typescript-eslint/no-require-imports
369
- const { HeadroomProxyManager } = require('../daemon/lib/headroom-proxy-manager');
370
- const baseUrl = HeadroomProxyManager.getInstance().getBaseUrl();
371
-
372
- if (baseUrl && headroomAi?.compress) {
373
- const result = await headroomAi.compress(messages, {
374
- model: modelId || 'claude',
375
- baseUrl,
376
- tokenBudget: compactionThreshold,
364
+ const baseUrl = process.env.HEADROOM_PROXY_URL || process.env.ANTHROPIC_BASE_URL;
365
+ if (baseUrl && baseUrl.includes('127.0.0.1')) {
366
+ // POST /headroom/compress with the current messages
367
+ const payload = JSON.stringify({ messages });
368
+ const result: any = await new Promise((resolve, reject) => {
369
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
370
+ const http = require('http');
371
+ const url = new URL('/headroom/compress', baseUrl);
372
+ const req = http.request(
373
+ {
374
+ hostname: url.hostname,
375
+ port: parseInt(url.port || '8787', 10),
376
+ path: '/headroom/compress',
377
+ method: 'POST',
378
+ headers: {
379
+ 'Content-Type': 'application/json',
380
+ 'Content-Length': Buffer.byteLength(payload),
381
+ },
382
+ },
383
+ (res: any) => {
384
+ let body = '';
385
+ res.on('data', (c: Buffer) => { body += c; });
386
+ res.on('end', () => {
387
+ try { resolve(JSON.parse(body)); }
388
+ catch { reject(new Error('Invalid JSON from compression server')); }
389
+ });
390
+ res.on('error', reject);
391
+ }
392
+ );
393
+ req.setTimeout(5000, () => { req.destroy(); reject(new Error('Compression server timeout')); });
394
+ req.on('error', reject);
395
+ req.write(payload);
396
+ req.end();
377
397
  });
378
398
 
379
399
  if (result?.messages?.length) {
@@ -403,8 +423,7 @@ export default function contextCompaction(pi: ExtensionAPI): void {
403
423
  }
404
424
  }
405
425
  } catch (hrErr) {
406
- // Headroom compress failed — this should not happen if the proxy is running.
407
- // Log prominently and fall through to legacy L1 below.
426
+ // Headroom compress failed — log and fall through to legacy L1.
408
427
  process.stderr.write(
409
428
  `[context-compaction] ⚠️ Headroom compress error: ${String(hrErr)}\n` +
410
429
  `[context-compaction] Falling back to legacy L1 tool result clearing.\n`
@@ -230,6 +230,11 @@ export async function spreadActivation(
230
230
  initialActivation: EnrichedActivation,
231
231
  graphClient?: { safeRead: (q: string, p?: any) => Promise<any[]> } | null,
232
232
  ): Promise<EnrichedActivation> {
233
+ const { isMemgraphAvailable } = require('../../lib/graph-availability');
234
+ if (!isMemgraphAvailable()) {
235
+ process.stderr.write('[cortex:activation-bridge] Memgraph unavailable — skipping activation spread\n');
236
+ return {};
237
+ }
233
238
  if (!initialActivation.concepts.length) return initialActivation;
234
239
 
235
240
  const mg = graphClient !== undefined ? graphClient : getDefaultMg();
@@ -154,6 +154,32 @@ try {
154
154
  }
155
155
  } catch (err) { /* fail-open: WAL module init */ if (process.env.HELIOS_DEBUG) console.error(`[cortex-learn] WAL init error: ${String(err)}`); }
156
156
 
157
+ // Register availability listener so cortex WAL replays automatically
158
+ // when Memgraph comes back up after a period of unavailability (P4-2).
159
+ // _walReplayInProgress guard prevents concurrent replays racing on the journal
160
+ // file when Memgraph flaps rapidly (F13).
161
+ let _walReplayInProgress = false;
162
+ try {
163
+ const { onAvailabilityChange } = require('../../lib/graph-availability');
164
+ onAvailabilityChange((available: boolean) => {
165
+ if (!available || _walReplayInProgress) return;
166
+ _walReplayInProgress = true;
167
+ // Fire-and-forget: WAL replay runs in background, never blocks dispatch
168
+ import('./wal-replay').then(({ replayCortexWal }) => {
169
+ replayCortexWal()
170
+ .catch((e: unknown) =>
171
+ process.stderr.write(`[cortex-learn] WAL replay failed: ${String(e)}\n`)
172
+ )
173
+ .finally(() => { _walReplayInProgress = false; });
174
+ }).catch(() => {
175
+ // wal-replay not available (e.g. pre-build) — silently skip
176
+ _walReplayInProgress = false;
177
+ });
178
+ });
179
+ } catch (_) {
180
+ // graph-availability not available — WAL replay disabled
181
+ }
182
+
157
183
  function _persistCoherenceCounts(): void {
158
184
  try {
159
185
  const obj: Record<string, number> = {};
File without changes
File without changes
@@ -11,7 +11,7 @@ const HIGH_IMPACT_SHARED_BRANCHES = /\b(main|master|dev|development|staging|stag
11
11
  const HIGH_IMPACT_GIT_OPS = /\b(cherry.?pick|git\s+push|force.?push|git\s+merge|merge\s+(?:to|into)\s+|push\s+to\s+(?:origin|remote|upstream)|git\s+rebase|git\s+reset\s+--hard)\b/i;
12
12
  const HIGH_IMPACT_AUTH_CRYPTO = /\b(?:auth|crypto|session|oauth|jwt|encrypt|decrypt|secret|cipher|password)\.(ts|js|tsx|jsx)\b|(?:session|token|secret|password|credential|encrypt|decrypt|hash|cipher|jwt|oauth)\s+(?:file|module|key|management)/i;
13
13
  const HIGH_IMPACT_INFRA = /\b(?:(?:run|apply|execute|deploy|push|revert|rollback|sync)\s+migration|schema\s+change|drop\s+table|alter\s+table|environment\s+variable|aws\s+ssm|amplify\s+deploy|\.env\b)\b/i;
14
- const HIGH_IMPACT_RUNTIME_UPGRADE = /\b(npm\s+install|npm\s+i|npm\s+update|npm\s+up)\b.*(pi-coding-agent|@mariozechner\/pi|@helios-agent\/pi)/i;
14
+ const HIGH_IMPACT_RUNTIME_UPGRADE = /\b(npm\s+install|npm\s+i|npm\s+update|npm\s+up|pnpm\s+add|pnpm\s+install)\b.*(pi-coding-agent|@mariozechner\/pi|@helios-agent\/pi|@cgh567\/cli|@cgh567\/agent|@cgh567\/pi)/i;
15
15
  export const SHARED_INFRA_PATHS = /(?:~\/|\/Users\/\w+\/)?\.\.?pi\/agent\/(extensions|skills|agents)\//i;
16
16
 
17
17
  export function isHighImpactOperation(task: string): boolean {
@@ -28,15 +28,22 @@ import { homedir } from 'os';
28
28
 
29
29
  // Ordered list of jiti lib/jiti.cjs locations (lib/ not dist/ - dist requires module.createRequire which is unavailable in CJS context)
30
30
  const JITI_PATHS = [
31
+ // ── npm package layout (production: @cgh567/agent installed in node_modules) ──
32
+ // When helios-agent runs from node_modules/@cgh567/agent, jiti is provided by
33
+ // @cgh567/cli which is in the same scope. Walk up to workspace root node_modules.
34
+ // Resolve relative to __dirname (this file's location in extensions/helios-governance/handlers/)
35
+ // node_modules/@cgh567/agent/extensions/helios-governance/handlers/ -> up 6 levels -> workspace root
36
+ join(__dirname, '..', '..', '..', '..', '..', '..', 'node_modules', '@mariozechner', 'jiti', 'lib', 'jiti.cjs'),
37
+ // @cgh567/cli bundles jiti under its own node_modules
38
+ join(__dirname, '..', '..', '..', '..', '..', '..', 'node_modules', '@cgh567', 'cli', 'node_modules', '@mariozechner', 'jiti', 'lib', 'jiti.cjs'),
39
+ // ── local clone layout (dev: ~/Desktop/Helios/helios-agent-main) ──
40
+ join(homedir(), 'Desktop', 'Helios', 'helios-agent-main', 'node_modules', '@mariozechner', 'jiti', 'lib', 'jiti.cjs'),
41
+ join(homedir(), 'helios-agent-main', 'node_modules', '@mariozechner', 'jiti', 'lib', 'jiti.cjs'),
31
42
  join(homedir(), 'helios-agent', 'node_modules', '@mariozechner', 'jiti', 'lib', 'jiti.cjs'),
32
- join(homedir(), 'helios-agent', 'node_modules', '@helios-agent', 'pi-coding-agent', 'node_modules', 'jiti', 'lib', 'jiti.cjs'),
33
- join(homedir(), 'helios-agent', 'node_modules', '@earendil-works', 'pi-coding-agent', 'node_modules', 'jiti', 'lib', 'jiti.cjs'),
34
- join(process.env.NVM_DIR || join(homedir(), '.nvm'),
35
- 'versions', 'node', process.version, 'lib', 'node_modules',
36
- '@helios-agent', 'pi-coding-agent', 'node_modules', 'jiti', 'lib', 'jiti.cjs'),
37
- join(process.env.NVM_DIR || join(homedir(), '.nvm'),
38
- 'versions', 'node', process.version, 'lib', 'node_modules',
39
- '@earendil-works', 'pi-coding-agent', 'node_modules', 'jiti', 'lib', 'jiti.cjs'),
43
+ // ── HELIOS_ROOT env var override ──
44
+ ...(process.env.HELIOS_ROOT
45
+ ? [join(process.env.HELIOS_ROOT, 'node_modules', '@mariozechner', 'jiti', 'lib', 'jiti.cjs')]
46
+ : []),
40
47
  ];
41
48
 
42
49
  /** Module-level jiti instance cache: callerFile → jiti fn */
@@ -23,6 +23,11 @@ export async function queryGraphMemories(
23
23
  taskText: string,
24
24
  limit: number = 10
25
25
  ): Promise<GraphMemoryResult[]> {
26
+ const { isMemgraphAvailable } = require('../../lib/graph-availability');
27
+ if (!isMemgraphAvailable()) {
28
+ process.stderr.write('[hema:graph-memory] Memgraph unavailable — skipping graph memory recall\n');
29
+ return [];
30
+ }
26
31
  try {
27
32
  const mg = require('../../lib/safe-memgraph.js');
28
33
 
@@ -102,6 +107,11 @@ export async function queryCausalLessons(
102
107
  taskText: string,
103
108
  limit: number = 5
104
109
  ): Promise<GraphMemoryResult[]> {
110
+ const { isMemgraphAvailable } = require('../../lib/graph-availability');
111
+ if (!isMemgraphAvailable()) {
112
+ process.stderr.write('[hema:graph-memory] Memgraph unavailable — skipping causal lesson recall\n');
113
+ return [];
114
+ }
105
115
  try {
106
116
  const mg = require('../../lib/safe-memgraph.js');
107
117
 
@@ -1507,48 +1507,67 @@ export default function hemaDispatchV3(pi: any): void {
1507
1507
  logHemaEvent('hema_no_context', { nativePacksUsed, legacyUsed: admitted.admitted?.length > 0 });
1508
1508
  }
1509
1509
 
1510
- // ── Headroom: compress HEMA recall payload before injection ──────────
1511
- // The recall context injected into every subagent call is dominated by
1512
- // JSON graph payloads (leads, signals, tasks, goals, code nodes).
1513
- // SmartCrusher compresses these at 70–90%, preserving errors/anomalies
1514
- // and statistical distribution (30% start / 55% importance / 15% end).
1515
- // This reduces injection cost for all roles and all companies.
1516
- if (enrichedTask.length > 2000) { // only compress if there's meaningful context
1517
- try {
1518
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1519
- const headroomAi = require('headroom-ai');
1520
- // eslint-disable-next-line @typescript-eslint/no-require-imports
1521
- const { HeadroomProxyManager } = require('../../daemon/lib/headroom-proxy-manager');
1522
- const _hrUrl = HeadroomProxyManager.getInstance().getBaseUrl();
1523
- if (_hrUrl && headroomAi?.compress) {
1524
- const _hrResult = await headroomAi.compress(
1525
- [{ role: 'user', content: [{ type: 'text', text: enrichedTask }] }],
1526
- {
1527
- model: 'claude',
1528
- baseUrl: _hrUrl,
1529
- // Use role injection budget as the token budget
1530
- tokenBudget: agentType === 'planner' ? 8000
1531
- : agentType === 'orchestrator' ? 10000
1532
- : agentType === 'worker' ? 6000
1533
- : agentType === 'scout' ? 3000
1534
- : 6000,
1535
- }
1536
- );
1537
- if (_hrResult?.messages?.[0]?.content) {
1538
- const _compressed = typeof _hrResult.messages[0].content === 'string'
1539
- ? _hrResult.messages[0].content
1540
- : _hrResult.messages[0].content?.[0]?.text ?? enrichedTask;
1541
- if (_compressed.length < enrichedTask.length) {
1542
- const _saved = enrichedTask.length - _compressed.length;
1543
- process.stderr.write(`[hema-dispatch-v3] Headroom compressed recall context: -${_saved} chars (${agentType})\n`);
1544
- enrichedTask = _compressed;
1545
- logHemaEvent('hema_headroom_compressed', { saved: _saved, agentType });
1546
- }
1510
+ // ── Helios Compression: compress HEMA recall payload before injection ──
1511
+ // The recall context contains JSON graph payloads (leads, signals, tasks,
1512
+ // goals, code nodes). Send the assembled text as a tool_result block to
1513
+ // the compression server it will find and compress embedded JSON arrays.
1514
+ //
1515
+ // Uses direct HTTP to HEADROOM_PROXY_URL (same pattern as context-compaction.ts)
1516
+ // rather than the headroom-ai npm package so this works in Pi subprocess context.
1517
+ // Applies to all companies: the role injection budgets enforce per-agent limits.
1518
+ if (enrichedTask.length > 2000) {
1519
+ const _hrUrl = process.env.HEADROOM_PROXY_URL;
1520
+ if (_hrUrl) {
1521
+ try {
1522
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1523
+ const http = require('http');
1524
+ const _payload = JSON.stringify({
1525
+ messages: [{
1526
+ role: 'user',
1527
+ content: [{
1528
+ type: 'tool_result',
1529
+ tool_use_id: 'hema_recall',
1530
+ content: enrichedTask,
1531
+ }],
1532
+ }],
1533
+ });
1534
+ const _result: any = await new Promise((resolve, reject) => {
1535
+ const _url = new URL('/headroom/compress', _hrUrl);
1536
+ const _req = http.request(
1537
+ {
1538
+ hostname: _url.hostname,
1539
+ port: parseInt(_url.port || '8787', 10),
1540
+ path: '/headroom/compress',
1541
+ method: 'POST',
1542
+ headers: {
1543
+ 'Content-Type': 'application/json',
1544
+ 'Content-Length': Buffer.byteLength(_payload),
1545
+ },
1546
+ },
1547
+ (res: any) => {
1548
+ let body = '';
1549
+ res.on('data', (c: Buffer) => { body += c; });
1550
+ res.on('end', () => { try { resolve(JSON.parse(body)); } catch { reject(new Error('bad json')); } });
1551
+ res.on('error', reject);
1552
+ }
1553
+ );
1554
+ _req.setTimeout(3000, () => { _req.destroy(); reject(new Error('timeout')); });
1555
+ _req.on('error', reject);
1556
+ _req.write(_payload);
1557
+ _req.end();
1558
+ });
1559
+
1560
+ const _compressed = _result?.messages?.[0]?.content?.[0]?.content ?? enrichedTask;
1561
+ if (typeof _compressed === 'string' && _compressed.length < enrichedTask.length) {
1562
+ const _saved = enrichedTask.length - _compressed.length;
1563
+ process.stderr.write(`[hema-dispatch-v3] Headroom compressed recall context: -${_saved} chars (${agentType})\n`);
1564
+ enrichedTask = _compressed;
1565
+ logHemaEvent('hema_headroom_compressed', { saved: _saved, agentType });
1547
1566
  }
1567
+ } catch (_hrErr: any) {
1568
+ // Non-fatal: log and continue with uncompressed enrichedTask
1569
+ process.stderr.write(`[hema-dispatch-v3] Headroom recall compress skipped: ${_hrErr?.message}\n`);
1548
1570
  }
1549
- } catch (_hrErr: any) {
1550
- // Non-fatal: log and continue with uncompressed enrichedTask
1551
- process.stderr.write(`[hema-dispatch-v3] Headroom recall compress failed (non-fatal): ${_hrErr?.message}\n`);
1552
1571
  }
1553
1572
  }
1554
1573
 
File without changes
File without changes
@@ -23,6 +23,17 @@ import { createRequire } from 'module';
23
23
  const require = createRequire(import.meta.url);
24
24
  const QUIET = process.env.PI_QUIET === '1' || process.env.PI_LOG_LEVEL === 'error';
25
25
 
26
+ // Graph availability singleton — updated whenever Memgraph comes up or goes down
27
+ let _graphAvailability: { setMemgraphAvailable: (v: boolean) => void } | null = null;
28
+ function getGraphAvailability() {
29
+ if (!_graphAvailability) {
30
+ try {
31
+ _graphAvailability = require('../lib/graph-availability');
32
+ } catch (_) {}
33
+ }
34
+ return _graphAvailability;
35
+ }
36
+
26
37
  // ── Event emission helpers ───────────────────────────────────────────────────
27
38
 
28
39
  /** Load MAGE query modules after Bolt becomes reachable.
@@ -109,6 +120,7 @@ function emitMemgraphReady(log: (msg: string) => void): void {
109
120
  bus.emit('memgraph_ready', payload);
110
121
  } catch (e) { process.stderr.write(`[extensions] non-fatal if bus unavailable: ${String(e)}\n`); }
111
122
  log(`[memgraph-autostart] Memgraph ready — bolt reachable at ${BOLT_HOST}:${BOLT_PORT}`);
123
+ getGraphAvailability()?.setMemgraphAvailable(true);
112
124
  }
113
125
 
114
126
  /**
@@ -185,6 +197,7 @@ function emitMemgraphDegraded(log: (msg: string) => void, reason: string): void
185
197
  try {
186
198
  bus.emit('memgraph_degraded', payload);
187
199
  } catch (e) { process.stderr.write(`[extensions] non-fatal: ${String(e)}\n`); }
200
+ getGraphAvailability()?.setMemgraphAvailable(false);
188
201
  }
189
202
 
190
203
  /** Returns true if Memgraph bolt was confirmed reachable during this session. */
File without changes