@agentworkforce/sage 1.1.2 → 1.2.0

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 (53) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +56 -23
  3. package/dist/e2e/e2e-harness.d.ts +36 -0
  4. package/dist/e2e/e2e-harness.d.ts.map +1 -0
  5. package/dist/e2e/e2e-harness.js +278 -0
  6. package/dist/e2e/mock-cloud-proxy-server.d.ts +25 -0
  7. package/dist/e2e/mock-cloud-proxy-server.d.ts.map +1 -0
  8. package/dist/e2e/mock-cloud-proxy-server.js +149 -0
  9. package/dist/e2e/mock-relayfile-server.d.ts +35 -0
  10. package/dist/e2e/mock-relayfile-server.d.ts.map +1 -0
  11. package/dist/e2e/mock-relayfile-server.js +488 -0
  12. package/dist/integrations/cloud-proxy-provider.js +1 -1
  13. package/dist/integrations/freshness-envelope.js +1 -1
  14. package/dist/integrations/github.d.ts +24 -1
  15. package/dist/integrations/github.d.ts.map +1 -1
  16. package/dist/integrations/github.js +116 -1
  17. package/dist/integrations/linear-ingress.d.ts +30 -0
  18. package/dist/integrations/linear-ingress.d.ts.map +1 -0
  19. package/dist/integrations/linear-ingress.js +58 -0
  20. package/dist/integrations/notion-ingress.d.ts +26 -0
  21. package/dist/integrations/notion-ingress.d.ts.map +1 -0
  22. package/dist/integrations/notion-ingress.js +70 -0
  23. package/dist/integrations/provider-ingress-dedup.d.ts +14 -0
  24. package/dist/integrations/provider-ingress-dedup.d.ts.map +1 -0
  25. package/dist/integrations/provider-ingress-dedup.js +35 -0
  26. package/dist/integrations/provider-ingress-dedup.test.d.ts +2 -0
  27. package/dist/integrations/provider-ingress-dedup.test.d.ts.map +1 -0
  28. package/dist/integrations/provider-ingress-dedup.test.js +55 -0
  29. package/dist/integrations/provider-write-facade.d.ts +80 -0
  30. package/dist/integrations/provider-write-facade.d.ts.map +1 -0
  31. package/dist/integrations/provider-write-facade.js +417 -0
  32. package/dist/integrations/provider-write-facade.test.d.ts +2 -0
  33. package/dist/integrations/provider-write-facade.test.d.ts.map +1 -0
  34. package/dist/integrations/provider-write-facade.test.js +247 -0
  35. package/dist/integrations/read-your-writes.test.d.ts +2 -0
  36. package/dist/integrations/read-your-writes.test.d.ts.map +1 -0
  37. package/dist/integrations/read-your-writes.test.js +170 -0
  38. package/dist/integrations/recent-actions-overlay.d.ts +1 -0
  39. package/dist/integrations/recent-actions-overlay.d.ts.map +1 -1
  40. package/dist/integrations/recent-actions-overlay.js +3 -0
  41. package/dist/integrations/relayfile-reader-envelope.test.d.ts +2 -0
  42. package/dist/integrations/relayfile-reader-envelope.test.d.ts.map +1 -0
  43. package/dist/integrations/relayfile-reader-envelope.test.js +198 -0
  44. package/dist/integrations/relayfile-reader.d.ts +20 -1
  45. package/dist/integrations/relayfile-reader.d.ts.map +1 -1
  46. package/dist/integrations/relayfile-reader.js +334 -48
  47. package/dist/integrations/slack-egress.d.ts +2 -1
  48. package/dist/integrations/slack-egress.d.ts.map +1 -1
  49. package/dist/integrations/slack-egress.js +28 -1
  50. package/dist/observability.e2e.test.d.ts +2 -0
  51. package/dist/observability.e2e.test.d.ts.map +1 -0
  52. package/dist/observability.e2e.test.js +411 -0
  53. package/package.json +9 -4
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { RelayFileApiError } from '@relayfile/sdk';
3
+ import { error as envelopeError, hit, miss } from './freshness-envelope.js';
3
4
  const DEFAULT_CACHE_TTL_MS = 60_000;
