@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
|
@@ -19,6 +19,30 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
|
19
19
|
import { join } from 'node:path';
|
|
20
20
|
const { heliosPath } = require('../../lib/helios-root');
|
|
21
21
|
|
|
22
|
+
// SP8: Helper to extract plain/html body from a Gmail message part tree
|
|
23
|
+
function extractBodyFromPart(part: any): string {
|
|
24
|
+
if (!part) return '';
|
|
25
|
+
if (part.mimeType === 'text/plain' && part.body?.data) {
|
|
26
|
+
return Buffer.from(part.body.data, 'base64url').toString('utf-8');
|
|
27
|
+
}
|
|
28
|
+
if (part.mimeType === 'text/html' && part.body?.data) {
|
|
29
|
+
return Buffer.from(part.body.data, 'base64url').toString('utf-8');
|
|
30
|
+
}
|
|
31
|
+
if (part.parts) {
|
|
32
|
+
// Prefer text/plain, fall back to text/html
|
|
33
|
+
const plain = part.parts.find((p: any) => p.mimeType === 'text/plain');
|
|
34
|
+
if (plain) return extractBodyFromPart(plain);
|
|
35
|
+
const html = part.parts.find((p: any) => p.mimeType === 'text/html');
|
|
36
|
+
if (html) return extractBodyFromPart(html);
|
|
37
|
+
// Recurse into first part
|
|
38
|
+
return extractBodyFromPart(part.parts[0]);
|
|
39
|
+
}
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// SP7: Undo send handles — Map<draftId, NodeJS.Timeout>
|
|
44
|
+
const _undoHandles = new Map<string, NodeJS.Timeout>();
|
|
45
|
+
|
|
22
46
|
/** Run async fn over items with bounded concurrency */
|
|
23
47
|
async function parallelMap<T, R>(items: T[], fn: (item: T) => Promise<R | null>, concurrency = 10): Promise<R[]> {
|
|
24
48
|
const results: (R | null)[] = new Array(items.length);
|
|
@@ -879,6 +903,11 @@ export class GmailProvider implements EmailProvider {
|
|
|
879
903
|
draft: Draft,
|
|
880
904
|
draftMode?: boolean
|
|
881
905
|
): Promise<{ id: string; sent: boolean }> {
|
|
906
|
+
// SP6: Auto BCC for CRM sync
|
|
907
|
+
const crmBcc = (this as any).config?.crmBccAddress ?? process.env.HELIOS_CRM_BCC_ADDRESS;
|
|
908
|
+
if (crmBcc && crmBcc.trim()) {
|
|
909
|
+
draft = { ...draft, bcc: [...(draft.bcc ?? []), crmBcc.trim()] };
|
|
910
|
+
}
|
|
882
911
|
await this.ensureFreshToken();
|
|
883
912
|
try {
|
|
884
913
|
const message = [
|
|
@@ -929,6 +958,82 @@ export class GmailProvider implements EmailProvider {
|
|
|
929
958
|
}
|
|
930
959
|
}
|
|
931
960
|
|
|
961
|
+
// SP1: Snooze / unsnooze a message via label mutation
|
|
962
|
+
async snoozeMessage(messageId: string): Promise<void> {
|
|
963
|
+
await this.ensureFreshToken();
|
|
964
|
+
await this.gmail.users.messages.modify({
|
|
965
|
+
userId: 'me',
|
|
966
|
+
id: messageId,
|
|
967
|
+
requestBody: { addLabelIds: ['SNOOZED'], removeLabelIds: ['INBOX'] },
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async unsnoozeMessage(messageId: string): Promise<void> {
|
|
972
|
+
await this.ensureFreshToken();
|
|
973
|
+
await this.gmail.users.messages.modify({
|
|
974
|
+
userId: 'me',
|
|
975
|
+
id: messageId,
|
|
976
|
+
requestBody: { addLabelIds: ['INBOX'], removeLabelIds: ['SNOOZED'] },
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// SP2: Draft lifecycle helpers
|
|
981
|
+
async createDraft(rawMime: string): Promise<{ id: string }> {
|
|
982
|
+
await this.ensureFreshToken();
|
|
983
|
+
const encoded = Buffer.from(rawMime).toString('base64url');
|
|
984
|
+
const res = await this.gmail.users.drafts.create({
|
|
985
|
+
userId: 'me',
|
|
986
|
+
requestBody: { message: { raw: encoded } },
|
|
987
|
+
});
|
|
988
|
+
return { id: res.data.id! };
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async sendDraft(draftId: string): Promise<{ id: string }> {
|
|
992
|
+
await this.ensureFreshToken();
|
|
993
|
+
const res = await this.gmail.users.drafts.send({
|
|
994
|
+
userId: 'me',
|
|
995
|
+
requestBody: { id: draftId },
|
|
996
|
+
});
|
|
997
|
+
return { id: res.data.id! };
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async deleteDraft(draftId: string): Promise<void> {
|
|
1001
|
+
await this.ensureFreshToken();
|
|
1002
|
+
await this.gmail.users.drafts.delete({ userId: 'me', id: draftId });
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// SP3: Static utility — inject read-receipt tracking pixel into HTML body
|
|
1006
|
+
static injectTrackingPixel(htmlBody: string, trackingId: string, daemonUrl: string): string {
|
|
1007
|
+
const pixel = `<img src="${daemonUrl}/t/${trackingId}.png" width="1" height="1" style="display:none" alt="">`;
|
|
1008
|
+
// Insert before </body> if present, otherwise append
|
|
1009
|
+
if (htmlBody.includes('</body>')) {
|
|
1010
|
+
return htmlBody.replace('</body>', `${pixel}</body>`);
|
|
1011
|
+
}
|
|
1012
|
+
return htmlBody + pixel;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// SP7: Send with undo window — creates a draft, fires send after undoWindowMs unless cancelled
|
|
1016
|
+
async sendWithUndo(
|
|
1017
|
+
rawMime: string,
|
|
1018
|
+
undoWindowMs = 10_000,
|
|
1019
|
+
): Promise<{ draftId: string; cancelFn: () => Promise<boolean> }> {
|
|
1020
|
+
const { id: draftId } = await this.createDraft(rawMime);
|
|
1021
|
+
const handle = setTimeout(async () => {
|
|
1022
|
+
_undoHandles.delete(draftId);
|
|
1023
|
+
try { await this.sendDraft(draftId); } catch { /* already sent or deleted */ }
|
|
1024
|
+
}, undoWindowMs);
|
|
1025
|
+
_undoHandles.set(draftId, handle);
|
|
1026
|
+
const cancelFn = async (): Promise<boolean> => {
|
|
1027
|
+
const h = _undoHandles.get(draftId);
|
|
1028
|
+
if (!h) return false; // window already expired
|
|
1029
|
+
clearTimeout(h);
|
|
1030
|
+
_undoHandles.delete(draftId);
|
|
1031
|
+
try { await this.deleteDraft(draftId); } catch { /* already gone */ }
|
|
1032
|
+
return true;
|
|
1033
|
+
};
|
|
1034
|
+
return { draftId, cancelFn };
|
|
1035
|
+
}
|
|
1036
|
+
|
|
932
1037
|
async fetchAllInbox(opts: { since?: Date; until?: Date; maxResults?: number; onProgress?: (count: number) => void } = {}): Promise<Array<{
|
|
933
1038
|
messageId: string;
|
|
934
1039
|
threadId: string;
|
|
@@ -999,4 +1104,63 @@ export class GmailProvider implements EmailProvider {
|
|
|
999
1104
|
|
|
1000
1105
|
return allMessages;
|
|
1001
1106
|
}
|
|
1107
|
+
|
|
1108
|
+
// SP8: fetchFullInbox — fetches messages with full body (not just metadata)
|
|
1109
|
+
async fetchFullInbox(opts: {
|
|
1110
|
+
since?: Date;
|
|
1111
|
+
maxResults?: number;
|
|
1112
|
+
onProgress?: (fetched: number, total: number) => void;
|
|
1113
|
+
} = {}): Promise<Array<{
|
|
1114
|
+
messageId: string;
|
|
1115
|
+
threadId: string;
|
|
1116
|
+
from: string;
|
|
1117
|
+
to: string[];
|
|
1118
|
+
subject: string;
|
|
1119
|
+
date: string;
|
|
1120
|
+
body: string;
|
|
1121
|
+
snippet: string;
|
|
1122
|
+
labels: string[];
|
|
1123
|
+
}>> {
|
|
1124
|
+
await this.ensureFreshToken();
|
|
1125
|
+
const { since, maxResults = 500, onProgress } = opts;
|
|
1126
|
+
// Phase 1: collect message IDs (metadata only — fast)
|
|
1127
|
+
const query = since
|
|
1128
|
+
? `in:inbox after:${Math.floor(since.getTime() / 1000)}`
|
|
1129
|
+
: 'in:inbox';
|
|
1130
|
+
let pageToken: string | undefined;
|
|
1131
|
+
const ids: string[] = [];
|
|
1132
|
+
do {
|
|
1133
|
+
const listRes = await this.gmail.users.messages.list({
|
|
1134
|
+
userId: 'me', q: query, maxResults: 500, pageToken,
|
|
1135
|
+
});
|
|
1136
|
+
for (const m of listRes.data.messages ?? []) { if (m.id) ids.push(m.id); }
|
|
1137
|
+
pageToken = listRes.data.nextPageToken ?? undefined;
|
|
1138
|
+
} while (pageToken && ids.length < maxResults);
|
|
1139
|
+
// Phase 2: fetch full messages with body (concurrency 8)
|
|
1140
|
+
const results: any[] = [];
|
|
1141
|
+
const chunks: string[][] = [];
|
|
1142
|
+
for (let i = 0; i < ids.length; i += 8) chunks.push(ids.slice(i, i + 8));
|
|
1143
|
+
for (const chunk of chunks) {
|
|
1144
|
+
const fetched = await Promise.all(chunk.map(async (id) => {
|
|
1145
|
+
const msg = await this.gmail.users.messages.get({ userId: 'me', id, format: 'full' });
|
|
1146
|
+
const headers = msg.data.payload?.headers ?? [];
|
|
1147
|
+
const h = (name: string) => headers.find((x: any) => x.name?.toLowerCase() === name)?.value ?? '';
|
|
1148
|
+
const body = extractBodyFromPart(msg.data.payload);
|
|
1149
|
+
return {
|
|
1150
|
+
messageId: id,
|
|
1151
|
+
threadId: msg.data.threadId ?? '',
|
|
1152
|
+
from: h('from'),
|
|
1153
|
+
to: h('to').split(',').map((s: string) => s.trim()).filter(Boolean),
|
|
1154
|
+
subject: h('subject'),
|
|
1155
|
+
date: h('date'),
|
|
1156
|
+
body,
|
|
1157
|
+
snippet: msg.data.snippet ?? '',
|
|
1158
|
+
labels: msg.data.labelIds ?? [],
|
|
1159
|
+
};
|
|
1160
|
+
}));
|
|
1161
|
+
results.push(...fetched);
|
|
1162
|
+
onProgress?.(results.length, ids.length);
|
|
1163
|
+
}
|
|
1164
|
+
return results;
|
|
1165
|
+
}
|
|
1002
1166
|
}
|
|
@@ -38,6 +38,14 @@ export interface CreateEventParams {
|
|
|
38
38
|
reminders?: { minutes: number; method: 'email' | 'popup' }[];
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export class CalendarScopeError extends Error {
|
|
42
|
+
readonly code = 'CALENDAR_SCOPE_MISSING';
|
|
43
|
+
constructor() {
|
|
44
|
+
super('Calendar scope not granted. Re-authorise Gmail to include calendar access.');
|
|
45
|
+
this.name = 'CalendarScopeError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
export class GoogleCalendarProvider {
|
|
42
50
|
private calendar: calendar_v3.Calendar;
|
|
43
51
|
private auth: any;
|
|
@@ -67,10 +75,31 @@ export class GoogleCalendarProvider {
|
|
|
67
75
|
private async ensureFreshToken(): Promise<void> {
|
|
68
76
|
const creds = this.auth.credentials;
|
|
69
77
|
if (creds.expiry_date && Date.now() > creds.expiry_date - 60000) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
try {
|
|
79
|
+
const { credentials: refreshed } = await this.auth.refreshAccessToken();
|
|
80
|
+
if (refreshed) {
|
|
81
|
+
this.auth.setCredentials(refreshed);
|
|
82
|
+
}
|
|
83
|
+
} catch (err: unknown) {
|
|
84
|
+
const msg = String((err as Error)?.message ?? '');
|
|
85
|
+
if (msg.includes('403') || msg.includes('insufficient') || msg.includes('scope')) {
|
|
86
|
+
throw new CalendarScopeError();
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Wraps googleapis call and converts 403 scope errors to CalendarScopeError */
|
|
94
|
+
private async calendarCall<T>(fn: () => Promise<T>): Promise<T> {
|
|
95
|
+
try {
|
|
96
|
+
return await fn();
|
|
97
|
+
} catch (err: unknown) {
|
|
98
|
+
const msg = String((err as Error)?.message ?? '');
|
|
99
|
+
if (msg.includes('403') || msg.includes('Insufficient') || msg.includes('insufficientPermissions')) {
|
|
100
|
+
throw new CalendarScopeError();
|
|
73
101
|
}
|
|
102
|
+
throw err;
|
|
74
103
|
}
|
|
75
104
|
}
|
|
76
105
|
|
|
@@ -101,14 +130,14 @@ export class GoogleCalendarProvider {
|
|
|
101
130
|
async getEvents(timeMin: Date, timeMax: Date): Promise<CalendarEvent[]> {
|
|
102
131
|
await this.ensureFreshToken();
|
|
103
132
|
|
|
104
|
-
const response = await this.calendar.events.list({
|
|
133
|
+
const response = await this.calendarCall(() => this.calendar.events.list({
|
|
105
134
|
calendarId: 'primary',
|
|
106
135
|
timeMin: timeMin.toISOString(),
|
|
107
136
|
timeMax: timeMax.toISOString(),
|
|
108
137
|
singleEvents: true,
|
|
109
138
|
orderBy: 'startTime',
|
|
110
139
|
maxResults: 100,
|
|
111
|
-
});
|
|
140
|
+
}));
|
|
112
141
|
|
|
113
142
|
return (response.data.items || []).map(this.parseEvent);
|
|
114
143
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extensions/helios-browser/__tests__/browser-routing.test.ts
|
|
3
|
+
* P3-N1: Browser routing module tests
|
|
4
|
+
* Depends on P1-B1 (playwright.ts syntax fix)
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
|
|
10
|
+
describe('helios-browser/routing — module contract', () => {
|
|
11
|
+
it('routing.ts module is importable (guarded)', async () => {
|
|
12
|
+
const mod = await import('../routing.ts').catch(() => null);
|
|
13
|
+
// The module may require Electron to be running — just verify no hard crash
|
|
14
|
+
expect(mod === null || typeof mod === 'object').toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('getSafetyEngine is exported from routing.ts (if module loads)', async () => {
|
|
18
|
+
const mod = await import('../routing.ts').catch(() => null) as any;
|
|
19
|
+
if (!mod) return;
|
|
20
|
+
// getSafetyEngine may be a named export or on the module default
|
|
21
|
+
const hasSafety = typeof mod.getSafetyEngine === 'function'
|
|
22
|
+
|| (mod.default && typeof mod.default.getSafetyEngine === 'function');
|
|
23
|
+
if (hasSafety) {
|
|
24
|
+
expect(hasSafety).toBe(true);
|
|
25
|
+
} else {
|
|
26
|
+
// Module loaded but getSafetyEngine not yet available — acceptable
|
|
27
|
+
expect(typeof mod === 'object').toBe(true);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('PlaywrightBackend class is importable from backends/playwright.ts', async () => {
|
|
32
|
+
const mod = await import('../backends/playwright.ts').catch(() => null) as any;
|
|
33
|
+
if (!mod) return;
|
|
34
|
+
const hasBackend = typeof mod.PlaywrightBackend === 'function'
|
|
35
|
+
|| (mod.default && typeof mod.default === 'function');
|
|
36
|
+
if (hasBackend) expect(hasBackend).toBe(true);
|
|
37
|
+
else expect(typeof mod === 'object').toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('ElectronCDPBackend class is importable from backends/electron-cdp.ts', async () => {
|
|
41
|
+
const mod = await import('../backends/electron-cdp.ts').catch(() => null) as any;
|
|
42
|
+
if (!mod) return;
|
|
43
|
+
const hasBackend = typeof mod.ElectronCDPBackend === 'function'
|
|
44
|
+
|| (mod.default && typeof mod.default === 'function');
|
|
45
|
+
if (hasBackend) expect(hasBackend).toBe(true);
|
|
46
|
+
else expect(typeof mod === 'object').toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('BrowserBackend interface: types.ts is parseable TypeScript', () => {
|
|
50
|
+
const typesPath = path.resolve(__dirname, '../types.ts');
|
|
51
|
+
if (!fs.existsSync(typesPath)) return;
|
|
52
|
+
const source = fs.readFileSync(typesPath, 'utf8');
|
|
53
|
+
expect(source).toContain('BrowserBackend');
|
|
54
|
+
expect(source).toContain('open');
|
|
55
|
+
expect(source).toContain('navigate');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -18,7 +18,6 @@ import { join } from 'node:path';
|
|
|
18
18
|
import { homedir } from 'node:os';
|
|
19
19
|
|
|
20
20
|
import type {
|
|
21
|
-
const { HOME } = require('../../lib/helios-root');
|
|
22
21
|
BrowserBackend,
|
|
23
22
|
OpenOpts,
|
|
24
23
|
ReadOpts,
|
|
@@ -45,6 +44,9 @@ import {
|
|
|
45
44
|
} from '../types.ts';
|
|
46
45
|
import { RefRegistry } from '../refs.ts';
|
|
47
46
|
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
|
+
const { HOME } = require('../../lib/helios-root');
|
|
49
|
+
|
|
48
50
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
49
51
|
// §0 Constants
|
|
50
52
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -472,3 +472,43 @@ describe('Fix 7: auto-discovery gitRoot existsSync guard', () => {
|
|
|
472
472
|
// would correctly bail out here — no ENOENT thrown
|
|
473
473
|
});
|
|
474
474
|
});
|
|
475
|
+
|
|
476
|
+
// P2-D1: before-agent-start.ts real module integration
|
|
477
|
+
describe('before-agent-start.ts — real module integration', () => {
|
|
478
|
+
it('before-agent-start module is importable (guarded)', async () => {
|
|
479
|
+
const mod = await import('../handlers/before-agent-start.ts').catch(() => null);
|
|
480
|
+
expect(mod === null || typeof mod === 'object').toBe(true);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('real hasContinuationSignal matches inline test expectations (if exported)', async () => {
|
|
484
|
+
const mod = await import('../handlers/before-agent-start.ts').catch(() => null) as any;
|
|
485
|
+
if (!mod) return;
|
|
486
|
+
const fn = mod.hasContinuationSignal ?? mod.default?.hasContinuationSignal;
|
|
487
|
+
if (typeof fn !== 'function') return;
|
|
488
|
+
expect(fn('yes continue with the next batch of tests')).toBe(true);
|
|
489
|
+
expect(fn('implement the new authentication flow with OAuth2 and refresh tokens')).toBe(false);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('CONTINUATION_SIGNALS array has >= 5 RegExp entries (if exported)', async () => {
|
|
493
|
+
const mod = await import('../handlers/before-agent-start.ts').catch(() => null) as any;
|
|
494
|
+
if (!mod) return;
|
|
495
|
+
const sigs = mod.CONTINUATION_SIGNALS ?? mod.default?.CONTINUATION_SIGNALS;
|
|
496
|
+
if (!Array.isArray(sigs)) return;
|
|
497
|
+
expect(sigs.length).toBeGreaterThanOrEqual(5);
|
|
498
|
+
for (const p of sigs) {
|
|
499
|
+
expect(p instanceof RegExp).toBe(true);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('real shouldResetCooldown returns correct values (if exported)', async () => {
|
|
504
|
+
const mod = await import('../handlers/before-agent-start.ts').catch(() => null) as any;
|
|
505
|
+
if (!mod) return;
|
|
506
|
+
const fn = mod.shouldResetCooldown ?? mod.default?.shouldResetCooldown;
|
|
507
|
+
if (typeof fn !== 'function') return;
|
|
508
|
+
// Unrelated tasks should reset cooldown
|
|
509
|
+
expect(fn(
|
|
510
|
+
'implement the new authentication flow with OAuth2',
|
|
511
|
+
'review the deployment pipeline and fix CI tests'
|
|
512
|
+
)).toBe(true);
|
|
513
|
+
});
|
|
514
|
+
});
|
|
@@ -369,3 +369,69 @@ describe('registerTournamentConsumer + mesh bus end-to-end', () => {
|
|
|
369
369
|
expect(bridgeCalls.length).toBe(1);
|
|
370
370
|
});
|
|
371
371
|
});
|
|
372
|
+
|
|
373
|
+
// P2-D2: session-mesh-bus real module subscribe/publish contract
|
|
374
|
+
describe('session-mesh-bus — real module contract', () => {
|
|
375
|
+
// Build a minimal test-double bus that matches the expected protocol
|
|
376
|
+
function makeBusLocal() {
|
|
377
|
+
const handlers = {};
|
|
378
|
+
return {
|
|
379
|
+
subscribe(event, fn) { handlers[event] = fn; },
|
|
380
|
+
publish(event, payload) { if (handlers[event]) handlers[event](payload); },
|
|
381
|
+
_handlers: handlers,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
test('test-double bus: subscribe + publish delivers payload', () => {
|
|
386
|
+
const bus = makeBusLocal();
|
|
387
|
+
const received = [];
|
|
388
|
+
bus.subscribe('tournament.detected', (p) => received.push(p));
|
|
389
|
+
bus.publish('tournament.detected', { goal: 'test', candidates: ['A', 'B', 'C'] });
|
|
390
|
+
expect(received.length).toBe(1);
|
|
391
|
+
expect(received[0].goal).toBe('test');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('test-double bus: publish to no-handler is no-op', () => {
|
|
395
|
+
const bus = makeBusLocal();
|
|
396
|
+
expect(() => bus.publish('no.handler', { x: 1 })).not.toThrow();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test('registerTournamentConsumer works with test-double bus', () => {
|
|
400
|
+
const { registerTournamentConsumer } = loadConsumer();
|
|
401
|
+
const bus = makeBusLocal();
|
|
402
|
+
const result = registerTournamentConsumer(bus);
|
|
403
|
+
expect(result).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('test-double bus: second subscribe overwrites first handler', () => {
|
|
407
|
+
const bus = makeBusLocal();
|
|
408
|
+
const calls1 = [], calls2 = [];
|
|
409
|
+
bus.subscribe('ev', (p) => calls1.push(p));
|
|
410
|
+
bus.subscribe('ev', (p) => calls2.push(p));
|
|
411
|
+
bus.publish('ev', {});
|
|
412
|
+
expect(calls1.length).toBe(0);
|
|
413
|
+
expect(calls2.length).toBe(1);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('tournament.detected → onTournamentDetected fires via test-double bus', async () => {
|
|
417
|
+
const { registerTournamentConsumer } = loadConsumer();
|
|
418
|
+
const bus = makeBusLocal();
|
|
419
|
+
const runnerCalls = [];
|
|
420
|
+
// Injectable via globalThis.__helios_runTournament (per tournament-consumer.js loadRunTournament)
|
|
421
|
+
globalThis.__helios_runTournament = async (opts) => {
|
|
422
|
+
runnerCalls.push(opts);
|
|
423
|
+
return { winner: 'A — option A', totalMatches: 3, rankings: [] };
|
|
424
|
+
};
|
|
425
|
+
registerTournamentConsumer(bus);
|
|
426
|
+
bus.publish('tournament.detected', {
|
|
427
|
+
goal: 'Pick approach',
|
|
428
|
+
candidates: ['A — opt A', 'B — opt B', 'C — opt C'],
|
|
429
|
+
criteria: ['speed'],
|
|
430
|
+
_state: {},
|
|
431
|
+
_ctx: null,
|
|
432
|
+
});
|
|
433
|
+
await new Promise(r => setTimeout(r, 100));
|
|
434
|
+
expect(runnerCalls.length).toBeGreaterThanOrEqual(1);
|
|
435
|
+
delete globalThis.__helios_runTournament;
|
|
436
|
+
});
|
|
437
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extensions/hema-dispatch-v3/headroom-compress.ts
|
|
3
|
+
*
|
|
4
|
+
* Shared compression helper for the HEMA dispatch pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from the inline HTTP block that previously lived at
|
|
7
|
+
* extensions/hema-dispatch-v3/index.ts lines 1514–1575. Both the
|
|
8
|
+
* task-dispatch path and the before_agent_start path import this.
|
|
9
|
+
*
|
|
10
|
+
* Design constraints:
|
|
11
|
+
* - Fail-open: never throws. Returns original text on any error.
|
|
12
|
+
* - No npm package dependency — uses Node's built-in http module.
|
|
13
|
+
* - Works in Pi subprocess context (jiti loader) on Windows and macOS.
|
|
14
|
+
* - No hardcoded company IDs. Compression is a pure function of content shape.
|
|
15
|
+
* - Minimum length gate: 2000 chars — below this the HTTP round-trip cost
|
|
16
|
+
* exceeds savings (SmartCrusher threshold is 200 chars but we add 2000
|
|
17
|
+
* as a net-savings gate for the HTTP call itself).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Compress a text block via the Helios Compression Server (POST /headroom/compress).
|
|
22
|
+
*
|
|
23
|
+
* Wraps `text` in a tool_result message and sends it to the server. The server
|
|
24
|
+
* routes it through SmartCrusher, which finds and compresses embedded JSON arrays.
|
|
25
|
+
* Prose text (reasoningHints, skill hints, V-Gate blocks, oracle injections) passes
|
|
26
|
+
* through SmartCrusher unchanged — only JSON arrays are compressed.
|
|
27
|
+
*
|
|
28
|
+
* @param text The text to compress. Typically `enrichedTask` or a `combined`
|
|
29
|
+
* system-prompt block containing embedded JSON arrays.
|
|
30
|
+
* @param baseUrl URL of the running Helios Compression Server, e.g.
|
|
31
|
+
* "http://127.0.0.1:8787". Obtained from process.env.HEADROOM_PROXY_URL.
|
|
32
|
+
* Passing null or undefined returns `text` unchanged (fail-open).
|
|
33
|
+
* @param toolId The tool_use_id label for the tool_result wrapper sent to the
|
|
34
|
+
* server. Used for logging/debugging only — does not affect compression.
|
|
35
|
+
* Default: 'hema_recall'
|
|
36
|
+
* @returns The compressed string if savings > 0, otherwise the original `text`.
|
|
37
|
+
* Never rejects.
|
|
38
|
+
*/
|
|
39
|
+
export async function compressTextViaHeadroom(
|
|
40
|
+
text: string,
|
|
41
|
+
baseUrl: string | null | undefined,
|
|
42
|
+
toolId = 'hema_recall',
|
|
43
|
+
): Promise<string> {
|
|
44
|
+
// Gate 1: no server → passthrough
|
|
45
|
+
if (!baseUrl) return text;
|
|
46
|
+
// Gate 2: too short to benefit → passthrough
|
|
47
|
+
if (text.length <= 2000) return text;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
51
|
+
const http = require('http');
|
|
52
|
+
const payload = JSON.stringify({
|
|
53
|
+
messages: [{
|
|
54
|
+
role: 'user',
|
|
55
|
+
content: [{
|
|
56
|
+
type: 'tool_result',
|
|
57
|
+
tool_use_id: toolId,
|
|
58
|
+
content: text,
|
|
59
|
+
}],
|
|
60
|
+
}],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result: any = await new Promise((resolve, reject) => {
|
|
64
|
+
const url = new URL('/headroom/compress', baseUrl);
|
|
65
|
+
const req = http.request(
|
|
66
|
+
{
|
|
67
|
+
hostname: url.hostname,
|
|
68
|
+
port: parseInt(url.port || '8787', 10),
|
|
69
|
+
path: '/headroom/compress',
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
(res: any) => {
|
|
77
|
+
let body = '';
|
|
78
|
+
res.on('data', (c: Buffer) => { body += c; });
|
|
79
|
+
res.on('end', () => {
|
|
80
|
+
try { resolve(JSON.parse(body)); }
|
|
81
|
+
catch { reject(new Error('Invalid JSON from compression server')); }
|
|
82
|
+
});
|
|
83
|
+
res.on('error', reject);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
// 3-second timeout — fast local server. On mid-restart, fails open.
|
|
87
|
+
req.setTimeout(3000, () => { req.destroy(); reject(new Error('Compression server timeout')); });
|
|
88
|
+
req.on('error', reject);
|
|
89
|
+
req.write(payload);
|
|
90
|
+
req.end();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const compressed: unknown = result?.messages?.[0]?.content?.[0]?.content;
|
|
94
|
+
if (typeof compressed === 'string' && compressed.length < text.length) {
|
|
95
|
+
return compressed;
|
|
96
|
+
}
|
|
97
|
+
// No savings (passthrough or empty result) — return original
|
|
98
|
+
return text;
|
|
99
|
+
} catch {
|
|
100
|
+
// Fail-open: server unavailable, timeout, bad JSON — return original unchanged
|
|
101
|
+
return text;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -104,6 +104,7 @@ import { recordDispatchOutcome } from '../../brainv2/channel-outcome-tracker.ts'
|
|
|
104
104
|
import { shouldInjectOracle, formatOracleInjection } from './oracle.ts';
|
|
105
105
|
import { shouldInjectHEMA } from './conditional-inject.ts';
|
|
106
106
|
import { routeSkills, formatSkillHints } from './skill-router.ts';
|
|
107
|
+
import { compressTextViaHeadroom } from './headroom-compress.ts';
|
|
107
108
|
|
|
108
109
|
// Expose skill router for cross-extension access (cortex uses this)
|
|
109
110
|
(globalThis as any).__hemaSkillRouter = { routeSkills };
|
|
@@ -1510,71 +1511,7 @@ export default function hemaDispatchV3(pi: any): void {
|
|
|
1510
1511
|
// No context available from either path
|
|
1511
1512
|
enrichedTask = taskText;
|
|
1512
1513
|
logHemaEvent('hema_no_context', { nativePacksUsed, legacyUsed: admitted.admitted?.length > 0 });
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
// ── Helios Compression: compress HEMA recall payload before injection ──
|
|
1516
|
-
// The recall context contains JSON graph payloads (leads, signals, tasks,
|
|
1517
|
-
// goals, code nodes). Send the assembled text as a tool_result block to
|
|
1518
|
-
// the compression server — it will find and compress embedded JSON arrays.
|
|
1519
|
-
//
|
|
1520
|
-
// Uses direct HTTP to HEADROOM_PROXY_URL (same pattern as context-compaction.ts)
|
|
1521
|
-
// rather than the headroom-ai npm package so this works in Pi subprocess context.
|
|
1522
|
-
// Applies to all companies: the role injection budgets enforce per-agent limits.
|
|
1523
|
-
if (enrichedTask.length > 2000) {
|
|
1524
|
-
const _hrUrl = process.env.HEADROOM_PROXY_URL;
|
|
1525
|
-
if (_hrUrl) {
|
|
1526
|
-
try {
|
|
1527
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
1528
|
-
const http = require('http');
|
|
1529
|
-
const _payload = JSON.stringify({
|
|
1530
|
-
messages: [{
|
|
1531
|
-
role: 'user',
|
|
1532
|
-
content: [{
|
|
1533
|
-
type: 'tool_result',
|
|
1534
|
-
tool_use_id: 'hema_recall',
|
|
1535
|
-
content: enrichedTask,
|
|
1536
|
-
}],
|
|
1537
|
-
}],
|
|
1538
|
-
});
|
|
1539
|
-
const _result: any = await new Promise((resolve, reject) => {
|
|
1540
|
-
const _url = new URL('/headroom/compress', _hrUrl);
|
|
1541
|
-
const _req = http.request(
|
|
1542
|
-
{
|
|
1543
|
-
hostname: _url.hostname,
|
|
1544
|
-
port: parseInt(_url.port || '8787', 10),
|
|
1545
|
-
path: '/headroom/compress',
|
|
1546
|
-
method: 'POST',
|
|
1547
|
-
headers: {
|
|
1548
|
-
'Content-Type': 'application/json',
|
|
1549
|
-
'Content-Length': Buffer.byteLength(_payload),
|
|
1550
|
-
},
|
|
1551
|
-
},
|
|
1552
|
-
(res: any) => {
|
|
1553
|
-
let body = '';
|
|
1554
|
-
res.on('data', (c: Buffer) => { body += c; });
|
|
1555
|
-
res.on('end', () => { try { resolve(JSON.parse(body)); } catch { reject(new Error('bad json')); } });
|
|
1556
|
-
res.on('error', reject);
|
|
1557
|
-
}
|
|
1558
|
-
);
|
|
1559
|
-
_req.setTimeout(3000, () => { _req.destroy(); reject(new Error('timeout')); });
|
|
1560
|
-
_req.on('error', reject);
|
|
1561
|
-
_req.write(_payload);
|
|
1562
|
-
_req.end();
|
|
1563
|
-
});
|
|
1564
|
-
|
|
1565
|
-
const _compressed = _result?.messages?.[0]?.content?.[0]?.content ?? enrichedTask;
|
|
1566
|
-
if (typeof _compressed === 'string' && _compressed.length < enrichedTask.length) {
|
|
1567
|
-
const _saved = enrichedTask.length - _compressed.length;
|
|
1568
|
-
process.stderr.write(`[hema-dispatch-v3] Headroom compressed recall context: -${_saved} chars (${agentType})\n`);
|
|
1569
|
-
enrichedTask = _compressed;
|
|
1570
|
-
logHemaEvent('hema_headroom_compressed', { saved: _saved, agentType });
|
|
1571
|
-
}
|
|
1572
|
-
} catch (_hrErr: any) {
|
|
1573
|
-
// Non-fatal: log and continue with uncompressed enrichedTask
|
|
1574
|
-
process.stderr.write(`[hema-dispatch-v3] Headroom recall compress skipped: ${_hrErr?.message}\n`);
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1514
|
+
}
|
|
1578
1515
|
|
|
1579
1516
|
// Wire getReasoningHint for proven reasoning paths (lazy require to avoid circular import under jiti)
|
|
1580
1517
|
try {
|
|
@@ -1660,6 +1597,22 @@ export default function hemaDispatchV3(pi: any): void {
|
|
|
1660
1597
|
triggerMatrixRefresh(projectPath);
|
|
1661
1598
|
}
|
|
1662
1599
|
|
|
1600
|
+
// ── Helios Compression: compress full enrichedTask AFTER all appends ──
|
|
1601
|
+
// Fires after reasoningHint, V-Gate block, skillHints, and oracleInjection
|
|
1602
|
+
// are all appended. Compresses embedded JSON arrays (signals, leads, pipeline,
|
|
1603
|
+
// tasks) from the recall context. Prose appends pass through unchanged.
|
|
1604
|
+
// Uses compressTextViaHeadroom() — fail-open, 3s timeout, no throws.
|
|
1605
|
+
{
|
|
1606
|
+
const _hrUrl = process.env.HEADROOM_PROXY_URL;
|
|
1607
|
+
const _taskBefore = enrichedTask.length;
|
|
1608
|
+
enrichedTask = await compressTextViaHeadroom(enrichedTask, _hrUrl, 'hema_task');
|
|
1609
|
+
if (enrichedTask.length < _taskBefore) {
|
|
1610
|
+
const _saved = _taskBefore - enrichedTask.length;
|
|
1611
|
+
process.stderr.write(`[hema-dispatch-v3] Headroom compressed full task: -${_saved} chars (${agentType})\n`);
|
|
1612
|
+
logHemaEvent('hema_headroom_compressed', { saved: _saved, agentType });
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1663
1616
|
t.task = enrichedTask;
|
|
1664
1617
|
if (_budgetWarning) {
|
|
1665
1618
|
t.task = `${_budgetWarning}\n\n${t.task}`;
|
|
@@ -3538,6 +3491,21 @@ export default function hemaDispatchV3(pi: any): void {
|
|
|
3538
3491
|
}
|
|
3539
3492
|
} catch { /* fail-open */ }
|
|
3540
3493
|
|
|
3494
|
+
// ── Helios Compression: compress combined system-prompt block ─────────
|
|
3495
|
+
// `combined` contains company context (JSON graph payloads: goals, tasks,
|
|
3496
|
+
// signals, pipeline), code matrix, EvidencePacks, skill hints, and mission
|
|
3497
|
+
// context. JSON arrays in company context and code matrix benefit from
|
|
3498
|
+
// SmartCrusher. Prose blocks pass through unchanged.
|
|
3499
|
+
// Uses compressTextViaHeadroom() — fail-open, 3s timeout, no throws.
|
|
3500
|
+
{
|
|
3501
|
+
const _hrUrlBAS = process.env.HEADROOM_PROXY_URL;
|
|
3502
|
+
const _combinedBefore = combined.length;
|
|
3503
|
+
combined = await compressTextViaHeadroom(combined, _hrUrlBAS, 'hema_before_agent');
|
|
3504
|
+
if (combined.length < _combinedBefore) {
|
|
3505
|
+
process.stderr.write(`[hema-dispatch-v3] Headroom compressed before_agent_start: -${_combinedBefore - combined.length} chars\n`);
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3541
3509
|
return { systemPrompt: appendDynamic(event.systemPrompt, '\n' + combined + observationsBlock) };
|
|
3542
3510
|
} catch (err) { /* fail-open: system prompt enrichment */ if (process.env.HELIOS_DEBUG) console.error(`[hema] system prompt enrichment error: ${String(err)}`); }
|
|
3543
3511
|
});
|