@cgh567/agent 2.4.3 → 2.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/business/talisman-ceo.md +183 -0
- package/agents/business/talisman-comms.md +257 -0
- package/agents/business/talisman-cto.md +153 -0
- package/agents/business/talisman-finance.md +246 -0
- package/agents/business/talisman-marketing.md +240 -0
- package/agents/business/talisman-sales.md +242 -0
- package/agents/business/talisman-support.md +236 -0
- package/bin/helios-rpc-wrapper.sh +4 -1
- package/bin/helios-rpc.js +19 -0
- package/daemon/adapters/helios-rpc-adapter.js +5 -12
- package/daemon/context-enrichment.js +27 -0
- package/daemon/helios-api.js +310 -58
- package/daemon/helios-company-daemon.js +179 -53
- package/daemon/lib/blast-radius-analyzer.js +75 -0
- package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
- package/daemon/lib/forensic-log.js +113 -0
- package/daemon/lib/goal-research-pipeline.js +644 -0
- package/daemon/lib/harada/cascade-judge.js +84 -1
- package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
- package/daemon/lib/harada/pillar-dispatcher.js +23 -2
- package/daemon/lib/hbo-bridge.js +73 -5
- package/daemon/lib/headroom-middleware.js +129 -0
- package/daemon/lib/headroom-proxy-manager.js +319 -0
- package/daemon/lib/intelligence/department-page-generator.js +46 -1
- package/daemon/lib/interpretation-engine.js +92 -0
- package/daemon/lib/mental-model-cache.js +96 -0
- package/daemon/lib/project-factory.js +47 -0
- package/daemon/lib/session-log-reader.js +93 -0
- package/daemon/lib/standard-work-bootstrap.js +87 -1
- package/daemon/lib/task-completion-processor.js +12 -0
- package/daemon/package.json +2 -1
- package/daemon/routes/agents.js +51 -6
- package/daemon/routes/channels.js +116 -2
- package/daemon/routes/crm.js +85 -0
- package/daemon/routes/dashboard.js +62 -16
- package/daemon/routes/dept.js +10 -1
- package/daemon/routes/email-triage.js +19 -10
- package/daemon/routes/hbo.js +367 -13
- package/daemon/routes/hed.js +133 -0
- package/daemon/routes/inbox.js +466 -10
- package/daemon/routes/project.js +392 -9
- package/daemon/schema-definitions.js +10 -0
- package/daemon/schema-migrations-hbo.js +10 -0
- package/daemon/schema-migrations-proj.js +22 -0
- package/extensions/__tests__/codebase-index.test.ts +73 -0
- package/extensions/__tests__/extension-command-registration.test.ts +35 -0
- package/extensions/__tests__/git-push-guard.test.ts +68 -0
- package/extensions/context-compaction.ts +104 -76
- package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
- package/extensions/email/actions/draft-response.ts +21 -1
- package/extensions/email/auth/accounts.ts +5 -11
- package/extensions/email/auth/inbox-dog.ts +5 -2
- package/extensions/email/backfill.ts +20 -13
- package/extensions/email/providers/gmail.ts +164 -0
- package/extensions/email/providers/google-calendar.ts +34 -5
- package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
- package/extensions/helios-browser/backends/playwright.ts +3 -1
- package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
- package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
- package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
- package/extensions/hema-dispatch-v3/index.ts +33 -65
- package/extensions/interview/__tests__/server.test.ts +117 -0
- package/extensions/lib/helios-root.cjs +46 -0
- package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
- package/extensions/warm-tick/warm-tick-maintenance.ts +156 -0
- package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
- package/lib/__tests__/crash-fixes.test.ts +49 -0
- package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
- package/lib/broker/__tests__/jit-subscription.test.js +44 -1
- package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
- package/lib/compression/__tests__/ccr-store.test.js +138 -0
- package/lib/compression/__tests__/pipeline.test.js +280 -0
- package/lib/compression/__tests__/smart-crusher.test.js +242 -0
- package/lib/compression/dist/server.js +34 -1
- package/lib/compression/dist/start-server.js +77 -0
- package/lib/graph/learning/headroom-learn-bridge.js +175 -0
- package/lib/hbo-core-store.ts +71 -0
- package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
- package/lib/skill-sync.js +6 -1
- package/lib/startup-integrity.js +9 -2
- package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
- package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
- package/lib/triage-core/__tests__/classifier.test.ts +45 -7
- package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
- package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
- package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
- package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
- package/lib/triage-core/__tests__/signals.test.ts +357 -0
- package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
- package/lib/triage-core/backfill-cost-estimator.ts +91 -0
- package/lib/triage-core/backfill-orchestrator.ts +119 -0
- package/lib/triage-core/classifier.ts +38 -6
- package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
- package/lib/triage-core/cos/response-debt.ts +2 -2
- package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
- package/lib/triage-core/graph/batch-persistence.ts +66 -2
- package/lib/triage-core/graph/betweenness-worker.js +75 -0
- package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
- package/lib/triage-core/graph/persistence.ts +1 -1
- package/lib/triage-core/graph/schema-v2.ts +2 -0
- package/lib/triage-core/graph/schema.cypher +1 -0
- package/lib/triage-core/graph/triage-query.ts +1 -1
- package/lib/triage-core/learning.ts +15 -20
- package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
- package/lib/triage-core/mental-model/cos-integration.ts +1 -1
- package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
- package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
- package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
- package/lib/triage-core/mental-model/model-assembler.ts +16 -3
- package/lib/triage-core/orchestrator.ts +4 -4
- package/lib/triage-core/scheduled-sends.ts +39 -2
- package/lib/triage-core/signals/comms-style.ts +1 -1
- package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
- package/lib/triage-core/signals/favee-type.ts +6 -1
- package/lib/triage-core/signals/goal-relevance.ts +31 -2
- package/lib/triage-core/signals/personal-importance.ts +1 -1
- package/lib/triage-core/signals/referral-chain.ts +0 -1
- package/lib/triage-core/signals/relationship-decay.ts +4 -0
- package/lib/triage-core/signals/relationship-health.ts +6 -1
- package/lib/triage-core/signals/trajectory-signal.ts +38 -3
- package/lib/triage-core/tournament-runner.js +11 -1
- package/lib/triage-core/triage-llm-factory.ts +110 -0
- package/lib/triage-core/triage-local-llm.ts +145 -0
- package/lib/triage-core/triage-sql-store.ts +337 -0
- package/lib/triage-core/types.ts +2 -2
- package/lib/unified-graph.atomic.test.ts +52 -0
- package/lib/unified-graph.failure-categories.test.ts +55 -0
- package/package.json +10 -3
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/skills/helios-bookkeeping/SKILL.md +321 -0
- package/skills/helios-briefer/SKILL.md +44 -0
- package/skills/helios-client-relations/SKILL.md +322 -0
- package/skills/helios-personal-triager/SKILL.md +45 -0
- package/skills/helios-recruitment/SKILL.md +317 -0
- package/skills/helios-relationship-nudger/SKILL.md +77 -0
- package/skills/helios-researcher/SKILL.md +44 -0
- package/skills/helios-scheduler/SKILL.md +58 -0
- package/skills/helios-tax-analyst/SKILL.md +280 -0
- package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -1823
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* daemon/lib/headroom-proxy-manager.js
|
|
4
|
+
*
|
|
5
|
+
* Singleton process manager for the Helios Compression Server
|
|
6
|
+
* (lib/compression/dist/server.js).
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* - Find a free TCP port in range 8787–8796
|
|
10
|
+
* - Spawn server.js as a child process (no shell — cross-platform)
|
|
11
|
+
* - Wait for the {"event":"ready",...} JSON line on child stdout
|
|
12
|
+
* - Expose getBaseUrl() so all call sites can reach the server
|
|
13
|
+
* - Auto-restart on crash with exponential backoff (1s → 2s → ... → 30s)
|
|
14
|
+
* - Log lifecycle events to daemon/headroom-proxy.log (append)
|
|
15
|
+
*
|
|
16
|
+
* Interface (matches all call sites in helios-api.js, helios-company-daemon.js,
|
|
17
|
+
* helios-rpc-adapter.js, planning.ts):
|
|
18
|
+
* HeadroomProxyManager.getInstance(): HeadroomProxyManager
|
|
19
|
+
* instance.start(): Promise<{baseUrl: string}>
|
|
20
|
+
* instance.getBaseUrl(): string | null
|
|
21
|
+
* instance.stop(): Promise<void>
|
|
22
|
+
*
|
|
23
|
+
* Cross-platform: uses process.execPath (Node binary), path.join, net.createServer.
|
|
24
|
+
* No hardcoded company IDs. Port is dynamic — injected via HEADROOM_PROXY_URL env.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const net = require('net');
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const { spawn } = require('child_process');
|
|
31
|
+
|
|
32
|
+
// Path to the compiled compression server entry point.
|
|
33
|
+
// Relative from this file: daemon/lib/ → ../../lib/compression/dist/server.js
|
|
34
|
+
const SERVER_SCRIPT = path.join(__dirname, '..', '..', 'lib', 'compression', 'dist', 'server.js');
|
|
35
|
+
|
|
36
|
+
// Log file: daemon/headroom-proxy.log (same dir as daemon/, sibling of lib/)
|
|
37
|
+
const LOG_FILE = path.join(__dirname, '..', 'headroom-proxy.log');
|
|
38
|
+
|
|
39
|
+
// Port scan range — 50 ports gives headroom when multiple test suites run concurrently
|
|
40
|
+
// (vitest maxWorkers=4, each test file may start its own server instance).
|
|
41
|
+
//
|
|
42
|
+
// PRECONDITION (O2): HEADROOM_PORT_START must be set in process.env BEFORE the first
|
|
43
|
+
// require() of this module. PORT_START is a module-level constant evaluated once at
|
|
44
|
+
// load time. Setting the env var after require() has no effect on the running instance.
|
|
45
|
+
// Tests that need a custom range must: (1) set process.env.HEADROOM_PORT_START,
|
|
46
|
+
// (2) delete require.cache[managerPath], (3) require() again. See test files for pattern.
|
|
47
|
+
//
|
|
48
|
+
// Can be overridden via env for specific deployment constraints:
|
|
49
|
+
// HEADROOM_PORT_START=9000 to use a different range entirely.
|
|
50
|
+
const PORT_START = parseInt(process.env.HEADROOM_PORT_START || '8787', 10);
|
|
51
|
+
const PORT_END = PORT_START + 49; // 50 ports
|
|
52
|
+
|
|
53
|
+
// Backoff config (ms)
|
|
54
|
+
const BACKOFF_INITIAL = 1000;
|
|
55
|
+
const BACKOFF_MAX = 30000;
|
|
56
|
+
const READY_TIMEOUT = 15000; // 15s max wait for ready signal
|
|
57
|
+
|
|
58
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Append a JSON line to the proxy log. Fail-open — never throws.
|
|
62
|
+
*/
|
|
63
|
+
function appendLog(obj) {
|
|
64
|
+
try {
|
|
65
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify({ ...obj, ts: Date.now() }) + '\n');
|
|
66
|
+
} catch (_) { /* non-critical */ }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Find the first free TCP port in [start, end].
|
|
71
|
+
* Returns a Promise<number> or rejects if none are free.
|
|
72
|
+
*/
|
|
73
|
+
function findFreePort(start, end) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
let current = start;
|
|
76
|
+
|
|
77
|
+
function tryPort() {
|
|
78
|
+
if (current > end) {
|
|
79
|
+
reject(new Error(`No free port found in range ${start}–${end}`));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const server = net.createServer();
|
|
83
|
+
server.unref();
|
|
84
|
+
server.listen(current, '127.0.0.1', () => {
|
|
85
|
+
const port = server.address().port;
|
|
86
|
+
server.close(() => resolve(port));
|
|
87
|
+
});
|
|
88
|
+
server.on('error', (err) => {
|
|
89
|
+
if (err.code === 'EADDRINUSE') {
|
|
90
|
+
current++;
|
|
91
|
+
tryPort();
|
|
92
|
+
} else {
|
|
93
|
+
reject(err);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
tryPort();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Singleton ─────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
let _instance = null;
|
|
105
|
+
|
|
106
|
+
class HeadroomProxyManager {
|
|
107
|
+
constructor() {
|
|
108
|
+
this._child = null;
|
|
109
|
+
this._baseUrl = null;
|
|
110
|
+
this._port = null;
|
|
111
|
+
this._stopped = false;
|
|
112
|
+
this._backoffMs = BACKOFF_INITIAL;
|
|
113
|
+
this._starting = false;
|
|
114
|
+
this._startPromise = null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns the module-level singleton. Safe to call from multiple require()s.
|
|
119
|
+
*/
|
|
120
|
+
static getInstance() {
|
|
121
|
+
if (!_instance) _instance = new HeadroomProxyManager();
|
|
122
|
+
return _instance;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Start the compression server. Idempotent — multiple calls return the same promise.
|
|
127
|
+
* Resolves with {baseUrl} once the server emits its ready signal.
|
|
128
|
+
* Rejects if server fails to start within READY_TIMEOUT ms.
|
|
129
|
+
*/
|
|
130
|
+
start() {
|
|
131
|
+
// Already started
|
|
132
|
+
if (this._baseUrl) return Promise.resolve({ baseUrl: this._baseUrl });
|
|
133
|
+
// In-flight start — reuse same promise
|
|
134
|
+
if (this._startPromise) return this._startPromise;
|
|
135
|
+
|
|
136
|
+
this._startPromise = this._doStart().then((result) => {
|
|
137
|
+
this._startPromise = null;
|
|
138
|
+
return result;
|
|
139
|
+
}).catch((err) => {
|
|
140
|
+
this._startPromise = null;
|
|
141
|
+
throw err;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return this._startPromise;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Internal: find a port, spawn the server, wait for ready.
|
|
149
|
+
*/
|
|
150
|
+
async _doStart() {
|
|
151
|
+
this._stopped = false;
|
|
152
|
+
|
|
153
|
+
// Verify server script exists
|
|
154
|
+
if (!fs.existsSync(SERVER_SCRIPT)) {
|
|
155
|
+
throw new Error(`Compression server not found at ${SERVER_SCRIPT}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const port = await findFreePort(PORT_START, PORT_END);
|
|
159
|
+
this._port = port;
|
|
160
|
+
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
reject(new Error(`Compression server did not emit ready within ${READY_TIMEOUT}ms on port ${port}`));
|
|
164
|
+
if (this._child) {
|
|
165
|
+
try { this._child.kill(); } catch (_) {}
|
|
166
|
+
this._child = null;
|
|
167
|
+
}
|
|
168
|
+
}, READY_TIMEOUT);
|
|
169
|
+
|
|
170
|
+
const child = spawn(
|
|
171
|
+
process.execPath,
|
|
172
|
+
[SERVER_SCRIPT],
|
|
173
|
+
{
|
|
174
|
+
stdio : ['ignore', 'pipe', 'pipe'],
|
|
175
|
+
env : { ...process.env, HEADROOM_PORT: String(port) },
|
|
176
|
+
shell : false,
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
this._child = child;
|
|
181
|
+
|
|
182
|
+
let stdoutBuf = '';
|
|
183
|
+
child.stdout.on('data', (chunk) => {
|
|
184
|
+
stdoutBuf += chunk.toString('utf-8');
|
|
185
|
+
const lines = stdoutBuf.split('\n');
|
|
186
|
+
stdoutBuf = lines.pop(); // keep partial line
|
|
187
|
+
|
|
188
|
+
for (const line of lines) {
|
|
189
|
+
if (!line.trim()) continue;
|
|
190
|
+
try {
|
|
191
|
+
const parsed = JSON.parse(line);
|
|
192
|
+
if (parsed.event === 'ready') {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
this._baseUrl = `http://127.0.0.1:${parsed.port || port}`;
|
|
195
|
+
this._backoffMs = BACKOFF_INITIAL; // reset backoff on successful start
|
|
196
|
+
appendLog({ event: 'ready', port: parsed.port || port, pid: child.pid });
|
|
197
|
+
resolve({ baseUrl: this._baseUrl });
|
|
198
|
+
}
|
|
199
|
+
} catch (_) { /* non-JSON stdout line — skip */ }
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
child.stderr.on('data', (chunk) => {
|
|
204
|
+
// Forward compression server stderr to our stderr for visibility
|
|
205
|
+
process.stderr.write(`[compression-server] ${chunk.toString('utf-8')}`);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
child.on('error', (err) => {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
appendLog({ event: 'spawn_error', error: err.message });
|
|
211
|
+
reject(err);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
child.on('exit', (code, signal) => {
|
|
215
|
+
// On Windows, Node.js reports abnormal OS termination as exit code 4294967295
|
|
216
|
+
// (0xFFFFFFFF — the unsigned 32-bit representation of -1).
|
|
217
|
+
// This indicates an OS-level process termination (e.g. OOM, crash).
|
|
218
|
+
if (code === 4294967295) {
|
|
219
|
+
process.stderr.write(
|
|
220
|
+
`[headroom-proxy-manager] WARN: compression server on port ${port} exited with abnormal code 0xFFFFFFFF (-1) — ` +
|
|
221
|
+
`likely an OOM kill or unhandled exception in the server process. ` +
|
|
222
|
+
`Scheduling restart with backoff.\n`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
appendLog({ event: 'exit', code, signal, port });
|
|
226
|
+
this._child = null;
|
|
227
|
+
this._baseUrl = null;
|
|
228
|
+
|
|
229
|
+
// Schedule restart unless intentionally stopped
|
|
230
|
+
if (!this._stopped) {
|
|
231
|
+
const delay = this._backoffMs;
|
|
232
|
+
this._backoffMs = Math.min(this._backoffMs * 2, BACKOFF_MAX);
|
|
233
|
+
appendLog({ event: 'restart_scheduled', delayMs: delay });
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
if (!this._stopped) {
|
|
236
|
+
this._doStart().catch((restartErr) => {
|
|
237
|
+
process.stderr.write(
|
|
238
|
+
`[headroom-proxy-manager] Restart failed: ${restartErr.message}\n`
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}, delay);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Returns the base URL of the running compression server, or null if not running.
|
|
250
|
+
* Never throws.
|
|
251
|
+
*/
|
|
252
|
+
getBaseUrl() {
|
|
253
|
+
return this._baseUrl;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Stop the compression server and disable auto-restart.
|
|
258
|
+
*
|
|
259
|
+
* Awaits the child process exit event (with a 3s safety-net timeout) before
|
|
260
|
+
* resolving. This eliminates the H2 race where the old child still holds the
|
|
261
|
+
* TCP port while a subsequent start() call runs findFreePort.
|
|
262
|
+
*
|
|
263
|
+
* Safe to call multiple times — idempotent after the first call.
|
|
264
|
+
*/
|
|
265
|
+
async stop() {
|
|
266
|
+
this._stopped = true;
|
|
267
|
+
this._baseUrl = null;
|
|
268
|
+
const child = this._child;
|
|
269
|
+
this._child = null;
|
|
270
|
+
|
|
271
|
+
if (child) {
|
|
272
|
+
await new Promise((resolve) => {
|
|
273
|
+
// Safety net: if exit never fires (e.g. zombie process), resolve after 3s
|
|
274
|
+
// so callers are never blocked indefinitely.
|
|
275
|
+
const timeout = setTimeout(resolve, 3000);
|
|
276
|
+
timeout.unref?.(); // don't hold the event loop open
|
|
277
|
+
|
|
278
|
+
child.once('exit', () => {
|
|
279
|
+
clearTimeout(timeout);
|
|
280
|
+
resolve();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// Send the kill signal. kill() may throw on Windows if the process
|
|
284
|
+
// has already exited — catch and resolve immediately in that case.
|
|
285
|
+
try {
|
|
286
|
+
child.kill();
|
|
287
|
+
} catch (_) {
|
|
288
|
+
clearTimeout(timeout);
|
|
289
|
+
resolve();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
appendLog({ event: 'stopped' });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* buildCompressionEnv() — returns the env fragment to inject into Pi subprocess spawns.
|
|
300
|
+
*
|
|
301
|
+
* Called from helios-rpc-adapter.js when building the subprocess env object.
|
|
302
|
+
* Exported here so it can be tested directly — the test starts a real proxy manager,
|
|
303
|
+
* calls this function, and asserts HEADROOM_PROXY_URL is set to the real baseUrl.
|
|
304
|
+
*
|
|
305
|
+
* Returns { HEADROOM_PROXY_URL: string } when the proxy is running,
|
|
306
|
+
* or {} when the proxy is not started / getBaseUrl() is null.
|
|
307
|
+
* Never throws.
|
|
308
|
+
*/
|
|
309
|
+
function buildCompressionEnv() {
|
|
310
|
+
try {
|
|
311
|
+
const baseUrl = HeadroomProxyManager.getInstance().getBaseUrl();
|
|
312
|
+
if (baseUrl) {
|
|
313
|
+
return { HEADROOM_PROXY_URL: baseUrl };
|
|
314
|
+
}
|
|
315
|
+
} catch (_) {}
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = { HeadroomProxyManager, buildCompressionEnv };
|
|
@@ -327,9 +327,54 @@ function makeDepartmentPageGenerator({ mgQuery, broadcast }) {
|
|
|
327
327
|
|
|
328
328
|
rawData = await deptModule.fetchData(companyId);
|
|
329
329
|
|
|
330
|
+
// ── Step 1b: Harada cascade enrichment ───────────────────────────────────────
|
|
331
|
+
// Inject GoalPillar cascade progress for this department so the LLM has
|
|
332
|
+
// real strategic context even when department-specific signals are sparse.
|
|
333
|
+
// This is the primary data source for "Work in Progress" and "Decisions & Asks".
|
|
334
|
+
const cascadeRows = await mgQuery(
|
|
335
|
+
`MATCH (gp:GoalPillar {companyId: $cid})
|
|
336
|
+
WHERE gp.department = $dept
|
|
337
|
+
OR toLower(gp.name) CONTAINS toLower($dept)
|
|
338
|
+
OPTIONAL MATCH (gp)-[:HAS_CELL]->(ac:ActionCell)
|
|
339
|
+
OPTIONAL MATCH (t:Task {companyId: $cid})
|
|
340
|
+
WHERE t.pillarId = gp.id
|
|
341
|
+
RETURN gp.id AS pillarId, gp.name AS pillarName,
|
|
342
|
+
gp.l2Strategy AS l2Strategy, gp.l2Content AS l2Content,
|
|
343
|
+
gp.l2ReviewStatus AS l2ReviewStatus,
|
|
344
|
+
count(DISTINCT ac) AS totalCells,
|
|
345
|
+
count(DISTINCT CASE WHEN ac.status = 'closed' THEN ac END) AS closedCells,
|
|
346
|
+
collect(DISTINCT {id: t.id, title: t.title, status: t.status, originKind: t.originKind})[..5] AS recentTasks
|
|
347
|
+
ORDER BY gp.createdAt DESC LIMIT 3`,
|
|
348
|
+
{ cid: companyId, dept: department }
|
|
349
|
+
).catch(() => null);
|
|
350
|
+
|
|
351
|
+
const cascadeArr = cascadeRows && cascadeRows.rows
|
|
352
|
+
? cascadeRows.rows
|
|
353
|
+
: (Array.isArray(cascadeRows) ? cascadeRows : []);
|
|
354
|
+
|
|
355
|
+
if (cascadeArr.length > 0) {
|
|
356
|
+
const pillarSummary = cascadeArr.map((r, i) => {
|
|
357
|
+
const row = Array.isArray(r) ? {
|
|
358
|
+
pillarName: r[1], l2Strategy: r[2], l2Content: r[3], l2ReviewStatus: r[4],
|
|
359
|
+
totalCells: r[5], closedCells: r[6], recentTasks: r[7],
|
|
360
|
+
} : r;
|
|
361
|
+
const progress = row.totalCells > 0
|
|
362
|
+
? `${row.closedCells}/${row.totalCells} cells closed`
|
|
363
|
+
: 'no action cells yet';
|
|
364
|
+
const status = row.l2ReviewStatus ? `review: ${row.l2ReviewStatus}` : 'pending review';
|
|
365
|
+
return `${row.pillarName}: ${status}, ${progress}. Strategy: ${row.l2Strategy || row.l2Content || '(not yet defined)'}`;
|
|
366
|
+
}).join('\n');
|
|
367
|
+
rawData._haradaCascadeSummary = pillarSummary;
|
|
368
|
+
}
|
|
369
|
+
|
|
330
370
|
// ── Step 2: Build base prompt ────────────────────────────────────────────────
|
|
331
371
|
const basePrompt = deptModule.buildPrompt(rawData, today);
|
|
332
372
|
|
|
373
|
+
// ── Step 2a: Harada cascade context block ────────────────────────────────────
|
|
374
|
+
const cascadeBlock = rawData._haradaCascadeSummary
|
|
375
|
+
? `\nStrategic cascade progress (Hoshin Kanri):\n${rawData._haradaCascadeSummary}\n`
|
|
376
|
+
: '';
|
|
377
|
+
|
|
333
378
|
// ── Step 2b: Prior learnings priming block ────────────────────────────
|
|
334
379
|
const priorLearningsRaw = await mgQuery(
|
|
335
380
|
`MATCH (l:DeptLearning {companyId: $cid, department: $dept})
|
|
@@ -356,7 +401,7 @@ Additionally, include these fields in your JSON output:
|
|
|
356
401
|
"learningsStructured": [{"text": "string", "type": "observation|principle|required_change"}]
|
|
357
402
|
`;
|
|
358
403
|
|
|
359
|
-
const prompt = basePrompt + primingBlock + additionalOutputSchema;
|
|
404
|
+
const prompt = basePrompt + cascadeBlock + primingBlock + additionalOutputSchema;
|
|
360
405
|
|
|
361
406
|
// ── Step 3: Call LLM ───────────────────────────────────────────────────
|
|
362
407
|
const llmText = await callLLM(prompt, MAX_TOKENS);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* interpretation-engine.js — P_EXPLAIN interpretation loop
|
|
4
|
+
* Creates HITLInteraction nodes for new tasks and queues agent explanation.
|
|
5
|
+
*
|
|
6
|
+
* API contract: rawWriteFn is the safe-memgraph rawWrite function
|
|
7
|
+
* (exported as `{ rawWrite }` from lib/safe-memgraph.js), not an object.
|
|
8
|
+
* It is called as: rawWriteFn(cypher, params)
|
|
9
|
+
*
|
|
10
|
+
* Retry policy (inline CJS — no p-retry dependency):
|
|
11
|
+
* 3 attempts, exponential backoff 500ms→8000ms, randomised ±25%.
|
|
12
|
+
* Every failed attempt logs [hitl_write_retry] at warn level.
|
|
13
|
+
* Permanent failure (all attempts exhausted) logs [hitl_write_failed_permanently] at error level.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const _logger = (() => { try { return require('../lib/logger.js'); } catch { return null; } })();
|
|
17
|
+
const log = (lvl, msg, meta) => {
|
|
18
|
+
const fn = _logger?.[lvl === 'error' ? 'error' : 'warn'];
|
|
19
|
+
const text = `[${msg}]${meta ? ' ' + JSON.stringify(meta) : ''}`;
|
|
20
|
+
if (fn) fn(text);
|
|
21
|
+
else console[lvl === 'error' ? 'error' : 'warn'](`[${lvl}] ${msg}`, meta ?? '');
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Inline exponential-backoff retry — CJS, no external deps.
|
|
26
|
+
* @param {() => Promise<void>} fn — async function to retry
|
|
27
|
+
* @param {string} label — structured log event name prefix
|
|
28
|
+
* @param {{ retries?: number, minTimeout?: number, maxTimeout?: number }} [opts]
|
|
29
|
+
*/
|
|
30
|
+
async function withRetry(fn, label, opts = {}) {
|
|
31
|
+
const retries = opts.retries ?? 3;
|
|
32
|
+
const minTimeout = opts.minTimeout ?? 500;
|
|
33
|
+
const maxTimeout = opts.maxTimeout ?? 8000;
|
|
34
|
+
|
|
35
|
+
let lastErr;
|
|
36
|
+
for (let attempt = 1; attempt <= retries + 1; attempt++) {
|
|
37
|
+
try {
|
|
38
|
+
return await fn();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
lastErr = err;
|
|
41
|
+
const retriesLeft = retries + 1 - attempt;
|
|
42
|
+
if (retriesLeft === 0) break;
|
|
43
|
+
|
|
44
|
+
// Exponential backoff with ±25% jitter
|
|
45
|
+
const base = Math.min(minTimeout * Math.pow(2, attempt - 1), maxTimeout);
|
|
46
|
+
const jitter = base * 0.25 * (Math.random() * 2 - 1);
|
|
47
|
+
const delay = Math.max(minTimeout, Math.round(base + jitter));
|
|
48
|
+
|
|
49
|
+
log('warn', 'hitl_write_retry', {
|
|
50
|
+
event: `${label}_retry`,
|
|
51
|
+
attempt,
|
|
52
|
+
retriesLeft,
|
|
53
|
+
delayMs: delay,
|
|
54
|
+
err: err instanceof Error ? err.message : String(err),
|
|
55
|
+
});
|
|
56
|
+
await new Promise(r => setTimeout(r, delay));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
log('error', 'hitl_write_failed_permanently', {
|
|
61
|
+
event: `${label}_failed_permanently`,
|
|
62
|
+
err: lastErr instanceof Error ? lastErr.message : String(lastErr),
|
|
63
|
+
});
|
|
64
|
+
throw lastErr;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function createInterpretationExplanation(taskId, companyId, rawWriteFn) {
|
|
68
|
+
if (!rawWriteFn || typeof rawWriteFn !== 'function' || !taskId || !companyId) return;
|
|
69
|
+
const hiId = `hitl:interp:${taskId}:${Date.now()}`;
|
|
70
|
+
await withRetry(
|
|
71
|
+
() => rawWriteFn(
|
|
72
|
+
"MERGE (h:HITLInteraction {id: $hiId}) SET h.taskId = $taskId, h.companyId = $companyId, h.kind = 'interpretation', h.status = 'pending', h.createdAt = datetime()",
|
|
73
|
+
{ hiId, taskId, companyId }
|
|
74
|
+
),
|
|
75
|
+
'hitl_interp_create'
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function resolveInterpretation(hiId, taskId, action, body, rawWriteFn) {
|
|
80
|
+
if (!rawWriteFn || typeof rawWriteFn !== 'function' || !hiId) return;
|
|
81
|
+
await rawWriteFn(
|
|
82
|
+
"MATCH (h:HITLInteraction {id: $hiId}) SET h.status = 'resolved', h.action = $action, h.resolvedAt = datetime()",
|
|
83
|
+
{ hiId, action }
|
|
84
|
+
);
|
|
85
|
+
const commentId = `sys:interp:${hiId}`;
|
|
86
|
+
await rawWriteFn(
|
|
87
|
+
"MERGE (c:Comment {id: $commentId}) SET c.taskId = $taskId, c.body = $body, c.author = 'system', c.createdAt = datetime()",
|
|
88
|
+
{ commentId, taskId, body: body || `Interpretation ${action}` }
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { createInterpretationExplanation, resolveInterpretation };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* mental-model-cache.js — CachedPersonModel node CRUD
|
|
4
|
+
* Pre-assembled mental models for fast agent brief injection.
|
|
5
|
+
* Cache is invalidated by enrichmentNeeded flag on Person node.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const SCHEMA_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* writeCachedModel — upsert a CachedPersonModel node for a person in a company.
|
|
12
|
+
*
|
|
13
|
+
* @param {Function} mgQuery — Memgraph query function (async)
|
|
14
|
+
* @param {string} personId — Person node id
|
|
15
|
+
* @param {string} companyId — Company node id
|
|
16
|
+
* @param {object} assembledModel — Output from assembleMentalModel()
|
|
17
|
+
*/
|
|
18
|
+
async function writeCachedModel(mgQuery, personId, companyId, assembledModel) {
|
|
19
|
+
const modelId = `model:${companyId}:${personId}`;
|
|
20
|
+
const contextBrief = assembledModel.contextBrief || '';
|
|
21
|
+
const faveeJson = JSON.stringify(assembledModel.favee || {});
|
|
22
|
+
const topicsJson = JSON.stringify((assembledModel.activeTopics || []).slice(0, 10));
|
|
23
|
+
const questionsJson = JSON.stringify((assembledModel.openQuestions || []).slice(0, 5));
|
|
24
|
+
const completeness = contextBrief ? 'full' : (faveeJson !== '{}' ? 'partial' : 'none');
|
|
25
|
+
|
|
26
|
+
await mgQuery(
|
|
27
|
+
`MERGE (m:CachedPersonModel {id: $id})
|
|
28
|
+
ON CREATE SET
|
|
29
|
+
m.personId = $personId, m.companyId = $companyId,
|
|
30
|
+
m.contextBrief = $brief, m.faveeJson = $favee,
|
|
31
|
+
m.topicsJson = $topics, m.questionsJson = $questions,
|
|
32
|
+
m.assembledAt = localdatetime(), m.completeness = $completeness,
|
|
33
|
+
m.schemaVersion = $sv
|
|
34
|
+
ON MATCH SET
|
|
35
|
+
m.contextBrief = $brief, m.faveeJson = $favee,
|
|
36
|
+
m.topicsJson = $topics, m.questionsJson = $questions,
|
|
37
|
+
m.assembledAt = localdatetime(), m.completeness = $completeness,
|
|
38
|
+
m.dirtyAt = null
|
|
39
|
+
WITH m
|
|
40
|
+
MATCH (p:Person {id: $personId})
|
|
41
|
+
MERGE (p)-[:HAS_CACHED_MODEL]->(m)`,
|
|
42
|
+
{ id: modelId, personId, companyId, brief: contextBrief,
|
|
43
|
+
favee: faveeJson, topics: topicsJson, questions: questionsJson,
|
|
44
|
+
completeness, sv: SCHEMA_VERSION }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* invalidateCache — mark a CachedPersonModel dirty so next read triggers a refresh.
|
|
50
|
+
* Non-fatal: silently ignores errors if the model node doesn't exist yet.
|
|
51
|
+
*
|
|
52
|
+
* @param {Function} mgQuery
|
|
53
|
+
* @param {string} personId
|
|
54
|
+
* @param {string} companyId
|
|
55
|
+
*/
|
|
56
|
+
async function invalidateCache(mgQuery, personId, companyId) {
|
|
57
|
+
const modelId = `model:${companyId}:${personId}`;
|
|
58
|
+
await mgQuery(
|
|
59
|
+
`MATCH (m:CachedPersonModel {id: $id}) SET m.dirtyAt = localdatetime()`,
|
|
60
|
+
{ id: modelId }
|
|
61
|
+
).catch(() => {});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* getCachedModel — return a cached model if it exists, is not dirty, and is within maxAge.
|
|
66
|
+
* Returns null if the cache is cold, dirty, or expired.
|
|
67
|
+
*
|
|
68
|
+
* @param {Function} mgQuery
|
|
69
|
+
* @param {string} personId
|
|
70
|
+
* @param {string} companyId
|
|
71
|
+
* @param {number} maxAgeMinutes — default 60 minutes
|
|
72
|
+
* @returns {object|null}
|
|
73
|
+
*/
|
|
74
|
+
async function getCachedModel(mgQuery, personId, companyId, maxAgeMinutes = 60) {
|
|
75
|
+
const modelId = `model:${companyId}:${personId}`;
|
|
76
|
+
const rows = await mgQuery(
|
|
77
|
+
`MATCH (m:CachedPersonModel {id: $id})
|
|
78
|
+
WHERE m.dirtyAt IS NULL
|
|
79
|
+
AND m.assembledAt > localdatetime() - duration({minutes: $maxAge})
|
|
80
|
+
RETURN m.contextBrief, m.faveeJson, m.topicsJson, m.questionsJson,
|
|
81
|
+
m.assembledAt, m.completeness`,
|
|
82
|
+
{ id: modelId, maxAge: maxAgeMinutes }
|
|
83
|
+
);
|
|
84
|
+
if (!rows?.rows?.length) return null;
|
|
85
|
+
const r = rows.rows[0];
|
|
86
|
+
return {
|
|
87
|
+
contextBrief: r[0],
|
|
88
|
+
faveeJson: r[1],
|
|
89
|
+
topicsJson: r[2],
|
|
90
|
+
questionsJson: r[3],
|
|
91
|
+
assembledAt: r[4],
|
|
92
|
+
completeness: r[5],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { writeCachedModel, invalidateCache, getCachedModel, SCHEMA_VERSION };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* project-factory.js — Creates HeliosProject + ProjectDocument nodes when
|
|
4
|
+
* a GoalPillar is initialized via tickGoalDecompose().
|
|
5
|
+
*
|
|
6
|
+
* Called from daemon/lib/hbo-bridge.js tickGoalDecompose() after
|
|
7
|
+
* mandala.initializeMandala() succeeds for each GoalPillar.
|
|
8
|
+
*
|
|
9
|
+
* ID conventions:
|
|
10
|
+
* HeliosProject: proj:<companyId>:<pillarSlug>
|
|
11
|
+
* ProjectDocument: pdoc:<projectId>:main
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
async function ensureProject(mgQuery, companyId, goalId, pillarId, pillarTitle) {
|
|
15
|
+
const slug = pillarId.replace(/[^a-z0-9]/gi, '-').toLowerCase().slice(0, 40);
|
|
16
|
+
const projId = `proj:${companyId}:${slug}`;
|
|
17
|
+
const docId = `pdoc:${projId}:main`;
|
|
18
|
+
|
|
19
|
+
await mgQuery(
|
|
20
|
+
`MERGE (p:HeliosProject {id: $projId})
|
|
21
|
+
ON CREATE SET
|
|
22
|
+
p.companyId = $cid,
|
|
23
|
+
p.goalId = $goalId,
|
|
24
|
+
p.pillarId = $pillarId,
|
|
25
|
+
p.name = $name,
|
|
26
|
+
p.status = 'planning',
|
|
27
|
+
p.phase = 'planning',
|
|
28
|
+
p.createdAt = datetime()`,
|
|
29
|
+
{ projId, cid: companyId, goalId, pillarId, name: pillarTitle }
|
|
30
|
+
).catch(() => {});
|
|
31
|
+
|
|
32
|
+
await mgQuery(
|
|
33
|
+
`MERGE (d:ProjectDocument {id: $docId})
|
|
34
|
+
ON CREATE SET
|
|
35
|
+
d.projectId = $projId,
|
|
36
|
+
d.purpose = '',
|
|
37
|
+
d.approach = '',
|
|
38
|
+
d.successCriteria = '[]',
|
|
39
|
+
d.intentAnchor = '',
|
|
40
|
+
d.exclusions = '[]',
|
|
41
|
+
d.version = toInteger(1),
|
|
42
|
+
d.updatedAt = datetime()`,
|
|
43
|
+
{ docId, projId }
|
|
44
|
+
).catch(() => {});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { ensureProject };
|