4
5
  const DEFAULT_CACHE_CAPACITY = 256;
5
6
  const DEFAULT_SEARCH_LIMIT = 10;
@@ -19,6 +20,12 @@ function isRecord(value) {
19
20
  function readString(value) {
20
21
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
21
22
  }
23
+ function readRawString(value) {
24
+ return typeof value === 'string' ? value : undefined;
25
+ }
26
+ function toErrorMessage(error) {
27
+ return error instanceof Error && error.message ? error.message : String(error);
28
+ }
22
29
  function normalizeVfsPath(value) {
23
30
  const trimmed = value.trim();
24
31
  if (!trimmed) {
@@ -173,6 +180,106 @@ function toSearchResult(item, snippet) {
173
180
  properties: item.properties,
174
181
  };
175
182
  }
183
+ function stringifyOverlayData(data) {
184
+ try {
185
+ return JSON.stringify(data, null, 2);
186
+ }
187
+ catch {
188
+ return String(data);
189
+ }
190
+ }
191
+ function overlayEntryToFileContent(entry) {
192
+ const content = readRawString(entry.data.content);
193
+ if (content !== undefined) {
194
+ return entry.data.encoding === 'base64'
195
+ ? Buffer.from(content, 'base64').toString('utf-8')
196
+ : content;
197
+ }
198
+ return stringifyOverlayData(entry.data);
199
+ }
200
+ function overlayEntryToSerializedContent(entry) {
201
+ return stringifyOverlayData(entry.data);
202
+ }
203
+ function overlayEntryToProperties(entry) {
204
+ const properties = {
205
+ 'overlay.action': entry.action,
206
+ 'overlay.writtenAt': String(entry.writtenAt),
207
+ };
208
+ for (const [key, value] of Object.entries(entry.data)) {
209
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
210
+ properties[key] = String(value);
211
+ }
212
+ }
213
+ return properties;
214
+ }
215
+ function overlayEntryToQueryItem(entry) {
216
+ const content = overlayEntryToSerializedContent(entry);
217
+ // Handle numeric IDs (e.g., GitHub comment IDs) by converting to string
218
+ const idValue = typeof entry.data.id === 'number' ? String(entry.data.id) : readString(entry.data.id);
219
+ return {
220
+ path: entry.path,
221
+ revision: `overlay:${entry.writtenAt}`,
222
+ contentType: 'application/json',
223
+ provider: entry.provider,
224
+ providerObjectId: idValue ?? readString(entry.data.ts),
225
+ lastEditedAt: new Date(entry.writtenAt).toISOString(),
226
+ size: Buffer.byteLength(content),
227
+ properties: overlayEntryToProperties(entry),
228
+ };
229
+ }
230
+ function overlayHit(data, entries) {
231
+ const asOf = entries.reduce((latest, entry) => Math.max(latest, entry.writtenAt), 0) || Date.now();
232
+ return {
233
+ data,
234
+ status: 'hit',
235
+ source: 'overlay',
236
+ asOf,
237
+ ageMs: Math.max(0, Date.now() - asOf),
238
+ };
239
+ }
240
+ function rankOverlaySearchResults(query, entries, limit) {
241
+ const normalizedQuery = query.trim().toLowerCase();
242
+ const tokens = tokenizeQuery(normalizedQuery);
243
+ if (!normalizedQuery || entries.length === 0 || limit <= 0) {
244
+ return { results: [], entries: [] };
245
+ }
246
+ const candidates = entries
247
+ .map((entry) => {
248
+ const item = overlayEntryToQueryItem(entry);
249
+ const searchText = `${buildItemSearchText(item)}\n${overlayEntryToSerializedContent(entry)}`;
250
+ return {
251
+ entry,
252
+ item,
253
+ baseScore: scoreText(searchText, normalizedQuery, tokens),
254
+ };
255
+ })
256
+ .filter((candidate) => candidate.baseScore.score > 0);
257
+ candidates.sort(sortSearchResults);
258
+ const limited = candidates.slice(0, limit);
259
+ return {
260
+ results: limited.map((candidate) => toSearchResult(candidate.item, candidate.baseScore.snippet)),
261
+ entries: limited.map((candidate) => candidate.entry),
262
+ };
263
+ }
264
+ function dedupeOverlayEntriesByPath(entries) {
265
+ const seen = new Set();
266
+ const deduped = [];
267
+ for (const entry of entries) {
268
+ if (seen.has(entry.path)) {
269
+ continue;
270
+ }
271
+ seen.add(entry.path);
272
+ deduped.push(entry);
273
+ }
274
+ return deduped;
275
+ }
276
+ function formatOverlayEntries(title, entries) {
277
+ const lines = [title];
278
+ for (const entry of entries) {
279
+ lines.push('', `Path: ${entry.path}`, `Action: ${entry.action}`, `Written at: ${new Date(entry.writtenAt).toISOString()}`, 'Data:', overlayEntryToSerializedContent(entry));
280
+ }
281
+ return lines.join('\n');
282
+ }
176
283
  function treeEntryToQueryItem(entry, providerOverride) {
177
284
  return {
178
285
  path: entry.path,
@@ -356,6 +463,7 @@ export class SageRelayFileReader {
356
463
  workspaceId;
357
464
  cacheTtlMs;
358
465
  logger;
466
+ overlay;
359
467
  searchCache = new Map();
360
468
  fileCache = new Map();
361
469
  logTimestamps = new Map();
@@ -364,37 +472,62 @@ export class SageRelayFileReader {
364
472
  this.workspaceId = options.workspaceId.trim();
365
473
  this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
366
474
  this.logger = options.logger ?? console;
475
+ this.overlay = options.overlay ?? null;
367
476
  }
368
- static disabled(workspaceId = '') {
369
- return new SageRelayFileReader({ client: null, workspaceId });
477
+ static disabled(workspaceId = '', overlay) {
478
+ return new SageRelayFileReader({ client: null, workspaceId, overlay });
370
479
  }
371
480
  isEnabled() {
372
481
  return Boolean(this.client && this.workspaceId);
373
482
  }
374
483
  async searchFiles(query, provider, limit = DEFAULT_SEARCH_LIMIT) {
375
- return this.run('searchFiles', [], async () => {
376
- const trimmedQuery = query.trim();
377
- if (!trimmedQuery) {
378
- return [];
379
- }
380
- const normalizedProvider = provider?.trim() || undefined;
381
- const cacheKey = `search:${normalizedProvider ?? 'all'}:${limit}:${trimmedQuery.toLowerCase()}`;
382
- const cached = this.getCache(this.searchCache, cacheKey);
383
- if (cached) {
384
- return cached;
484
+ const envelope = await this.searchFilesEnveloped(query, provider, limit);
485
+ return envelope.data ?? [];
486
+ }
487
+ async searchFilesEnveloped(query, provider, limit = DEFAULT_SEARCH_LIMIT) {
488
+ const normalizedProvider = provider?.trim() || undefined;
489
+ const overlayResult = this.searchOverlay('searchFiles.overlay', query, {
490
+ provider: normalizedProvider,
491
+ pathPrefix: providerRoot(normalizedProvider),
492
+ }, limit);
493
+ const relayfileEnvelope = await this.relayfileEnvelope('searchFiles', () => this.searchFilesFromRelayFile(query, normalizedProvider, limit));
494
+ // Merge overlay and relayfile results, deduping by path. Seed `seen`
495
+ // with overlay paths so relayfile items that collide are dropped instead
496
+ // of duplicated.
497
+ if (overlayResult && overlayResult.data && overlayResult.data.length > 0) {
498
+ const seen = new Set(overlayResult.data.map((item) => item.path));
499
+ const merged = [...overlayResult.data];
500
+ for (const item of relayfileEnvelope.data ?? []) {
501
+ if (!seen.has(item.path) && merged.length < limit) {
502
+ seen.add(item.path);
503
+ merged.push(item);
504
+ }
385
505
  }
386
- const scanLimit = Math.min(DEFAULT_SCAN_LIMIT, Math.max(limit * 10, 50));
387
- const items = await this.collectSearchItems(normalizedProvider, providerRoot(normalizedProvider), scanLimit);
388
- const results = await this.rankSearchResults(trimmedQuery, items, limit);
389
- this.setCache(this.searchCache, cacheKey, results);
390
- return results;
391
- });
506
+ // Return a freshly-minted envelope. Inheriting `...relayfileEnvelope`
507
+ // would carry over status='error'/'miss' and a stale ageMs, causing
508
+ // consumers that branch on envelope.status to discard valid overlay
509
+ // data when the relayfile call errored.
510
+ const mergedAsOf = Math.max(overlayResult.asOf ?? 0, relayfileEnvelope.asOf ?? 0);
511
+ return {
512
+ data: merged.slice(0, limit),
513
+ status: 'hit',
514
+ source: 'overlay',
515
+ asOf: mergedAsOf,
516
+ ageMs: Math.max(0, Date.now() - mergedAsOf),
517
+ };
518
+ }
519
+ return relayfileEnvelope;
392
520
  }
393
521
  async readFile(filePath) {
394
- return this.run('readFile', null, async () => {
395
- const file = await this.readFileResponse(filePath);
396
- return decodeFileContent(file);
397
- });
522
+ const envelope = await this.readFileEnveloped(filePath);
523
+ return envelope.data;
524
+ }
525
+ async readFileEnveloped(filePath) {
526
+ const overlayResult = this.readOverlayFile('readFile.overlay', filePath);
527
+ if (overlayResult) {
528
+ return overlayResult;
529
+ }
530
+ return this.relayfileEnvelope('readFile', () => this.readFileFromRelayFile(filePath));
398
531
  }
399
532
  async listDir(dirPath, limit = DEFAULT_LIST_LIMIT) {
400
533
  return this.run('listDir', [], async () => {
@@ -438,25 +571,31 @@ export class SageRelayFileReader {
438
571
  });
439
572
  }
440
573
  async readGitHubFile(owner, repo, filePath, ref) {
441
- return this.run('readGitHubFile', null, async () => {
442
- const candidates = [
443
- buildGitHubContentPath(owner, repo, filePath, ref ?? 'HEAD'),
444
- buildGitHubContentPath(owner, repo, filePath, ref ?? 'main'),
445
- buildGitHubContentPath(owner, repo, filePath, ref ?? 'master'),
446
- ];
447
- const file = await this.readFirstAvailable(candidates);
448
- if (!file) {
449
- return null;
450
- }
451
- return formatGitHubFile(file, filePath);
452
- });
574
+ const envelope = await this.readGitHubFileEnveloped(owner, repo, filePath, ref);
575
+ return envelope.data;
576
+ }
577
+ async readGitHubFileEnveloped(owner, repo, filePath, ref) {
578
+ const candidates = [
579
+ buildGitHubContentPath(owner, repo, filePath, ref ?? 'HEAD'),
580
+ buildGitHubContentPath(owner, repo, filePath, ref ?? 'main'),
581
+ buildGitHubContentPath(owner, repo, filePath, ref ?? 'master'),
582
+ ];
583
+ const overlayResult = this.readGitHubFileOverlay(candidates, filePath);
584
+ if (overlayResult) {
585
+ return overlayResult;
586
+ }
587
+ return this.relayfileEnvelope('readGitHubFile', () => this.readGitHubFileFromRelayFile(owner, repo, filePath, ref));
453
588
  }
454
589
  async readGitHubPR(owner, repo, number) {
455
- return this.run('readGitHubPR', null, async () => {
456
- const metadata = await this.readFirstAvailable(buildGitHubPrMetaPaths(owner, repo, number));
457
- const diff = await this.tryReadFile(`${buildRepoRoot(owner, repo)}/pulls/${number}/diff.patch`);
458
- return formatGitHubPr(number, metadata ? parseJsonRecord(metadata) : null, diff);
459
- });
590
+ const envelope = await this.readGitHubPREnveloped(owner, repo, number);
591
+ return envelope.data;
592
+ }
593
+ async readGitHubPREnveloped(owner, repo, number) {
594
+ const overlayResult = this.readGitHubPROverlay(owner, repo, number);
595
+ if (overlayResult) {
596
+ return overlayResult;
597
+ }
598
+ return this.relayfileEnvelope('readGitHubPR', () => this.readGitHubPRFromRelayFile(owner, repo, number));
460
599
  }
461
600
  async readGitHubIssue(owner, repo, number) {
462
601
  return this.run('readGitHubIssue', null, async () => {
@@ -550,12 +689,19 @@ export class SageRelayFileReader {
550
689
  });
551
690
  }
552
691
  async searchSlackHistory(query, channel, limit = DEFAULT_SEARCH_LIMIT) {
553
- return this.run('searchSlackHistory', [], async () => {
554
- const root = channel ? buildSlackChannelRoot(channel) : `${SLACK_ROOT}/channels`;
555
- const items = await this.collectSearchItems('slack', root, DEFAULT_SCAN_LIMIT);
556
- const filtered = items.filter((item) => item.path.includes('/messages/') || item.path.includes('/threads/'));
557
- return this.rankSearchResults(query, filtered.length > 0 ? filtered : items, limit);
558
- });
692
+ const envelope = await this.searchSlackHistoryEnveloped(query, channel, limit);
693
+ return envelope.data ?? [];
694
+ }
695
+ async searchSlackHistoryEnveloped(query, channel, limit = DEFAULT_SEARCH_LIMIT) {
696
+ const root = channel ? buildSlackChannelRoot(channel) : `${SLACK_ROOT}/channels`;
697
+ const overlayResult = this.searchOverlay('searchSlackHistory.overlay', query, {
698
+ provider: 'slack',
699
+ pathPrefix: root,
700
+ }, limit, (entry) => entry.path.includes('/messages/') || entry.path.includes('/threads/'));
701
+ if (overlayResult) {
702
+ return overlayResult;
703
+ }
704
+ return this.relayfileEnvelope('searchSlackHistory', () => this.searchSlackHistoryFromRelayFile(query, channel, limit));
559
705
  }
560
706
  async run(scope, fallback, operation) {
561
707
  if (!this.isEnabled()) {
@@ -569,6 +715,91 @@ export class SageRelayFileReader {
569
715
  return fallback;
570
716
  }
571
717
  }
718
+ async relayfileEnvelope(scope, operation) {
719
+ if (!this.isEnabled()) {
720
+ return miss('relayfile');
721
+ }
722
+ try {
723
+ const data = await operation();
724
+ return data === null ? miss('relayfile') : hit(data, 'relayfile');
725
+ }
726
+ catch (error) {
727
+ if (this.isNotFoundError(error)) {
728
+ return miss('relayfile');
729
+ }
730
+ this.logError(scope, error);
731
+ return envelopeError(toErrorMessage(error), 'relayfile');
732
+ }
733
+ }
734
+ searchOverlay(scope, query, options, limit, filter) {
735
+ const overlay = this.overlay;
736
+ if (!overlay) {
737
+ return null;
738
+ }
739
+ try {
740
+ const entries = overlay.search(options).filter((entry) => !filter || filter(entry));
741
+ const ranked = rankOverlaySearchResults(query, entries, limit);
742
+ return ranked.results.length > 0 ? overlayHit(ranked.results, ranked.entries) : null;
743
+ }
744
+ catch (error) {
745
+ this.logError(scope, error);
746
+ return envelopeError(toErrorMessage(error), 'overlay');
747
+ }
748
+ }
749
+ readOverlayFile(scope, filePath) {
750
+ const overlay = this.overlay;
751
+ if (!overlay) {
752
+ return null;
753
+ }
754
+ try {
755
+ const entry = overlay.lookup(normalizeVfsPath(filePath));
756
+ return entry ? overlayHit(overlayEntryToFileContent(entry), [entry]) : null;
757
+ }
758
+ catch (error) {
759
+ this.logError(scope, error);
760
+ return envelopeError(toErrorMessage(error), 'overlay');
761
+ }
762
+ }
763
+ readGitHubFileOverlay(candidates, filePath) {
764
+ const overlay = this.overlay;
765
+ if (!overlay) {
766
+ return null;
767
+ }
768
+ try {
769
+ for (const candidate of candidates) {
770
+ const entry = overlay.lookup(candidate);
771
+ if (entry) {
772
+ return overlayHit(formatGitHubFile(overlayEntryToSerializedContent(entry), filePath), [entry]);
773
+ }
774
+ }
775
+ return null;
776
+ }
777
+ catch (error) {
778
+ this.logError('readGitHubFile.overlay', error);
779
+ return envelopeError(toErrorMessage(error), 'overlay');
780
+ }
781
+ }
782
+ readGitHubPROverlay(owner, repo, number) {
783
+ const overlay = this.overlay;
784
+ if (!overlay) {
785
+ return null;
786
+ }
787
+ const root = buildRepoRoot(owner, repo);
788
+ try {
789
+ const entries = dedupeOverlayEntriesByPath([
790
+ ...overlay.search({ provider: 'github', pathPrefix: `${root}/pulls/${number}/` }),
791
+ ...overlay.search({ provider: 'github', pathPrefix: `${root}/issues/${number}/comments/` }),
792
+ ]);
793
+ if (entries.length === 0) {
794
+ return null;
795
+ }
796
+ return overlayHit(formatOverlayEntries(`Recent GitHub PR #${number} overlay entries`, entries), entries);
797
+ }
798
+ catch (error) {
799
+ this.logError('readGitHubPR.overlay', error);
800
+ return envelopeError(toErrorMessage(error), 'overlay');
801
+ }
802
+ }
572
803
  getCache(cache, key) {
573
804
  const entry = cache.get(key);
574
805
  if (!entry) {
@@ -597,7 +828,7 @@ export class SageRelayFileReader {
597
828
  }
598
829
  }
599
830
  logError(scope, error) {
600
- const message = error instanceof Error ? error.message : String(error);
831
+ const message = toErrorMessage(error);
601
832
  const key = `${scope}:${message}`;
602
833
  const now = Date.now();
603
834
  const lastLoggedAt = this.logTimestamps.get(key) ?? 0;
@@ -608,7 +839,57 @@ export class SageRelayFileReader {
608
839
  this.logger.warn(`[relayfile-reader] ${scope} failed: ${message}`);
609
840
  }
610
841
  isNotFoundError(error) {
611
- return error instanceof RelayFileApiError && error.status === 404;
842
+ if (error instanceof RelayFileApiError && error.status === 404) {
843
+ return true;
844
+ }
845
+ if (!isRecord(error)) {
846
+ return false;
847
+ }
848
+ return error.status === 404 || error.statusCode === 404;
849
+ }
850
+ async searchFilesFromRelayFile(query, provider, limit = DEFAULT_SEARCH_LIMIT) {
851
+ const trimmedQuery = query.trim();
852
+ if (!trimmedQuery) {
853
+ return [];
854
+ }
855
+ const normalizedProvider = provider?.trim() || undefined;
856
+ const cacheKey = `search:${normalizedProvider ?? 'all'}:${limit}:${trimmedQuery.toLowerCase()}`;
857
+ const cached = this.getCache(this.searchCache, cacheKey);
858
+ if (cached) {
859
+ return cached;
860
+ }
861
+ const scanLimit = Math.min(DEFAULT_SCAN_LIMIT, Math.max(limit * 10, 50));
862
+ const items = await this.collectSearchItems(normalizedProvider, providerRoot(normalizedProvider), scanLimit);
863
+ const results = await this.rankSearchResults(trimmedQuery, items, limit);
864
+ this.setCache(this.searchCache, cacheKey, results);
865
+ return results;
866
+ }
867
+ async readFileFromRelayFile(filePath) {
868
+ const file = await this.readFileResponse(filePath);
869
+ return decodeFileContent(file);
870
+ }
871
+ async readGitHubFileFromRelayFile(owner, repo, filePath, ref) {
872
+ const candidates = [
873
+ buildGitHubContentPath(owner, repo, filePath, ref ?? 'HEAD'),
874
+ buildGitHubContentPath(owner, repo, filePath, ref ?? 'main'),
875
+ buildGitHubContentPath(owner, repo, filePath, ref ?? 'master'),
876
+ ];
877
+ const file = await this.readFirstAvailable(candidates);
878
+ if (!file) {
879
+ return null;
880
+ }
881
+ return formatGitHubFile(file, filePath);
882
+ }
883
+ async readGitHubPRFromRelayFile(owner, repo, number) {
884
+ const metadata = await this.readFirstAvailable(buildGitHubPrMetaPaths(owner, repo, number));
885
+ const diff = await this.tryReadFile(`${buildRepoRoot(owner, repo)}/pulls/${number}/diff.patch`);
886
+ return formatGitHubPr(number, metadata ? parseJsonRecord(metadata) : null, diff);
887
+ }
888
+ async searchSlackHistoryFromRelayFile(query, channel, limit = DEFAULT_SEARCH_LIMIT) {
889
+ const root = channel ? buildSlackChannelRoot(channel) : `${SLACK_ROOT}/channels`;
890
+ const items = await this.collectSearchItems('slack', root, DEFAULT_SCAN_LIMIT);
891
+ const filtered = items.filter((item) => item.path.includes('/messages/') || item.path.includes('/threads/'));
892
+ return this.rankSearchResults(query, filtered.length > 0 ? filtered : items, limit);
612
893
  }
613
894
  async readFileResponse(filePath) {
614
895
  const normalizedPath = normalizeVfsPath(filePath);
@@ -696,6 +977,7 @@ export class SageRelayFileReader {
696
977
  }
697
978
  async collectSearchItems(provider, rootPath, limit) {
698
979
  const collected = new Map();
980
+ let queryNotFoundError = null;
699
981
  try {
700
982
  const queried = await this.collectQueryItems({
701
983
  provider,
@@ -706,9 +988,10 @@ export class SageRelayFileReader {
706
988
  }
707
989
  }
708
990
  catch (error) {
709
- if (!this.isNotFoundError(error)) {
991
+ if (!this.isNotFoundError(error) || !rootPath) {
710
992
  throw error;
711
993
  }
994
+ queryNotFoundError = error;
712
995
  }
713
996
  if (rootPath && collected.size < limit) {
714
997
  const treeEntries = await this.collectTreeEntries(rootPath, DEFAULT_TREE_DEPTH, limit * 2);
@@ -719,6 +1002,9 @@ export class SageRelayFileReader {
719
1002
  collected.set(entry.path, treeEntryToQueryItem(entry, provider));
720
1003
  }
721
1004
  }
1005
+ if (queryNotFoundError && collected.size === 0) {
1006
+ throw queryNotFoundError;
1007
+ }
722
1008
  return [...collected.values()].slice(0, limit);
723
1009
  }
724
1010
  async rankSearchResults(query, items, limit) {
@@ -1,4 +1,5 @@
1
1
  import type { ConnectionProvider } from "./cloud-proxy-provider.js";
2
+ import type { RecentActionsOverlay } from "./recent-actions-overlay.js";
2
3
  import { type ThreadMessage } from "../slack.js";
3
4
  export interface SlackEgressResult {
4
5
  ok: boolean;
@@ -22,7 +23,7 @@ export interface SlackEgress {
22
23
  getBotUserId(): Promise<string | undefined>;
23
24
  getReactions(channel: string, timestamp: string): Promise<SlackReaction[]>;
24
25
  }
25
- export declare function createSlackEgress(provider: ConnectionProvider, workspaceId: string): SlackEgress;
26
+ export declare function createSlackEgress(provider: ConnectionProvider, workspaceId: string, overlay?: RecentActionsOverlay): SlackEgress;
26
27
  export type { ConnectionProvider } from "./cloud-proxy-provider.js";
27
28
  export type { ThreadMessage } from "../slack.js";
28
29
  //# sourceMappingURL=slack-egress.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slack-egress.d.ts","sourceRoot":"","sources":["../../src/integrations/slack-egress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAgB,MAAM,2BAA2B,CAAC;AAElF,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AA4BrB,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,IAAI,SAAwB,EAAE,YAAY,CAAC,EAAE,MAAM;CAMjF;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC1F,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9B,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAC5B,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC5C,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;CAC5E;AA+ED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,GAAG,WAAW,CAkKhG;AAED,YAAY,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"slack-egress.d.ts","sourceRoot":"","sources":["../../src/integrations/slack-egress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAgB,MAAM,2BAA2B,CAAC;AAClF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAExE,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AA4BrB,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,IAAI,SAAwB,EAAE,YAAY,CAAC,EAAE,MAAM;CAMjF;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC1F,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC9B,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,kBAAkB,CAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAC5B,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC5C,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;CAC5E;AAqGD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,kBAAkB,EAC5B,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,WAAW,CAgLb;AAED,YAAY,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
@@ -48,6 +48,20 @@ function createProxyRequest(workspaceId, endpoint, method, options) {
48
48
  ...(options?.params !== undefined ? { params: options.params } : {}),
49
49
  };
50
50
  }
51
+ function buildSlackMessageOverlayPath(channelId, ts) {
52
+ return `/slack/channels/${channelId}/messages/${ts}.json`;
53
+ }
54
+ function buildSlackReactionOverlayPath(channelId, ts, emoji) {
55
+ return `/slack/channels/${channelId}/messages/${ts}/reactions/${emoji}.json`;
56
+ }
57
+ function recordSlackOverlay(overlay, action, path, data) {
58
+ overlay?.record({
59
+ path,
60
+ data,
61
+ provider: "slack",
62
+ action,
63
+ });
64
+ }
51
65
  async function executeSlackRequest(provider, request) {
52
66
  try {
53
67
  const response = await provider.proxy(request);
@@ -63,7 +77,7 @@ async function executeSlackRequest(provider, request) {
63
77
  throw toSlackEgressError(error);
64
78
  }
65
79
  }
66
- export function createSlackEgress(provider, workspaceId) {
80
+ export function createSlackEgress(provider, workspaceId, overlay) {
67
81
  const resolvedWorkspaceId = assertWorkspaceId(workspaceId);
68
82
  let cachedBotUserId;
69
83
  let botUserIdLoaded = false;
@@ -74,6 +88,14 @@ export function createSlackEgress(provider, workspaceId) {
74
88
  ...(threadTs ? { thread_ts: threadTs } : {}),
75
89
  };
76
90
  const response = await executeSlackRequest(provider, createProxyRequest(resolvedWorkspaceId, "/chat.postMessage", "POST", { data: body }));
91
+ if (response.ok === true && response.ts) {
92
+ recordSlackOverlay(overlay, "postMessage", buildSlackMessageOverlayPath(channel, response.ts), {
93
+ channel,
94
+ text,
95
+ ts: response.ts,
96
+ ...(threadTs ? { thread_ts: threadTs } : {}),
97
+ });
98
+ }
77
99
  return {
78
100
  ok: response.ok === true,
79
101
  ...(response.ts ? { ts: response.ts } : {}),
@@ -116,6 +138,11 @@ export function createSlackEgress(provider, workspaceId) {
116
138
  name: emoji,
117
139
  },
118
140
  }));
141
+ recordSlackOverlay(overlay, "addReaction", buildSlackReactionOverlayPath(channel, timestamp, emoji), {
142
+ channel,
143
+ timestamp,
144
+ name: emoji,
145
+ });
119
146
  }
120
147
  catch {
121
148
  // Reactions are best-effort and should not block callers.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=observability.e2e.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.e2e.test.d.ts","sourceRoot":"","sources":["../src/observability.e2e.test.ts"],"names":[],"mappings":""}