@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.
Files changed (157) hide show
  1. package/agents/business/talisman-ceo.md +183 -0
  2. package/agents/business/talisman-comms.md +257 -0
  3. package/agents/business/talisman-cto.md +153 -0
  4. package/agents/business/talisman-finance.md +246 -0
  5. package/agents/business/talisman-marketing.md +240 -0
  6. package/agents/business/talisman-sales.md +242 -0
  7. package/agents/business/talisman-support.md +236 -0
  8. package/bin/helios-rpc.js +19 -0
  9. package/daemon/adapters/helios-rpc-adapter.js +5 -12
  10. package/daemon/adapters/tui_wakeup.js +8 -0
  11. package/daemon/context-enrichment.js +27 -0
  12. package/daemon/daemon-manager.js +1 -1
  13. package/daemon/db/email-infrastructure-migrate.js +192 -0
  14. package/daemon/db/hbo-core-migrate.js +189 -0
  15. package/daemon/helios-api.js +863 -64
  16. package/daemon/helios-company-daemon.js +233 -33
  17. package/daemon/lib/blast-radius-analyzer.js +75 -0
  18. package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
  19. package/daemon/lib/forensic-log.js +113 -0
  20. package/daemon/lib/goal-research-pipeline.js +644 -0
  21. package/daemon/lib/harada/cascade-judge.js +84 -1
  22. package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
  23. package/daemon/lib/harada/pillar-dispatcher.js +23 -2
  24. package/daemon/lib/hbo-bridge.js +74 -6
  25. package/daemon/lib/headroom-middleware.js +129 -0
  26. package/daemon/lib/headroom-proxy-manager.js +309 -0
  27. package/daemon/lib/hed-engine.js +25 -0
  28. package/daemon/lib/intelligence/department-page-generator.js +46 -1
  29. package/daemon/lib/interpretation-engine.js +92 -0
  30. package/daemon/lib/mental-model-cache.js +96 -0
  31. package/daemon/lib/project-factory.js +47 -0
  32. package/daemon/lib/session-log-reader.js +93 -0
  33. package/daemon/lib/standard-work-bootstrap.js +87 -1
  34. package/daemon/lib/task-completion-processor.js +23 -0
  35. package/daemon/lib/wizard-engine.js +57 -6
  36. package/daemon/package.json +2 -1
  37. package/daemon/routes/agents.js +51 -6
  38. package/daemon/routes/channels.js +116 -2
  39. package/daemon/routes/crm.js +85 -0
  40. package/daemon/routes/dashboard.js +62 -16
  41. package/daemon/routes/dept.js +10 -1
  42. package/daemon/routes/email-triage.js +19 -10
  43. package/daemon/routes/hbo.js +618 -58
  44. package/daemon/routes/hed.js +133 -0
  45. package/daemon/routes/inbox.js +397 -8
  46. package/daemon/routes/project.js +580 -66
  47. package/daemon/routes/routines.js +14 -0
  48. package/daemon/routes/tasks.js +15 -1
  49. package/daemon/schema-apply.js +174 -0
  50. package/daemon/schema-definitions.js +433 -0
  51. package/daemon/schema-migrations-hbo.js +20 -0
  52. package/daemon/schema-migrations-hed.js +18 -0
  53. package/daemon/schema-migrations-proj.js +153 -0
  54. package/extensions/__tests__/codebase-index.test.ts +73 -0
  55. package/extensions/__tests__/extension-command-registration.test.ts +35 -0
  56. package/extensions/__tests__/git-push-guard.test.ts +68 -0
  57. package/extensions/context-compaction.ts +104 -76
  58. package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
  59. package/extensions/cortex/wal-replay.ts +91 -0
  60. package/extensions/email/actions/draft-response.ts +21 -1
  61. package/extensions/email/auth/accounts.ts +5 -11
  62. package/extensions/email/auth/inbox-dog.ts +5 -2
  63. package/extensions/email/backfill.ts +20 -13
  64. package/extensions/email/providers/gmail.ts +164 -0
  65. package/extensions/email/providers/google-calendar.ts +34 -5
  66. package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
  67. package/extensions/helios-browser/backends/playwright.ts +3 -1
  68. package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
  69. package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
  70. package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
  71. package/extensions/hema-dispatch-v3/index.ts +46 -72
  72. package/extensions/interview/__tests__/server.test.ts +117 -0
  73. package/extensions/lib/helios-root.cjs +46 -0
  74. package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
  75. package/extensions/warm-tick/warm-tick-maintenance.ts +164 -0
  76. package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
  77. package/lib/__tests__/crash-fixes.test.ts +49 -0
  78. package/lib/__tests__/hbo-core-store.test.js +238 -0
  79. package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
  80. package/lib/broker/__tests__/jit-subscription.test.js +44 -1
  81. package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
  82. package/lib/compression/__tests__/ccr-store.test.js +138 -0
  83. package/lib/compression/__tests__/pipeline.test.js +280 -0
  84. package/lib/compression/__tests__/smart-crusher.test.js +242 -0
  85. package/lib/compression/dist/server.js +34 -1
  86. package/lib/compression/dist/start-server.js +77 -0
  87. package/lib/event-bus.mts +1 -1
  88. package/lib/graph/learning/headroom-learn-bridge.js +175 -0
  89. package/lib/graph-availability.js +62 -0
  90. package/lib/hbo-core-store.compiled.js +834 -0
  91. package/lib/hbo-core-store.js +124 -0
  92. package/lib/hbo-core-store.ts +979 -0
  93. package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
  94. package/lib/skill-sync.js +6 -1
  95. package/lib/startup-integrity.js +9 -2
  96. package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
  97. package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
  98. package/lib/triage-core/__tests__/classifier.test.ts +45 -7
  99. package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
  100. package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
  101. package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
  102. package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
  103. package/lib/triage-core/__tests__/signals.test.ts +357 -0
  104. package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
  105. package/lib/triage-core/backfill-cost-estimator.ts +91 -0
  106. package/lib/triage-core/backfill-orchestrator.ts +119 -0
  107. package/lib/triage-core/classifier.ts +41 -8
  108. package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
  109. package/lib/triage-core/cos/response-debt.ts +2 -2
  110. package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
  111. package/lib/triage-core/graph/batch-persistence.ts +66 -2
  112. package/lib/triage-core/graph/betweenness-worker.js +75 -0
  113. package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
  114. package/lib/triage-core/graph/persistence.ts +1 -1
  115. package/lib/triage-core/graph/schema-v2.ts +2 -0
  116. package/lib/triage-core/graph/schema.cypher +11 -0
  117. package/lib/triage-core/graph/triage-query.ts +1 -1
  118. package/lib/triage-core/learning.ts +15 -20
  119. package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
  120. package/lib/triage-core/mental-model/cos-integration.ts +1 -1
  121. package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
  122. package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
  123. package/lib/triage-core/mental-model/key-facts.ts +1 -2
  124. package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
  125. package/lib/triage-core/mental-model/model-assembler.ts +16 -3
  126. package/lib/triage-core/orchestrator.ts +8 -15
  127. package/lib/triage-core/scheduled-sends.ts +39 -2
  128. package/lib/triage-core/signals/comms-style.ts +1 -1
  129. package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
  130. package/lib/triage-core/signals/favee-type.ts +6 -1
  131. package/lib/triage-core/signals/goal-relevance.ts +31 -2
  132. package/lib/triage-core/signals/personal-importance.ts +1 -1
  133. package/lib/triage-core/signals/referral-chain.ts +0 -1
  134. package/lib/triage-core/signals/relationship-decay.ts +4 -0
  135. package/lib/triage-core/signals/relationship-health.ts +6 -1
  136. package/lib/triage-core/signals/trajectory-signal.ts +38 -3
  137. package/lib/triage-core/tournament-runner.js +11 -1
  138. package/lib/triage-core/triage-llm-factory.ts +110 -0
  139. package/lib/triage-core/triage-local-llm.ts +145 -0
  140. package/lib/triage-core/triage-sql-store.ts +337 -0
  141. package/lib/triage-core/types.ts +2 -2
  142. package/lib/unified-graph.atomic.test.ts +52 -0
  143. package/lib/unified-graph.failure-categories.test.ts +55 -0
  144. package/package.json +18 -7
  145. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  146. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  147. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  148. package/skills/helios-bookkeeping/SKILL.md +321 -0
  149. package/skills/helios-briefer/SKILL.md +44 -0
  150. package/skills/helios-client-relations/SKILL.md +322 -0
  151. package/skills/helios-personal-triager/SKILL.md +45 -0
  152. package/skills/helios-recruitment/SKILL.md +317 -0
  153. package/skills/helios-relationship-nudger/SKILL.md +77 -0
  154. package/skills/helios-researcher/SKILL.md +44 -0
  155. package/skills/helios-scheduler/SKILL.md +58 -0
  156. package/skills/helios-tax-analyst/SKILL.md +280 -0
  157. 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<{ pagerank: number; communities: number }> {
321
- const result = { pagerank: 0, communities: 0 };
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.pagerank = rank
337
+ SET node.pageRank = rank
331
338
  RETURN count(node) AS total`,
332
339
  );
333
- result.pagerank = pr[0]?.total || 0;
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.pagerank = rank
350
+ SET node.pageRank = rank
344
351
  RETURN count(node) AS total`,
345
- );
346
- result.pagerank = pr[0]?.total || 0;
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.pagerank IS NULL SET p.pagerank = 0.0');
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.pagerank} nodes, Communities on ${algo.communities} nodes`);
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.pagerank > 0 RETURN count(p) AS c');
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
- const { credentials: refreshed } = await this.auth.refreshAccessToken();
71
- if (refreshed) {
72
- this.auth.setCredentials(refreshed);
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
+ }