@cgh567/agent 2.4.2 → 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/adapters/tui_wakeup.js +8 -0
- package/daemon/context-enrichment.js +27 -0
- package/daemon/daemon-manager.js +1 -1
- package/daemon/db/email-infrastructure-migrate.js +192 -0
- package/daemon/db/hbo-core-migrate.js +189 -0
- package/daemon/helios-api.js +863 -64
- package/daemon/helios-company-daemon.js +233 -33
- 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 +74 -6
- package/daemon/lib/headroom-middleware.js +129 -0
- package/daemon/lib/headroom-proxy-manager.js +309 -0
- package/daemon/lib/hed-engine.js +25 -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 +23 -0
- package/daemon/lib/wizard-engine.js +57 -6
- 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 +618 -58
- package/daemon/routes/hed.js +133 -0
- package/daemon/routes/inbox.js +397 -8
- package/daemon/routes/project.js +580 -66
- package/daemon/routes/routines.js +14 -0
- package/daemon/routes/tasks.js +15 -1
- package/daemon/schema-apply.js +174 -0
- package/daemon/schema-definitions.js +433 -0
- package/daemon/schema-migrations-hbo.js +20 -0
- package/daemon/schema-migrations-hed.js +18 -0
- package/daemon/schema-migrations-proj.js +153 -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/cortex/wal-replay.ts +91 -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 +46 -72
- 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 +164 -0
- package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
- package/lib/__tests__/crash-fixes.test.ts +49 -0
- package/lib/__tests__/hbo-core-store.test.js +238 -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/event-bus.mts +1 -1
- package/lib/graph/learning/headroom-learn-bridge.js +175 -0
- package/lib/graph-availability.js +62 -0
- package/lib/hbo-core-store.compiled.js +834 -0
- package/lib/hbo-core-store.js +124 -0
- package/lib/hbo-core-store.ts +979 -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 +41 -8
- 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 +11 -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/key-facts.ts +1 -2
- 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 +8 -15
- 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 +18 -7
- 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
|
@@ -129,7 +129,7 @@ export async function ingestEmailIndex(indexPath: string): Promise<BackfillResul
|
|
|
129
129
|
|
|
130
130
|
const emailNodes: Array<{
|
|
131
131
|
messageId: string; subject: string; date: string; isoDate: string;
|
|
132
|
-
threadId: string; labels: string; snippet: string;
|
|
132
|
+
threadId: string; labels: string; snippet: string; listUnsubscribeUrl?: string; from: string;
|
|
133
133
|
}> = [];
|
|
134
134
|
const senderEdges: Array<{ email: string; name: string; messageId: string }> = [];
|
|
135
135
|
const recipientEdges: Array<{ email: string; name: string; messageId: string }> = [];
|
|
@@ -155,6 +155,9 @@ export async function ingestEmailIndex(indexPath: string): Promise<BackfillResul
|
|
|
155
155
|
threadId: email.threadId || '',
|
|
156
156
|
labels,
|
|
157
157
|
snippet: (email.snippet || '').slice(0, 200),
|
|
158
|
+
// SP4: Store List-Unsubscribe header for one-click unsubscribe
|
|
159
|
+
listUnsubscribeUrl: email.listUnsubscribeUrl || email.headers?.['list-unsubscribe'] || email.headers?.['List-Unsubscribe'] || undefined,
|
|
160
|
+
from: sender.email, // H3: store sender email for block_sender query
|
|
158
161
|
});
|
|
159
162
|
|
|
160
163
|
senderEdges.push({ email: sender.email, name: sender.name, messageId: email.messageId });
|
|
@@ -194,11 +197,15 @@ export async function ingestEmailIndex(indexPath: string): Promise<BackfillResul
|
|
|
194
197
|
e.labels = item.labels,
|
|
195
198
|
e.snippet = item.snippet,
|
|
196
199
|
e.body = '',
|
|
197
|
-
e.bodyStored = false
|
|
200
|
+
e.bodyStored = false,
|
|
201
|
+
e.listUnsubscribeUrl = item.listUnsubscribeUrl,
|
|
202
|
+
e.from = item.from
|
|
198
203
|
ON MATCH SET
|
|
199
204
|
e.subject = item.subject,
|
|
200
205
|
e.labels = item.labels,
|
|
201
|
-
e.snippet = item.snippet
|
|
206
|
+
e.snippet = item.snippet,
|
|
207
|
+
e.listUnsubscribeUrl = coalesce(item.listUnsubscribeUrl, e.listUnsubscribeUrl),
|
|
208
|
+
e.from = coalesce(item.from, e.from)`,
|
|
202
209
|
{ batch: chunk },
|
|
203
210
|
);
|
|
204
211
|
result.emailsCreated += chunk.length;
|
|
@@ -317,8 +324,8 @@ export async function ingestEmailAddresses(indexPath: string): Promise<{ created
|
|
|
317
324
|
// ---------------------------------------------------------------------------
|
|
318
325
|
// Phase 0E: Run PageRank + Community Detection on Person/Email subgraph
|
|
319
326
|
// ---------------------------------------------------------------------------
|
|
320
|
-
export async function runGraphAlgorithms(): Promise<{
|
|
321
|
-
const result = {
|
|
327
|
+
export async function runGraphAlgorithms(): Promise<{ pageRank: number; communities: number }> {
|
|
328
|
+
const result = { pageRank: 0, communities: 0 };
|
|
322
329
|
|
|
323
330
|
// PageRank — MAGE algorithm with 100 iterations, damping factor 0.85, scoped to Person nodes
|
|
324
331
|
try {
|
|
@@ -327,10 +334,10 @@ export async function runGraphAlgorithms(): Promise<{ pagerank: number; communit
|
|
|
327
334
|
YIELD node, rank
|
|
328
335
|
WITH node, rank
|
|
329
336
|
WHERE "Person" IN labels(node)
|
|
330
|
-
SET node.
|
|
337
|
+
SET node.pageRank = rank
|
|
331
338
|
RETURN count(node) AS total`,
|
|
332
339
|
);
|
|
333
|
-
|
|
340
|
+
result.pageRank = pr[0]?.total || 0;
|
|
334
341
|
} catch (err: any) {
|
|
335
342
|
console.error(` [backfill] MAGE PageRank failed: ${err.message}, falling back to degree-based`);
|
|
336
343
|
// Fallback: degree-based PageRank without parameters
|
|
@@ -340,10 +347,10 @@ export async function runGraphAlgorithms(): Promise<{ pagerank: number; communit
|
|
|
340
347
|
YIELD node, rank
|
|
341
348
|
WITH node, rank
|
|
342
349
|
WHERE "Person" IN labels(node)
|
|
343
|
-
SET node.
|
|
350
|
+
SET node.pageRank = rank
|
|
344
351
|
RETURN count(node) AS total`,
|
|
345
|
-
|
|
346
|
-
|
|
352
|
+
);
|
|
353
|
+
result.pageRank = pr[0]?.total || 0;
|
|
347
354
|
} catch (fallbackErr: any) {
|
|
348
355
|
console.error(` [backfill] Fallback PageRank also failed: ${fallbackErr.message}`);
|
|
349
356
|
}
|
|
@@ -351,7 +358,7 @@ export async function runGraphAlgorithms(): Promise<{ pagerank: number; communit
|
|
|
351
358
|
|
|
352
359
|
// Fill NULL pageranks (MAGE only yields nodes above internal threshold)
|
|
353
360
|
try {
|
|
354
|
-
await graphWrite('MATCH (p:Person) WHERE p.
|
|
361
|
+
await graphWrite('MATCH (p:Person) WHERE p.pageRank IS NULL SET p.pageRank = 0.0');
|
|
355
362
|
} catch (e) { process.stderr.write(`[email-backfill] non-critical: ${String(e)}\n`); }
|
|
356
363
|
|
|
357
364
|
// Community detection — scoped to Person nodes
|
|
@@ -551,7 +558,7 @@ export async function runFullBackfill(options?: {
|
|
|
551
558
|
// Step 5: PageRank + Community Detection (0E)
|
|
552
559
|
console.log('\n[0E] Running PageRank + Community Detection...');
|
|
553
560
|
const algo = await runGraphAlgorithms();
|
|
554
|
-
console.log(` ✓ PageRank on ${algo.
|
|
561
|
+
console.log(` ✓ PageRank on ${algo.pageRank} nodes, Communities on ${algo.communities} nodes`);
|
|
555
562
|
|
|
556
563
|
// Step 6: VIP flags (0F)
|
|
557
564
|
if (existsSync(gmailIndex)) {
|
|
@@ -574,7 +581,7 @@ export async function runFullBackfill(options?: {
|
|
|
574
581
|
const pc = await graphRead('MATCH (p:Person) RETURN count(p) AS c');
|
|
575
582
|
const sf = await graphRead('MATCH ()-[r:SENT_FROM]->() RETURN count(r) AS c');
|
|
576
583
|
const st = await graphRead('MATCH ()-[r:SENT_TO]->() RETURN count(r) AS c');
|
|
577
|
-
const pr = await graphRead('MATCH (p:Person) WHERE p.
|
|
584
|
+
const pr = await graphRead('MATCH (p:Person) WHERE p.pageRank > 0 RETURN count(p) AS c');
|
|
578
585
|
const vp = await graphRead('MATCH (p:Person {isVip: true}) RETURN count(p) AS c');
|
|
579
586
|
const val = (r: any[]) => { const v = r[0]?.c; return (v && typeof v === 'object' && 'low' in v) ? v.low : (v || 0); };
|
|
580
587
|
console.log(` Email nodes: ${val(ec)}`);
|
|
@@ -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
|
+
}
|