@cgh567/agent 2.4.3 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/business/talisman-ceo.md +183 -0
- package/agents/business/talisman-comms.md +257 -0
- package/agents/business/talisman-cto.md +153 -0
- package/agents/business/talisman-finance.md +246 -0
- package/agents/business/talisman-marketing.md +240 -0
- package/agents/business/talisman-sales.md +242 -0
- package/agents/business/talisman-support.md +236 -0
- package/bin/helios-rpc.js +19 -0
- package/daemon/adapters/helios-rpc-adapter.js +5 -12
- package/daemon/context-enrichment.js +27 -0
- package/daemon/helios-api.js +290 -45
- package/daemon/helios-company-daemon.js +160 -50
- package/daemon/lib/blast-radius-analyzer.js +75 -0
- package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
- package/daemon/lib/forensic-log.js +113 -0
- package/daemon/lib/goal-research-pipeline.js +644 -0
- package/daemon/lib/harada/cascade-judge.js +84 -1
- package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
- package/daemon/lib/harada/pillar-dispatcher.js +23 -2
- package/daemon/lib/hbo-bridge.js +73 -5
- package/daemon/lib/headroom-middleware.js +129 -0
- package/daemon/lib/headroom-proxy-manager.js +309 -0
- package/daemon/lib/intelligence/department-page-generator.js +46 -1
- package/daemon/lib/interpretation-engine.js +92 -0
- package/daemon/lib/mental-model-cache.js +96 -0
- package/daemon/lib/project-factory.js +47 -0
- package/daemon/lib/session-log-reader.js +93 -0
- package/daemon/lib/standard-work-bootstrap.js +87 -1
- package/daemon/lib/task-completion-processor.js +12 -0
- package/daemon/package.json +2 -1
- package/daemon/routes/agents.js +51 -6
- package/daemon/routes/channels.js +116 -2
- package/daemon/routes/crm.js +85 -0
- package/daemon/routes/dashboard.js +62 -16
- package/daemon/routes/dept.js +10 -1
- package/daemon/routes/email-triage.js +19 -10
- package/daemon/routes/hbo.js +367 -13
- package/daemon/routes/hed.js +133 -0
- package/daemon/routes/inbox.js +397 -8
- package/daemon/routes/project.js +392 -9
- package/daemon/schema-definitions.js +10 -0
- package/daemon/schema-migrations-hbo.js +10 -0
- package/daemon/schema-migrations-proj.js +22 -0
- package/extensions/__tests__/codebase-index.test.ts +73 -0
- package/extensions/__tests__/extension-command-registration.test.ts +35 -0
- package/extensions/__tests__/git-push-guard.test.ts +68 -0
- package/extensions/context-compaction.ts +104 -76
- package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
- package/extensions/email/actions/draft-response.ts +21 -1
- package/extensions/email/auth/accounts.ts +5 -11
- package/extensions/email/auth/inbox-dog.ts +5 -2
- package/extensions/email/backfill.ts +20 -13
- package/extensions/email/providers/gmail.ts +164 -0
- package/extensions/email/providers/google-calendar.ts +34 -5
- package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
- package/extensions/helios-browser/backends/playwright.ts +3 -1
- package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
- package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
- package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
- package/extensions/hema-dispatch-v3/index.ts +33 -65
- package/extensions/interview/__tests__/server.test.ts +117 -0
- package/extensions/lib/helios-root.cjs +46 -0
- package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
- package/extensions/warm-tick/warm-tick-maintenance.ts +156 -0
- package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
- package/lib/__tests__/crash-fixes.test.ts +49 -0
- package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
- package/lib/broker/__tests__/jit-subscription.test.js +44 -1
- package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
- package/lib/compression/__tests__/ccr-store.test.js +138 -0
- package/lib/compression/__tests__/pipeline.test.js +280 -0
- package/lib/compression/__tests__/smart-crusher.test.js +242 -0
- package/lib/compression/dist/server.js +34 -1
- package/lib/compression/dist/start-server.js +77 -0
- package/lib/graph/learning/headroom-learn-bridge.js +175 -0
- package/lib/hbo-core-store.ts +71 -0
- package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
- package/lib/skill-sync.js +6 -1
- package/lib/startup-integrity.js +9 -2
- package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
- package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
- package/lib/triage-core/__tests__/classifier.test.ts +45 -7
- package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
- package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
- package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
- package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
- package/lib/triage-core/__tests__/signals.test.ts +357 -0
- package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
- package/lib/triage-core/backfill-cost-estimator.ts +91 -0
- package/lib/triage-core/backfill-orchestrator.ts +119 -0
- package/lib/triage-core/classifier.ts +38 -6
- package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
- package/lib/triage-core/cos/response-debt.ts +2 -2
- package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
- package/lib/triage-core/graph/batch-persistence.ts +66 -2
- package/lib/triage-core/graph/betweenness-worker.js +75 -0
- package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
- package/lib/triage-core/graph/persistence.ts +1 -1
- package/lib/triage-core/graph/schema-v2.ts +2 -0
- package/lib/triage-core/graph/schema.cypher +1 -0
- package/lib/triage-core/graph/triage-query.ts +1 -1
- package/lib/triage-core/learning.ts +15 -20
- package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
- package/lib/triage-core/mental-model/cos-integration.ts +1 -1
- package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
- package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
- package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
- package/lib/triage-core/mental-model/model-assembler.ts +16 -3
- package/lib/triage-core/orchestrator.ts +4 -4
- package/lib/triage-core/scheduled-sends.ts +39 -2
- package/lib/triage-core/signals/comms-style.ts +1 -1
- package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
- package/lib/triage-core/signals/favee-type.ts +6 -1
- package/lib/triage-core/signals/goal-relevance.ts +31 -2
- package/lib/triage-core/signals/personal-importance.ts +1 -1
- package/lib/triage-core/signals/referral-chain.ts +0 -1
- package/lib/triage-core/signals/relationship-decay.ts +4 -0
- package/lib/triage-core/signals/relationship-health.ts +6 -1
- package/lib/triage-core/signals/trajectory-signal.ts +38 -3
- package/lib/triage-core/tournament-runner.js +11 -1
- package/lib/triage-core/triage-llm-factory.ts +110 -0
- package/lib/triage-core/triage-local-llm.ts +145 -0
- package/lib/triage-core/triage-sql-store.ts +337 -0
- package/lib/triage-core/types.ts +2 -2
- package/lib/unified-graph.atomic.test.ts +52 -0
- package/lib/unified-graph.failure-categories.test.ts +55 -0
- package/package.json +10 -3
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/skills/helios-bookkeeping/SKILL.md +321 -0
- package/skills/helios-briefer/SKILL.md +44 -0
- package/skills/helios-client-relations/SKILL.md +322 -0
- package/skills/helios-personal-triager/SKILL.md +45 -0
- package/skills/helios-recruitment/SKILL.md +317 -0
- package/skills/helios-relationship-nudger/SKILL.md +77 -0
- package/skills/helios-researcher/SKILL.md +44 -0
- package/skills/helios-scheduler/SKILL.md +58 -0
- package/skills/helios-tax-analyst/SKILL.md +280 -0
- package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -1823
|
@@ -390,6 +390,35 @@ Market benchmarks: ${benchmarks}`;
|
|
|
390
390
|
|
|
391
391
|
this._log(`[CascadeJudge] L2 verdict for pillar ${pillarId}: ${verdict}${critique ? ' — ' + critique : ''}`);
|
|
392
392
|
|
|
393
|
+
// T3-06: On PASS, dispatch L3 research for all unreviewed ActionCells in this pillar.
|
|
394
|
+
if (verdict === 'pass') {
|
|
395
|
+
setImmediate(async () => {
|
|
396
|
+
try {
|
|
397
|
+
const cells = await this._mg(
|
|
398
|
+
`MATCH (ac:ActionCell {pillarId: $pillarId, companyId: $cid})
|
|
399
|
+
WHERE ac.l3ReviewStatus IS NULL OR ac.l3ReviewStatus = ''
|
|
400
|
+
RETURN ac.id AS id, ac.agentId AS agentId
|
|
401
|
+
LIMIT 8`,
|
|
402
|
+
{ pillarId, cid: this._companyId }
|
|
403
|
+
).catch(() => null);
|
|
404
|
+
if (!cells?.rows?.length) return;
|
|
405
|
+
const { CascadeResearchDispatcher } = require('./cascade-research-dispatcher');
|
|
406
|
+
const crd = new CascadeResearchDispatcher(this._mg.bind(this), this._companyId);
|
|
407
|
+
for (const cr of cells.rows) {
|
|
408
|
+
const cellId = cr[0] ?? cr['id'];
|
|
409
|
+
const cellAgentId = cr[1] ?? cr['agentId'];
|
|
410
|
+
if (!cellId) continue;
|
|
411
|
+
await crd.dispatchL3Research(cellId, pillarId, this._companyId, cellAgentId || null).catch(e =>
|
|
412
|
+
this._log(`[CascadeJudge] T3-06: L3 dispatch failed for cell ${cellId}: ${e && e.message || e}`)
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
this._log(`[CascadeJudge] T3-06: dispatched L3 research for ${cells.rows.length} ActionCells of pillar ${pillarId}`);
|
|
416
|
+
} catch (t3Err) {
|
|
417
|
+
this._log(`[CascadeJudge] T3-06: L3 dispatch error: ${t3Err && t3Err.message || t3Err}`);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
393
422
|
// 7/9. Return verdict — caller dispatches L3 (pass) or revised L2 (fail)
|
|
394
423
|
return { verdict, criteriaResults, critique };
|
|
395
424
|
} catch (err) {
|
|
@@ -607,12 +636,66 @@ Sub-agent execution plan: ${l3Content}`;
|
|
|
607
636
|
|
|
608
637
|
this._log(`[CascadeJudge] L3 verdict for cell ${cellId}: ${verdict}${critique ? ' — ' + critique : ''}`);
|
|
609
638
|
|
|
610
|
-
|
|
639
|
+
return { verdict, criteriaResults, critique };
|
|
611
640
|
} catch (err) {
|
|
612
641
|
this._log(`[CascadeJudge] judgeL3 error for cell ${cellId}: ${err && err.message || err}`);
|
|
613
642
|
throw err;
|
|
614
643
|
}
|
|
615
644
|
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* T3-04: Bulk scan — judge all GoalPillar nodes with l2ReviewStatus='pending_review'.
|
|
648
|
+
* Called every 5 ticks from helios-company-daemon.js tick loop.
|
|
649
|
+
* Processes up to 5 pillars per tick to avoid blocking.
|
|
650
|
+
*/
|
|
651
|
+
async judgeReadyPillars() {
|
|
652
|
+
const cid = this._companyId;
|
|
653
|
+
try {
|
|
654
|
+
const result = await this._mg(
|
|
655
|
+
`MATCH (gp:GoalPillar {companyId: $cid})
|
|
656
|
+
WHERE gp.l2ReviewStatus = 'pending_review'
|
|
657
|
+
RETURN gp.id AS id
|
|
658
|
+
LIMIT 5`,
|
|
659
|
+
{ cid }
|
|
660
|
+
).catch(() => null);
|
|
661
|
+
for (const row of (result?.rows ?? [])) {
|
|
662
|
+
const pillarId = row[0] ?? row['id'];
|
|
663
|
+
if (!pillarId) continue;
|
|
664
|
+
await this.judgeL2(pillarId).catch(e =>
|
|
665
|
+
this._log(`[CascadeJudge] judgeReadyPillars: judgeL2 failed for ${pillarId}: ${e && e.message || e}`)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
} catch (err) {
|
|
669
|
+
this._log(`[CascadeJudge] judgeReadyPillars error: ${err && err.message || err}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* T3-04: Bulk scan — judge all ActionCell nodes with l3ReviewStatus='pending_review'.
|
|
675
|
+
* Called every 5 ticks from helios-company-daemon.js tick loop.
|
|
676
|
+
* Processes up to 5 cells per tick to avoid blocking.
|
|
677
|
+
*/
|
|
678
|
+
async judgeReadyActionCells() {
|
|
679
|
+
const cid = this._companyId;
|
|
680
|
+
try {
|
|
681
|
+
const result = await this._mg(
|
|
682
|
+
`MATCH (ac:ActionCell {companyId: $cid})
|
|
683
|
+
WHERE ac.l3ReviewStatus = 'pending_review'
|
|
684
|
+
RETURN ac.id AS id
|
|
685
|
+
LIMIT 5`,
|
|
686
|
+
{ cid }
|
|
687
|
+
).catch(() => null);
|
|
688
|
+
for (const row of (result?.rows ?? [])) {
|
|
689
|
+
const cellId = row[0] ?? row['id'];
|
|
690
|
+
if (!cellId) continue;
|
|
691
|
+
await this.judgeL3(cellId).catch(e =>
|
|
692
|
+
this._log(`[CascadeJudge] judgeReadyActionCells: judgeL3 failed for ${cellId}: ${e && e.message || e}`)
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
} catch (err) {
|
|
696
|
+
this._log(`[CascadeJudge] judgeReadyActionCells error: ${err && err.message || err}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
616
699
|
}
|
|
617
700
|
|
|
618
701
|
module.exports = { CascadeJudge };
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* cascade-research-dispatcher.js — Dispatches L2 and L3 research tasks
|
|
4
|
+
* for the Hoshin Kanri cascade with full parent context in the task body.
|
|
5
|
+
*
|
|
6
|
+
* L2: Dept head agent receives company goal + GoalResearchBrief + pillar domain
|
|
7
|
+
* L3: Sub-agent receives pillar L2 content + action cell description
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class CascadeResearchDispatcher {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Function} mgQuery - (cypher, params) => Promise<result>
|
|
13
|
+
* @param {string} companyId - Company identifier
|
|
14
|
+
* @param {Function} [log] - Optional logger function
|
|
15
|
+
*/
|
|
16
|
+
constructor(mgQuery, companyId, log) {
|
|
17
|
+
this._mg = mgQuery;
|
|
18
|
+
this._companyId = companyId;
|
|
19
|
+
this._log = log || (() => {});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── L2 Research Dispatch ──────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Dispatch a harada_l2_research task to the dept head agent for a pillar.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} pillarId - GoalPillar node id
|
|
28
|
+
* @param {string} goalId - CompanyGoal node id
|
|
29
|
+
* @param {string} companyId - Company identifier (overrides constructor value if provided)
|
|
30
|
+
* @param {string} agentId - Assignee agent id
|
|
31
|
+
* @param {string} [l2ReviewCritique] - Previous CEO critique (for revision cycles)
|
|
32
|
+
* @returns {{ taskId: string, agentId: string }}
|
|
33
|
+
*/
|
|
34
|
+
async dispatchL2Research(pillarId, goalId, companyId, agentId, l2ReviewCritique) {
|
|
35
|
+
const cid = companyId || this._companyId;
|
|
36
|
+
|
|
37
|
+
// 1. Fetch GoalPillar name, GoalResearchBrief, and CompanyGoal
|
|
38
|
+
const contextRows = await this._mg(
|
|
39
|
+
`MATCH (p:GoalPillar {id: $pillarId})
|
|
40
|
+
OPTIONAL MATCH (g:CompanyGoal {id: $goalId})
|
|
41
|
+
OPTIONAL MATCH (grb:GoalResearchBrief {companyId: $cid})
|
|
42
|
+
WHERE grb.goalId = $goalId OR grb.id STARTS WITH ('grb:' + $goalId)
|
|
43
|
+
RETURN
|
|
44
|
+
p.name AS pillarName,
|
|
45
|
+
g.title AS goalTitle,
|
|
46
|
+
g.description AS goalDescription,
|
|
47
|
+
grb.tournamentWinner AS tournamentWinner,
|
|
48
|
+
grb.marketContext AS marketContext,
|
|
49
|
+
grb.benchmarks AS benchmarks,
|
|
50
|
+
grb.crmContext AS crmContext
|
|
51
|
+
LIMIT 1`,
|
|
52
|
+
{ pillarId, goalId, cid }
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const row = contextRows && contextRows.rows && contextRows.rows[0]
|
|
56
|
+
? (Array.isArray(contextRows.rows[0])
|
|
57
|
+
? {
|
|
58
|
+
pillarName: contextRows.rows[0][0],
|
|
59
|
+
goalTitle: contextRows.rows[0][1],
|
|
60
|
+
goalDescription: contextRows.rows[0][2],
|
|
61
|
+
tournamentWinner: contextRows.rows[0][3],
|
|
62
|
+
marketContext: contextRows.rows[0][4],
|
|
63
|
+
benchmarks: contextRows.rows[0][5],
|
|
64
|
+
crmContext: contextRows.rows[0][6],
|
|
65
|
+
}
|
|
66
|
+
: contextRows.rows[0])
|
|
67
|
+
: {};
|
|
68
|
+
|
|
69
|
+
const pillarName = row.pillarName || pillarId;
|
|
70
|
+
const goalTitle = row.goalTitle || 'Company Goal';
|
|
71
|
+
const goalDescription = row.goalDescription || '';
|
|
72
|
+
const tournamentWinner = row.tournamentWinner || 'TBD';
|
|
73
|
+
const marketContext = row.marketContext || 'Not available';
|
|
74
|
+
const benchmarks = row.benchmarks || 'Not available';
|
|
75
|
+
const crmContext = row.crmContext || 'Not available';
|
|
76
|
+
|
|
77
|
+
// 2. Build task body
|
|
78
|
+
const previousCritiqueSection = l2ReviewCritique
|
|
79
|
+
? `\nPrevious CEO critique: ${l2ReviewCritique}`
|
|
80
|
+
: '';
|
|
81
|
+
|
|
82
|
+
const taskBody = `Goal: ${goalTitle}
|
|
83
|
+
Description: ${goalDescription}
|
|
84
|
+
Research Brief Summary: ${marketContext} | Tournament winner: ${tournamentWinner}
|
|
85
|
+
Benchmarks: ${benchmarks}
|
|
86
|
+
CRM context: ${crmContext}
|
|
87
|
+
Your pillar: ${pillarName} (id: ${pillarId})
|
|
88
|
+
Your role: ${pillarName} execution lead
|
|
89
|
+
|
|
90
|
+
Research your pillar domain using web search and your available tools.
|
|
91
|
+
Produce:
|
|
92
|
+
1. A 50-word purpose statement for this pillar
|
|
93
|
+
2. A strategy label (≤20 chars) matching or improving on the tournament winner
|
|
94
|
+
3. 3 key actions this pillar should execute
|
|
95
|
+
|
|
96
|
+
When complete, update pillar content by calling:
|
|
97
|
+
POST /api/hbo/pillar/${pillarId}/l2content
|
|
98
|
+
Body: { content: string, strategy: string, keyActions: string[] }
|
|
99
|
+
${previousCritiqueSection}`;
|
|
100
|
+
|
|
101
|
+
const taskId = `harada-l2:${pillarId}`;
|
|
102
|
+
|
|
103
|
+
// 3. MERGE Task
|
|
104
|
+
await this._mg(
|
|
105
|
+
`MERGE (t:Task {id: $taskId, companyId: $cid})
|
|
106
|
+
ON CREATE SET
|
|
107
|
+
t.originKind = $originKind,
|
|
108
|
+
t.assigneeAgentId = $agentId,
|
|
109
|
+
t.status = 'todo',
|
|
110
|
+
t.priority = 1,
|
|
111
|
+
t.body = $body,
|
|
112
|
+
t.createdAt = datetime()
|
|
113
|
+
ON MATCH SET
|
|
114
|
+
t.body = $body,
|
|
115
|
+
t.status = 'todo',
|
|
116
|
+
t.updatedAt = datetime()`,
|
|
117
|
+
{
|
|
118
|
+
taskId,
|
|
119
|
+
cid,
|
|
120
|
+
originKind: 'harada_l2_research',
|
|
121
|
+
agentId,
|
|
122
|
+
body: taskBody,
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// 4. Create AgentReadySignal
|
|
127
|
+
const arsId = `ars:harada-l2:${pillarId}`;
|
|
128
|
+
await this._mg(
|
|
129
|
+
`MERGE (s:AgentReadySignal {id: $arsId, companyId: $cid})
|
|
130
|
+
ON CREATE SET
|
|
131
|
+
s.agentId = $agentId,
|
|
132
|
+
s.status = 'pending',
|
|
133
|
+
s.createdAt = datetime()
|
|
134
|
+
ON MATCH SET
|
|
135
|
+
s.status = 'pending',
|
|
136
|
+
s.updatedAt = datetime()`,
|
|
137
|
+
{ arsId, cid, agentId }
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
this._log(`[CascadeResearchDispatcher] Dispatched L2 research task ${taskId} to agent ${agentId}`);
|
|
141
|
+
|
|
142
|
+
// 5. Return
|
|
143
|
+
return { taskId, agentId };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── L3 Research Dispatch ──────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Dispatch a harada_l3_research task to a sub-agent for an action cell.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} cellId - ActionCell node id
|
|
152
|
+
* @param {string} pillarId - Parent GoalPillar node id
|
|
153
|
+
* @param {string} companyId - Company identifier (overrides constructor value if provided)
|
|
154
|
+
* @param {string} agentId - Assignee agent id
|
|
155
|
+
* @param {string} [l3ReviewCritique] - Previous dept head critique (for revision cycles)
|
|
156
|
+
* @returns {{ taskId: string, agentId: string }}
|
|
157
|
+
*/
|
|
158
|
+
async dispatchL3Research(cellId, pillarId, companyId, agentId, l3ReviewCritique) {
|
|
159
|
+
const cid = companyId || this._companyId;
|
|
160
|
+
|
|
161
|
+
// 1. Fetch ActionCell description, GoalPillar l2Content, l2Strategy, pillar name
|
|
162
|
+
// and goal info via the pillar
|
|
163
|
+
const contextRows = await this._mg(
|
|
164
|
+
`MATCH (p:GoalPillar {id: $pillarId})
|
|
165
|
+
OPTIONAL MATCH (c:ActionCell {id: $cellId})
|
|
166
|
+
OPTIONAL MATCH (g:CompanyGoal)-[:HAS_PILLAR]->(p)
|
|
167
|
+
RETURN
|
|
168
|
+
c.description AS cellDescription,
|
|
169
|
+
p.name AS pillarName,
|
|
170
|
+
p.l2Content AS l2Content,
|
|
171
|
+
p.l2Strategy AS l2Strategy,
|
|
172
|
+
g.title AS goalTitle
|
|
173
|
+
LIMIT 1`,
|
|
174
|
+
{ cellId, pillarId }
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const row = contextRows && contextRows.rows && contextRows.rows[0]
|
|
178
|
+
? (Array.isArray(contextRows.rows[0])
|
|
179
|
+
? {
|
|
180
|
+
cellDescription: contextRows.rows[0][0],
|
|
181
|
+
pillarName: contextRows.rows[0][1],
|
|
182
|
+
l2Content: contextRows.rows[0][2],
|
|
183
|
+
l2Strategy: contextRows.rows[0][3],
|
|
184
|
+
goalTitle: contextRows.rows[0][4],
|
|
185
|
+
}
|
|
186
|
+
: contextRows.rows[0])
|
|
187
|
+
: {};
|
|
188
|
+
|
|
189
|
+
const cellDescription = row.cellDescription || cellId;
|
|
190
|
+
const pillarName = row.pillarName || pillarId;
|
|
191
|
+
const l2Content = row.l2Content || '';
|
|
192
|
+
const l2Strategy = row.l2Strategy || '';
|
|
193
|
+
const goalTitle = row.goalTitle || 'Company Goal';
|
|
194
|
+
|
|
195
|
+
// 2. Build task body
|
|
196
|
+
const previousCritiqueSection = l3ReviewCritique
|
|
197
|
+
? `\nPrevious dept head critique: ${l3ReviewCritique}`
|
|
198
|
+
: '';
|
|
199
|
+
|
|
200
|
+
const taskBody = `Goal: ${goalTitle}
|
|
201
|
+
Pillar: ${pillarName}
|
|
202
|
+
Pillar strategy: ${l2Strategy}
|
|
203
|
+
Pillar purpose: ${l2Content}
|
|
204
|
+
|
|
205
|
+
Your action cell: ${cellDescription} (id: ${cellId})
|
|
206
|
+
|
|
207
|
+
Research this specific action cell using available tools.
|
|
208
|
+
Produce a specific execution plan (≤100 words) for this action.
|
|
209
|
+
|
|
210
|
+
When complete, update by calling:
|
|
211
|
+
POST /api/hbo/actioncell/${cellId}/l3content
|
|
212
|
+
Body: { content: string }
|
|
213
|
+
${previousCritiqueSection}`;
|
|
214
|
+
|
|
215
|
+
const taskId = `harada-l3:${cellId}`;
|
|
216
|
+
|
|
217
|
+
// 3. MERGE Task
|
|
218
|
+
await this._mg(
|
|
219
|
+
`MERGE (t:Task {id: $taskId, companyId: $cid})
|
|
220
|
+
ON CREATE SET
|
|
221
|
+
t.originKind = $originKind,
|
|
222
|
+
t.assigneeAgentId = $agentId,
|
|
223
|
+
t.status = 'todo',
|
|
224
|
+
t.priority = 1,
|
|
225
|
+
t.body = $body,
|
|
226
|
+
t.createdAt = datetime()
|
|
227
|
+
ON MATCH SET
|
|
228
|
+
t.body = $body,
|
|
229
|
+
t.status = 'todo',
|
|
230
|
+
t.updatedAt = datetime()`,
|
|
231
|
+
{
|
|
232
|
+
taskId,
|
|
233
|
+
cid,
|
|
234
|
+
originKind: 'harada_l3_research',
|
|
235
|
+
agentId,
|
|
236
|
+
body: taskBody,
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// 4. Create AgentReadySignal
|
|
241
|
+
const arsId = `ars:harada-l3:${cellId}`;
|
|
242
|
+
await this._mg(
|
|
243
|
+
`MERGE (s:AgentReadySignal {id: $arsId, companyId: $cid})
|
|
244
|
+
ON CREATE SET
|
|
245
|
+
s.agentId = $agentId,
|
|
246
|
+
s.status = 'pending',
|
|
247
|
+
s.createdAt = datetime()
|
|
248
|
+
ON MATCH SET
|
|
249
|
+
s.status = 'pending',
|
|
250
|
+
s.updatedAt = datetime()`,
|
|
251
|
+
{ arsId, cid, agentId }
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
this._log(`[CascadeResearchDispatcher] Dispatched L3 research task ${taskId} to agent ${agentId}`);
|
|
255
|
+
|
|
256
|
+
// P_EXPLAIN-01: Post a Three Cs interpretation block as a Comment on the task
|
|
257
|
+
// so the user immediately sees what Helios understood this action cell to mean.
|
|
258
|
+
// Fire-and-forget (non-blocking, errors are silently swallowed).
|
|
259
|
+
setImmediate(async () => {
|
|
260
|
+
try {
|
|
261
|
+
const commentId = `comment:interp:${taskId}`;
|
|
262
|
+
const interpretationBody = '```interpretation\n' + JSON.stringify({
|
|
263
|
+
stated: cellDescription,
|
|
264
|
+
inferred: `Execute action cell "${cellDescription}" as part of the "${pillarName}" pillar strategy: ${l2Strategy || '(pending L2 review)'}`,
|
|
265
|
+
defaulted: 'Harada L3 research task — produce ≤100 word execution plan',
|
|
266
|
+
confidence: 0.75,
|
|
267
|
+
}) + '\n```';
|
|
268
|
+
await this._mg(
|
|
269
|
+
`MERGE (c:Comment {id: $commentId, companyId: $cid})
|
|
270
|
+
ON CREATE SET c.taskId=$taskId, c.body=$body, c.authorType='system',
|
|
271
|
+
c.createdAt=datetime(), c.kind='interpretation'`,
|
|
272
|
+
{ commentId, cid, taskId, body: interpretationBody }
|
|
273
|
+
);
|
|
274
|
+
} catch (_) {}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// 5. Return
|
|
278
|
+
return { taskId, agentId };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = { CascadeResearchDispatcher };
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
// Load the async tournament runner — the sync runTournament export was deprecated
|
|
21
21
|
// and is now a no-op stub. All callers must use runTournamentAsync which delegates
|
|
22
22
|
// to tournament-node-runner.js (cross-platform: Windows, macOS, Linux).
|
|
23
|
+
// Path: from daemon/lib/harada/ → ../../../ reaches project root → lib/triage-core/
|
|
23
24
|
let runTournamentAsync = null;
|
|
24
25
|
try {
|
|
25
|
-
const tr = require('
|
|
26
|
+
const tr = require('../../../lib/triage-core/tournament-runner.js');
|
|
26
27
|
runTournamentAsync = tr.runTournamentAsync || null;
|
|
27
28
|
if (!runTournamentAsync) {
|
|
28
29
|
process.stderr.write('[pillar-dispatcher] WARN: tournament-runner.js loaded but runTournamentAsync not exported — pillar strategy selection will use unranked first candidate\n');
|
|
@@ -381,6 +382,26 @@ class PillarDispatcher {
|
|
|
381
382
|
// Wire first open ActionCell to this task
|
|
382
383
|
await this._assignFirstOpenCell(pillar.id, agentId);
|
|
383
384
|
|
|
385
|
+
// P_EXPLAIN-02: Post a Three Cs interpretation block as a Comment on the L2 pillar task
|
|
386
|
+
// so the user immediately sees what Helios understood about this strategic dispatch.
|
|
387
|
+
setImmediate(async () => {
|
|
388
|
+
try {
|
|
389
|
+
const commentId = `comment:interp:${taskId}`;
|
|
390
|
+
const interpretationBody = '```interpretation\n' + JSON.stringify({
|
|
391
|
+
stated: `Execute pillar: ${pillar.name}`,
|
|
392
|
+
inferred: `Implement the "${strategy.label}" strategy for pillar "${pillar.name}" by working with your assigned action cells and reporting progress via POST /api/hbo/pillar/${pillar.id}/l2content`,
|
|
393
|
+
defaulted: 'Harada L2 pillar dispatch — strategic execution, workType=strategic',
|
|
394
|
+
confidence: 0.85,
|
|
395
|
+
}) + '\n```';
|
|
396
|
+
await this._mg(
|
|
397
|
+
`MERGE (c:Comment {id: $commentId, companyId: $cid})
|
|
398
|
+
ON CREATE SET c.taskId=$taskId, c.body=$body, c.authorType='system',
|
|
399
|
+
c.createdAt=datetime(), c.kind='interpretation'`,
|
|
400
|
+
{ commentId, cid: companyId, taskId, body: interpretationBody }
|
|
401
|
+
);
|
|
402
|
+
} catch (_) {}
|
|
403
|
+
});
|
|
404
|
+
|
|
384
405
|
this._log('info', `PillarDispatcher: dispatched "${pillar.name}" → ${agentId} [${strategy.label}]`);
|
|
385
406
|
}
|
|
386
407
|
|
|
@@ -455,7 +476,7 @@ class PillarDispatcher {
|
|
|
455
476
|
}
|
|
456
477
|
} else {
|
|
457
478
|
// Tournament runner was not loaded — this means pillar strategy selection is
|
|
458
|
-
// operating blind. Log at 'warn'
|
|
479
|
+
// operating blind. Log at 'warn' so operators see this in production.
|
|
459
480
|
this._log('warn', `PillarDispatcher: tournament runner unavailable for "${pillar.name}" — strategy selection will use first candidate (unranked). Check tournament-runner.js installation.`);
|
|
460
481
|
}
|
|
461
482
|
|
package/daemon/lib/hbo-bridge.js
CHANGED
|
@@ -74,6 +74,48 @@ function loadService(varRef, relPath) {
|
|
|
74
74
|
|
|
75
75
|
const MAX_TOURNAMENT_QUEUE = 50; // L1: module-level constant
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* _rankingsToPillars(rankingsJson) — converts GoalResearchBrief tournament rankings
|
|
79
|
+
* into { name, rationale, weight }[] for MandalaManager.initializeMandala().
|
|
80
|
+
*
|
|
81
|
+
* Weight allocation: proportional to TrueSkill conservativeScore when available,
|
|
82
|
+
* otherwise equal-weight with remainder added to rank-1 entry.
|
|
83
|
+
* Enforces sum=1.0 ± 0.001 tolerance.
|
|
84
|
+
*/
|
|
85
|
+
function _rankingsToPillars(rankingsJson) {
|
|
86
|
+
if (!rankingsJson) return null;
|
|
87
|
+
let rankings;
|
|
88
|
+
try { rankings = typeof rankingsJson === 'string' ? JSON.parse(rankingsJson) : rankingsJson; }
|
|
89
|
+
catch { return null; }
|
|
90
|
+
if (!Array.isArray(rankings) || rankings.length === 0) return null;
|
|
91
|
+
|
|
92
|
+
const n = rankings.length;
|
|
93
|
+
let weights;
|
|
94
|
+
if (rankings[0] && typeof rankings[0].conservativeScore === 'number') {
|
|
95
|
+
// TrueSkill-proportional weights
|
|
96
|
+
const total = rankings.reduce((s, r) => s + Math.max(0, r.conservativeScore), 0);
|
|
97
|
+
if (total > 0) {
|
|
98
|
+
weights = rankings.map(r => Math.round(Math.max(0, r.conservativeScore) / total * 10000) / 10000);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!weights) {
|
|
102
|
+
// Equal weights
|
|
103
|
+
const base = Math.round(10000 / n) / 10000;
|
|
104
|
+
weights = Array(n).fill(base);
|
|
105
|
+
}
|
|
106
|
+
// Normalize floating-point drift
|
|
107
|
+
const sum = weights.reduce((s, w) => s + w, 0);
|
|
108
|
+
if (Math.abs(sum - 1.0) > 0.001) {
|
|
109
|
+
weights[0] += (1.0 - sum);
|
|
110
|
+
weights[0] = Math.round(weights[0] * 10000) / 10000;
|
|
111
|
+
}
|
|
112
|
+
return rankings.map((r, i) => ({
|
|
113
|
+
name: r.pillarName || r.name || `Pillar ${i + 1}`,
|
|
114
|
+
rationale: r.rationale || r.description || '',
|
|
115
|
+
weight: weights[i],
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
77
119
|
// ── HBO Bridge ─────────────────────────────────────────────────────────────────
|
|
78
120
|
|
|
79
121
|
class HBOBridge {
|
|
@@ -685,13 +727,11 @@ class HBOBridge {
|
|
|
685
727
|
}
|
|
686
728
|
|
|
687
729
|
this._log('debug', `tickBudgetSync: synced ${spendRows.rows.length} agent(s) spend data`);
|
|
688
|
-
|
|
730
|
+
} catch (e) {
|
|
689
731
|
this._log('warn', `tickBudgetSync error (non-fatal): ${e.message}`);
|
|
690
732
|
}
|
|
691
733
|
}
|
|
692
734
|
|
|
693
|
-
// ── tickGoalDecompose ──────────────────────────────────────────────────────
|
|
694
|
-
|
|
695
735
|
/**
|
|
696
736
|
* Runs every 5 ticks (same cadence as tickGoalSync).
|
|
697
737
|
* Detects CompanyGoal nodes that have status='active' but no child
|
|
@@ -780,7 +820,21 @@ class HBOBridge {
|
|
|
780
820
|
try {
|
|
781
821
|
const { MandalaManager } = require('./harada/mandala');
|
|
782
822
|
const mandala = new MandalaManager(this._mg.bind(this), this._companyId);
|
|
783
|
-
|
|
823
|
+
// GoalResearchBrief: fetch tournament rankings for pillar name seeding
|
|
824
|
+
let _pillarNames = null;
|
|
825
|
+
try {
|
|
826
|
+
const _briefRows = await this._mgQuery(
|
|
827
|
+
`MATCH (g:CompanyGoal {id: $gid})-[:HAS_RESEARCH]->(grb:GoalResearchBrief)
|
|
828
|
+
WHERE grb.expiresAt > localdatetime()
|
|
829
|
+
RETURN grb.tournamentRankings AS rankings
|
|
830
|
+
ORDER BY grb.createdAt DESC LIMIT 1`,
|
|
831
|
+
{ gid: goalId }
|
|
832
|
+
);
|
|
833
|
+
if (_briefRows && _briefRows.length > 0 && _briefRows[0].rankings) {
|
|
834
|
+
_pillarNames = _rankingsToPillars(_briefRows[0].rankings);
|
|
835
|
+
}
|
|
836
|
+
} catch { /* non-fatal — initializeMandala works without pillar names */ }
|
|
837
|
+
await mandala.initializeMandala(goalId, goalTitle, _pillarNames);
|
|
784
838
|
this._log('info', `tickGoalDecompose: Mandala initialized for goal "${goalTitle}" (8 pillars, 64 cells)`);
|
|
785
839
|
} catch (e) {
|
|
786
840
|
this._log('debug', `tickGoalDecompose: Mandala init failed for goal ${goalId}: ${e.message}`);
|
|
@@ -952,7 +1006,21 @@ class HBOBridge {
|
|
|
952
1006
|
{ goalId: goal.id, cid: this._companyId }
|
|
953
1007
|
).catch(() => null);
|
|
954
1008
|
if (!existingPillar?.rows?.length) {
|
|
955
|
-
|
|
1009
|
+
// GoalResearchBrief: fetch tournament rankings for pillar name seeding
|
|
1010
|
+
let _pillarNames = null;
|
|
1011
|
+
try {
|
|
1012
|
+
const _briefRows = await this._mg(
|
|
1013
|
+
`MATCH (g:CompanyGoal {id: $gid})-[:HAS_RESEARCH]->(grb:GoalResearchBrief)
|
|
1014
|
+
WHERE grb.expiresAt > localdatetime()
|
|
1015
|
+
RETURN grb.tournamentRankings AS rankings
|
|
1016
|
+
ORDER BY grb.createdAt DESC LIMIT 1`,
|
|
1017
|
+
{ gid: goal.id }
|
|
1018
|
+
);
|
|
1019
|
+
if (_briefRows && _briefRows.length > 0 && _briefRows[0].rankings) {
|
|
1020
|
+
_pillarNames = _rankingsToPillars(_briefRows[0].rankings);
|
|
1021
|
+
}
|
|
1022
|
+
} catch { /* non-fatal — initializeMandala works without pillar names */ }
|
|
1023
|
+
await mandala.initializeMandala(goal.id, goal.title, _pillarNames);
|
|
956
1024
|
this._log('info', `tickGoalDecompose: Mandala initialized for goal "${goal.title}" (8 pillars, 64 cells)`);
|
|
957
1025
|
}
|
|
958
1026
|
} catch (e) {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* daemon/lib/headroom-middleware.js
|
|
4
|
+
*
|
|
5
|
+
* CCR (Compress-Cache-Retrieve) middleware helpers for helios-api.js.
|
|
6
|
+
*
|
|
7
|
+
* Exports:
|
|
8
|
+
* handleCcrRetrieve(req, res) — GET /api/hbo/retrieve/:hash handler
|
|
9
|
+
* isLargeArrayPayload(parsed) — true when an HBO API response body warrants compression
|
|
10
|
+
*
|
|
11
|
+
* Architecture note on CcrStore:
|
|
12
|
+
* The compression server runs as a CHILD PROCESS (spawned by HeadroomProxyManager).
|
|
13
|
+
* It has its own in-process CcrStore. The daemon process has a separate CcrStore
|
|
14
|
+
* instance which is always empty — it never receives the dropped rows.
|
|
15
|
+
*
|
|
16
|
+
* Therefore handleCcrRetrieve MUST proxy to the child process's HTTP endpoint
|
|
17
|
+
* (GET /v1/retrieve/:hash) rather than reading the daemon's local CcrStore.
|
|
18
|
+
* This is consistent with how helios-api.js proxies /api/headroom/health and
|
|
19
|
+
* /api/headroom/stats.
|
|
20
|
+
*
|
|
21
|
+
* No hardcoded company IDs. Cross-platform (http module, path.join).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const http = require('http');
|
|
25
|
+
|
|
26
|
+
// ── handleCcrRetrieve ─────────────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Handle GET /api/hbo/retrieve/:hash
|
|
29
|
+
*
|
|
30
|
+
* Proxies to the compression child's GET /v1/retrieve/:hash endpoint.
|
|
31
|
+
* req.params.hash is set by the caller (helios-api.js) before invoking.
|
|
32
|
+
*
|
|
33
|
+
* Responses:
|
|
34
|
+
* 200 { hash: string, data: <original JSON>, originalBytes: number }
|
|
35
|
+
* 400 { error: string } — missing or invalid hash param
|
|
36
|
+
* 404 { error: string } — hash not in child store or expired
|
|
37
|
+
* 503 { error: string } — compression server not running
|
|
38
|
+
*/
|
|
39
|
+
async function handleCcrRetrieve(req, res) {
|
|
40
|
+
const hash = (req.params && req.params.hash) ? String(req.params.hash) : '';
|
|
41
|
+
|
|
42
|
+
if (!hash || !/^[a-f0-9]{8,64}$/.test(hash)) {
|
|
43
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
44
|
+
res.end(JSON.stringify({ error: 'Invalid or missing hash parameter' }));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get the compression server URL from HeadroomProxyManager singleton.
|
|
49
|
+
// The child process owns the CcrStore — we must proxy to it.
|
|
50
|
+
let baseUrl;
|
|
51
|
+
try {
|
|
52
|
+
const { HeadroomProxyManager } = require('./headroom-proxy-manager');
|
|
53
|
+
baseUrl = HeadroomProxyManager.getInstance().getBaseUrl();
|
|
54
|
+
} catch (_) {}
|
|
55
|
+
|
|
56
|
+
if (!baseUrl) {
|
|
57
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
58
|
+
res.end(JSON.stringify({ error: 'Compression server not running — CCR retrieve unavailable' }));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Proxy GET /v1/retrieve/:hash to the child process
|
|
63
|
+
const url = new URL(baseUrl);
|
|
64
|
+
const proxyReq = http.request(
|
|
65
|
+
{
|
|
66
|
+
hostname: url.hostname,
|
|
67
|
+
port: parseInt(url.port || '8787', 10),
|
|
68
|
+
path: `/v1/retrieve/${hash}`,
|
|
69
|
+
method: 'GET',
|
|
70
|
+
},
|
|
71
|
+
(proxyRes) => {
|
|
72
|
+
let body = '';
|
|
73
|
+
proxyRes.on('data', (c) => { body += c; });
|
|
74
|
+
proxyRes.on('end', () => {
|
|
75
|
+
res.writeHead(proxyRes.statusCode, { 'Content-Type': 'application/json' });
|
|
76
|
+
res.end(body);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
proxyReq.setTimeout(3000, () => {
|
|
82
|
+
proxyReq.destroy();
|
|
83
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
84
|
+
res.end(JSON.stringify({ error: 'CCR retrieve timeout' }));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
proxyReq.on('error', () => {
|
|
88
|
+
if (!res.headersSent) {
|
|
89
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
90
|
+
res.end(JSON.stringify({ error: 'CCR retrieve: compression server unreachable' }));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
proxyReq.end();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── isLargeArrayPayload ───────────────────────────────────────────────────────
|
|
98
|
+
/**
|
|
99
|
+
* Returns true if a parsed JSON body should be routed through Headroom compression.
|
|
100
|
+
*
|
|
101
|
+
* Conditions (any one sufficient):
|
|
102
|
+
* 1. parsed is an array with >= 5 elements
|
|
103
|
+
* 2. parsed is an object with at least one value that is an array with >= 5 elements
|
|
104
|
+
*
|
|
105
|
+
* Threshold 5 matches SmartCrusher.cfg.minItemsToAnalyze (default 5).
|
|
106
|
+
* Below this threshold SmartCrusher always returns passthrough anyway.
|
|
107
|
+
*
|
|
108
|
+
* Called from helios-api.js res.end() wrapper on GET /api/hbo/* routes.
|
|
109
|
+
* No side effects. Pure function.
|
|
110
|
+
*/
|
|
111
|
+
function isLargeArrayPayload(parsed) {
|
|
112
|
+
if (parsed === null || parsed === undefined) return false;
|
|
113
|
+
|
|
114
|
+
// Case 1: top-level array
|
|
115
|
+
if (Array.isArray(parsed)) {
|
|
116
|
+
return parsed.length >= 5;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Case 2: object containing an array value
|
|
120
|
+
if (typeof parsed === 'object') {
|
|
121
|
+
const values = Object.values(parsed);
|
|
122
|
+
return values.some((v) => Array.isArray(v) && v.length >= 5);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { handleCcrRetrieve, isLargeArrayPayload };
|
|
129
|
+
|