@agent-native/core 0.114.6 → 0.114.7

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 (141) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
  5. package/corpus/core/src/cli/mcp.ts +124 -0
  6. package/corpus/core/src/cli/skills-content/help.ts +4 -3
  7. package/corpus/core/src/cli/skills-content/index.ts +1 -0
  8. package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
  9. package/corpus/core/src/cli/skills.ts +115 -13
  10. package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
  11. package/corpus/core/src/mcp-client/index.ts +9 -0
  12. package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
  13. package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
  14. package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
  15. package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
  16. package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
  17. package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
  18. package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
  19. package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
  20. package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
  21. package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
  22. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
  23. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
  24. package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
  25. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
  26. package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
  27. package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
  28. package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
  29. package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
  30. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
  31. package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
  32. package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
  33. package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
  34. package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
  35. package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
  37. package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
  39. package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
  42. package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
  44. package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
  47. package/corpus/templates/clips/desktop/package.json +1 -0
  48. package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
  49. package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
  50. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
  51. package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
  52. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
  53. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
  54. package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
  55. package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
  56. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
  57. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  58. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
  59. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
  60. package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
  61. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
  62. package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
  63. package/corpus/templates/clips/desktop/src/styles.css +956 -134
  64. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
  65. package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
  66. package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
  67. package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
  68. package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
  69. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
  70. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
  71. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
  72. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
  73. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
  74. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
  75. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
  76. package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
  77. package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
  78. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
  79. package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
  80. package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
  81. package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
  82. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
  83. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
  84. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
  86. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
  87. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
  88. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
  89. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
  90. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
  91. package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
  92. package/dist/cli/mcp-config-writers.d.ts +2 -2
  93. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  94. package/dist/cli/mcp-config-writers.js +6 -6
  95. package/dist/cli/mcp-config-writers.js.map +1 -1
  96. package/dist/cli/mcp.d.ts +14 -0
  97. package/dist/cli/mcp.d.ts.map +1 -1
  98. package/dist/cli/mcp.js +79 -0
  99. package/dist/cli/mcp.js.map +1 -1
  100. package/dist/cli/skills-content/help.d.ts +1 -1
  101. package/dist/cli/skills-content/help.d.ts.map +1 -1
  102. package/dist/cli/skills-content/help.js +4 -3
  103. package/dist/cli/skills-content/help.js.map +1 -1
  104. package/dist/cli/skills-content/index.d.ts +1 -0
  105. package/dist/cli/skills-content/index.d.ts.map +1 -1
  106. package/dist/cli/skills-content/index.js +1 -0
  107. package/dist/cli/skills-content/index.js.map +1 -1
  108. package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
  109. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
  110. package/dist/cli/skills-content/rewind-skill.js +53 -0
  111. package/dist/cli/skills-content/rewind-skill.js.map +1 -0
  112. package/dist/cli/skills.d.ts +6 -0
  113. package/dist/cli/skills.d.ts.map +1 -1
  114. package/dist/cli/skills.js +90 -14
  115. package/dist/cli/skills.js.map +1 -1
  116. package/dist/mcp/screen-memory-stdio.d.ts +365 -0
  117. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  118. package/dist/mcp/screen-memory-stdio.js +1014 -91
  119. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  120. package/dist/mcp-client/index.d.ts +1 -1
  121. package/dist/mcp-client/index.d.ts.map +1 -1
  122. package/dist/mcp-client/index.js +1 -1
  123. package/dist/mcp-client/index.js.map +1 -1
  124. package/dist/mcp-client/screen-memory-local.d.ts +90 -0
  125. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
  126. package/dist/mcp-client/screen-memory-local.js +590 -12
  127. package/dist/mcp-client/screen-memory-local.js.map +1 -1
  128. package/dist/notifications/routes.d.ts +3 -3
  129. package/dist/observability/routes.d.ts +1 -1
  130. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  131. package/dist/resources/handlers.d.ts +1 -1
  132. package/package.json +1 -1
  133. package/src/cli/mcp-config-writers.ts +6 -4
  134. package/src/cli/mcp.ts +124 -0
  135. package/src/cli/skills-content/help.ts +4 -3
  136. package/src/cli/skills-content/index.ts +1 -0
  137. package/src/cli/skills-content/rewind-skill.ts +52 -0
  138. package/src/cli/skills.ts +115 -13
  139. package/src/mcp/screen-memory-stdio.ts +1360 -104
  140. package/src/mcp-client/index.ts +9 -0
  141. package/src/mcp-client/screen-memory-local.ts +847 -13
@@ -5,6 +5,11 @@ export interface ScreenMemoryConfig {
5
5
  maxBytes: number;
6
6
  segmentSeconds: number;
7
7
  sampleIntervalSeconds: number;
8
+ captureMode: "visuals" | "visuals-audio";
9
+ reviewBeforeSending: boolean;
10
+ agentClipRetention: "forever" | "24-hours" | "7-days" | "30-days";
11
+ excludedBundleIds: string[];
12
+ excludePrivateWindows: boolean;
8
13
  }
9
14
 
10
15
  export interface ScreenMemoryStatus {
@@ -37,6 +42,74 @@ export interface ScreenMemoryContextItem {
37
42
  sourceFile: string;
38
43
  }
39
44
 
45
+ export type ScreenMemoryEvidenceSourceType =
46
+ | "app-context"
47
+ | "transcript"
48
+ | "ocr";
49
+
50
+ export interface ScreenMemoryTimeRange {
51
+ startedAt: string | null;
52
+ endedAt: string;
53
+ }
54
+
55
+ export interface ScreenMemoryCoverageGap {
56
+ startedAt: string | null;
57
+ endedAt: string | null;
58
+ reason:
59
+ | "no-context-files"
60
+ | "no-evidence-in-requested-range"
61
+ | "missing-before-first-evidence"
62
+ | "capture-stale"
63
+ | "timestamps-unavailable"
64
+ | "index-pending"
65
+ | "index-failed"
66
+ | "index-skipped"
67
+ | "privacy-excluded-or-unretained";
68
+ }
69
+
70
+ export interface ScreenMemorySegmentReference {
71
+ id: string;
72
+ startedAt: string | null;
73
+ endedAt: string | null;
74
+ }
75
+
76
+ export interface ScreenMemoryEvidenceItem {
77
+ id: string;
78
+ momentId: string;
79
+ capturedAt: string | null;
80
+ sourceType: ScreenMemoryEvidenceSourceType;
81
+ excerpt: string;
82
+ excerptTruncated: boolean;
83
+ appName: string | null;
84
+ windowTitle: string | null;
85
+ bundleId: string | null;
86
+ url: string | null;
87
+ title: string | null;
88
+ segmentRefs: ScreenMemorySegmentReference[];
89
+ jumpTarget: {
90
+ kind: "screen-memory-moment";
91
+ momentId: string;
92
+ capturedAt: string | null;
93
+ segmentId: string | null;
94
+ };
95
+ }
96
+
97
+ export interface ScreenMemoryRetrievalCoverage {
98
+ requestedRange: ScreenMemoryTimeRange;
99
+ coveredRange: { startedAt: string | null; endedAt: string | null };
100
+ gaps: ScreenMemoryCoverageGap[];
101
+ }
102
+
103
+ export interface ScreenMemoryTruncation {
104
+ itemLimit: number;
105
+ returnedItems: number;
106
+ omittedItems: number;
107
+ maxExcerptChars: number;
108
+ excerptsTruncated: number;
109
+ sourceRowsReadLimit: number;
110
+ sourceRowsReadTruncated: boolean;
111
+ }
112
+
40
113
  export interface ScreenMemoryQueryResult {
41
114
  feature: "screen-memory";
42
115
  localOnly: true;
@@ -46,6 +119,10 @@ export interface ScreenMemoryQueryResult {
46
119
  sinceMinutes: number | null;
47
120
  count: number;
48
121
  items: ScreenMemoryContextItem[];
122
+ /** Stable, bounded local retrieval contract. `items` remains for legacy callers. */
123
+ evidence: ScreenMemoryEvidenceItem[];
124
+ coverage: ScreenMemoryRetrievalCoverage;
125
+ truncation: ScreenMemoryTruncation;
49
126
  contextFiles: string[];
50
127
  note: string;
51
128
  }
@@ -54,15 +131,45 @@ export interface ScreenMemoryLocalOptions {
54
131
  env?: NodeJS.ProcessEnv;
55
132
  platform?: NodeJS.Platform;
56
133
  homeDir?: string;
134
+ /** Test-only clock injection; retrieval never reads network time. */
135
+ now?: () => Date;
136
+ }
137
+
138
+ export interface ScreenMemoryAgentQueryResult extends ScreenMemoryQueryResult {
139
+ egress: {
140
+ requestId: string;
141
+ packet: {
142
+ question: string;
143
+ evidence: Array<{
144
+ id: string;
145
+ momentId: string;
146
+ sourceType: ScreenMemoryEvidenceSourceType;
147
+ capturedAt: string | null;
148
+ excerpt: string;
149
+ }>;
150
+ };
151
+ note: string;
152
+ };
57
153
  }
58
154
 
59
155
  const DEFAULT_CONFIG: ScreenMemoryConfig = {
60
156
  enabled: false,
61
157
  paused: false,
62
- retentionHours: 24,
158
+ retentionHours: 8,
63
159
  maxBytes: 20 * 1024 * 1024 * 1024,
64
160
  segmentSeconds: 5 * 60,
65
161
  sampleIntervalSeconds: 10,
162
+ captureMode: "visuals",
163
+ reviewBeforeSending: true,
164
+ agentClipRetention: "forever",
165
+ excludedBundleIds: [
166
+ "com.1password.1password",
167
+ "com.agilebits.onepassword7",
168
+ "com.bitwarden.desktop",
169
+ "com.dashlane.dashlane",
170
+ "com.lastpass.lastpass",
171
+ ],
172
+ excludePrivateWindows: false,
66
173
  };
67
174
 
68
175
  const JSONL_NAMES = [
@@ -71,6 +178,15 @@ const JSONL_NAMES = [
71
178
  "snapshots.jsonl",
72
179
  "screen-memory.jsonl",
73
180
  ];
181
+ const MAX_SOURCE_ROWS = 10_000;
182
+ const MAX_EXCERPT_CHARS = 1_200;
183
+ const MAX_TRANSCRIPT_JOIN_GAP_MS = 2_000;
184
+ type ScreenMemoryOcrIndexState =
185
+ | "pending"
186
+ | "indexing"
187
+ | "ready"
188
+ | "failed"
189
+ | "skipped";
74
190
 
75
191
  function asRecord(value: unknown): Record<string, unknown> | null {
76
192
  return value && typeof value === "object" && !Array.isArray(value)
@@ -87,14 +203,36 @@ function booleanValue(value: unknown, fallback: boolean): boolean {
87
203
  return typeof value === "boolean" ? value : fallback;
88
204
  }
89
205
 
206
+ function enumValue<const T extends string>(
207
+ value: unknown,
208
+ allowed: readonly T[],
209
+ fallback: T,
210
+ ): T {
211
+ return typeof value === "string" && allowed.includes(value as T)
212
+ ? (value as T)
213
+ : fallback;
214
+ }
215
+
216
+ function bundleIds(value: unknown, fallback: string[]): string[] {
217
+ if (!Array.isArray(value)) return [...fallback];
218
+ return [
219
+ ...new Set(
220
+ value
221
+ .filter((item): item is string => typeof item === "string")
222
+ .map((item) => item.trim().toLowerCase())
223
+ .filter(Boolean),
224
+ ),
225
+ ].sort();
226
+ }
227
+
90
228
  function normalizeConfig(value: unknown): ScreenMemoryConfig {
91
229
  const raw = asRecord(value) ?? {};
92
230
  return {
93
231
  enabled: booleanValue(raw.enabled, DEFAULT_CONFIG.enabled),
94
232
  paused: booleanValue(raw.paused, DEFAULT_CONFIG.paused),
95
- retentionHours: positiveNumber(
96
- raw.retentionHours,
97
- DEFAULT_CONFIG.retentionHours,
233
+ retentionHours: Math.min(
234
+ positiveNumber(raw.retentionHours, DEFAULT_CONFIG.retentionHours),
235
+ 24,
98
236
  ),
99
237
  maxBytes: positiveNumber(raw.maxBytes, DEFAULT_CONFIG.maxBytes),
100
238
  segmentSeconds: positiveNumber(
@@ -105,6 +243,28 @@ function normalizeConfig(value: unknown): ScreenMemoryConfig {
105
243
  raw.sampleIntervalSeconds,
106
244
  DEFAULT_CONFIG.sampleIntervalSeconds,
107
245
  ),
246
+ captureMode: enumValue(
247
+ raw.captureMode,
248
+ ["visuals", "visuals-audio"],
249
+ DEFAULT_CONFIG.captureMode,
250
+ ),
251
+ reviewBeforeSending:
252
+ typeof raw.reviewBeforeSending === "boolean"
253
+ ? raw.reviewBeforeSending
254
+ : DEFAULT_CONFIG.reviewBeforeSending,
255
+ agentClipRetention: enumValue(
256
+ raw.agentClipRetention,
257
+ ["forever", "24-hours", "7-days", "30-days"],
258
+ DEFAULT_CONFIG.agentClipRetention,
259
+ ),
260
+ excludedBundleIds: bundleIds(
261
+ raw.excludedBundleIds,
262
+ DEFAULT_CONFIG.excludedBundleIds,
263
+ ),
264
+ excludePrivateWindows:
265
+ typeof raw.excludePrivateWindows === "boolean"
266
+ ? raw.excludePrivateWindows
267
+ : DEFAULT_CONFIG.excludePrivateWindows,
108
268
  };
109
269
  }
110
270
 
@@ -287,6 +447,22 @@ async function contextFilesFor(
287
447
  // ignore missing candidate files
288
448
  }
289
449
  }
450
+ try {
451
+ const names = await fs.readdir(dir);
452
+ for (const name of names.filter(
453
+ (entry) =>
454
+ entry.endsWith(".ocr.jsonl") || entry.endsWith(".transcript.jsonl"),
455
+ )) {
456
+ const candidate = path.join(dir, name);
457
+ const stat = await fs.stat(candidate);
458
+ if (stat.isFile()) {
459
+ files.push(candidate);
460
+ storageBytes += stat.size;
461
+ }
462
+ }
463
+ } catch {
464
+ // ignore unavailable local directories
465
+ }
290
466
  }
291
467
 
292
468
  return { files, storageBytes };
@@ -303,6 +479,28 @@ function firstString(
303
479
  return null;
304
480
  }
305
481
 
482
+ function firstFiniteNumber(
483
+ raw: Record<string, unknown>,
484
+ keys: string[],
485
+ ): number | null {
486
+ for (const key of keys) {
487
+ const value = raw[key];
488
+ if (typeof value === "number" && Number.isFinite(value)) return value;
489
+ }
490
+ return null;
491
+ }
492
+
493
+ function firstBoolean(
494
+ raw: Record<string, unknown>,
495
+ keys: string[],
496
+ ): boolean | null {
497
+ for (const key of keys) {
498
+ const value = raw[key];
499
+ if (typeof value === "boolean") return value;
500
+ }
501
+ return null;
502
+ }
503
+
306
504
  function normalizeContextItem(
307
505
  value: unknown,
308
506
  sourceFile: string,
@@ -342,9 +540,307 @@ function itemTime(item: ScreenMemoryContextItem): number {
342
540
  return Number.isFinite(parsed) ? parsed : 0;
343
541
  }
344
542
 
345
- async function readItems(files: string[]): Promise<ScreenMemoryContextItem[]> {
543
+ interface LocalContextRow {
544
+ item: ScreenMemoryContextItem;
545
+ raw: Record<string, unknown>;
546
+ }
547
+
548
+ function isPrivacyExcludedRow(
549
+ row: LocalContextRow,
550
+ config: ScreenMemoryConfig,
551
+ ): boolean {
552
+ const bundleId = row.item.bundleId?.trim().toLowerCase();
553
+ if (
554
+ bundleId &&
555
+ config.excludedBundleIds.some(
556
+ (excluded) => excluded.trim().toLowerCase() === bundleId,
557
+ )
558
+ ) {
559
+ return true;
560
+ }
561
+
562
+ if (!config.excludePrivateWindows) return false;
563
+ if (
564
+ firstBoolean(row.raw, [
565
+ "isPrivate",
566
+ "is_private",
567
+ "privateWindow",
568
+ "private_window",
569
+ "incognito",
570
+ ]) === true
571
+ ) {
572
+ return true;
573
+ }
574
+ const title = row.item.windowTitle?.toLowerCase() ?? "";
575
+ return ["incognito", "inprivate", "private browsing", "private window"].some(
576
+ (marker) => title.includes(marker),
577
+ );
578
+ }
579
+
580
+ function rowTimeRange(
581
+ rows: LocalContextRow[],
582
+ fallback: ScreenMemoryTimeRange,
583
+ ): ScreenMemoryTimeRange {
584
+ const times = rows
585
+ .map(({ item }) => itemTime(item))
586
+ .filter((time) => time > 0);
587
+ if (times.length === 0) return fallback;
588
+ return {
589
+ startedAt: new Date(Math.min(...times)).toISOString(),
590
+ endedAt: new Date(Math.max(...times)).toISOString(),
591
+ };
592
+ }
593
+
594
+ interface LocalTranscriptSpan {
595
+ row: LocalContextRow;
596
+ segmentId: string;
597
+ source: string;
598
+ startMs: number;
599
+ endMs: number;
600
+ }
601
+
602
+ interface LocalSegment {
603
+ id: string;
604
+ startedAt: string | null;
605
+ endedAt: string | null;
606
+ clean: boolean;
607
+ }
608
+
609
+ function stableId(prefix: string, value: string): string {
610
+ let hash = 0x811c9dc5;
611
+ for (let index = 0; index < value.length; index += 1) {
612
+ hash ^= value.charCodeAt(index);
613
+ hash = Math.imul(hash, 0x01000193);
614
+ }
615
+ return `${prefix}-${(hash >>> 0).toString(36)}`;
616
+ }
617
+
618
+ function boundedExcerpt(value: string): {
619
+ excerpt: string;
620
+ excerptTruncated: boolean;
621
+ } {
622
+ const normalized = value.replace(/\s+/g, " ").trim();
623
+ if (normalized.length <= MAX_EXCERPT_CHARS) {
624
+ return { excerpt: normalized, excerptTruncated: false };
625
+ }
626
+ return {
627
+ excerpt: `${normalized.slice(0, MAX_EXCERPT_CHARS - 1)}…`,
628
+ excerptTruncated: true,
629
+ };
630
+ }
631
+
632
+ function redactCredentialText(value: string): string {
633
+ return value
634
+ .replace(
635
+ /\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9]{12,}|AKIA[A-Z0-9]{16})\b/g,
636
+ "[REDACTED CREDENTIAL]",
637
+ )
638
+ .replace(/\bBearer\s+[A-Za-z0-9._~+\/-]{8,}/gi, "Bearer [REDACTED]")
639
+ .replace(
640
+ /\b(api[_-]?key|access[_-]?token|password|secret)\s*[:=]\s*([^\s,;]{4,})/gi,
641
+ "$1=[REDACTED]",
642
+ );
643
+ }
644
+
645
+ function sourceTexts(
646
+ raw: Record<string, unknown>,
647
+ fallback: string,
648
+ ): Array<{ sourceType: ScreenMemoryEvidenceSourceType; text: string }> {
649
+ const localTranscriptText =
650
+ firstString(raw, ["text"]) &&
651
+ firstString(raw, ["segmentId", "segment_id"]) &&
652
+ (typeof raw.startMs === "number" || typeof raw.start_ms === "number")
653
+ ? firstString(raw, ["text"])
654
+ : null;
655
+ const values: Array<{
656
+ sourceType: ScreenMemoryEvidenceSourceType;
657
+ keys: string[];
658
+ }> = [
659
+ { sourceType: "transcript", keys: ["transcript", "transcriptText"] },
660
+ { sourceType: "ocr", keys: ["ocrText", "visibleText", "ocr"] },
661
+ ];
662
+ const evidence = values.flatMap(({ sourceType, keys }) => {
663
+ const text = firstString(raw, keys);
664
+ return text ? [{ sourceType, text }] : [];
665
+ });
666
+ if (localTranscriptText) {
667
+ evidence.unshift({
668
+ sourceType: "transcript",
669
+ text: localTranscriptText,
670
+ });
671
+ }
672
+ return evidence.length > 0
673
+ ? evidence
674
+ : fallback
675
+ ? [{ sourceType: "app-context", text: fallback }]
676
+ : [];
677
+ }
678
+
679
+ function segmentReferences(
680
+ raw: Record<string, unknown>,
681
+ capturedAt: string | null,
682
+ segments: LocalSegment[],
683
+ ): ScreenMemorySegmentReference[] {
684
+ const directId = firstString(raw, ["segmentId", "segment_id"]);
685
+ const matched = directId
686
+ ? segments.filter((segment) => segment.id === directId)
687
+ : capturedAt
688
+ ? segments.filter((segment) => {
689
+ const moment = Date.parse(capturedAt);
690
+ const started = segment.startedAt
691
+ ? Date.parse(segment.startedAt)
692
+ : NaN;
693
+ const ended = segment.endedAt ? Date.parse(segment.endedAt) : NaN;
694
+ return moment >= started && moment <= ended;
695
+ })
696
+ : [];
697
+ return matched.slice(0, 3).map((segment) => ({
698
+ id: segment.id,
699
+ startedAt: segment.startedAt,
700
+ endedAt: segment.endedAt,
701
+ }));
702
+ }
703
+
704
+ function normalizeEvidence(
705
+ row: LocalContextRow,
706
+ segments: LocalSegment[],
707
+ ): ScreenMemoryEvidenceItem[] {
708
+ const refs = segmentReferences(row.raw, row.item.capturedAt, segments);
709
+ const base = [
710
+ row.item.sourceFile,
711
+ row.item.capturedAt ?? "unknown-time",
712
+ row.item.appName ?? "",
713
+ row.item.windowTitle ?? "",
714
+ ].join("|");
715
+ const momentId =
716
+ firstString(row.raw, ["momentId", "moment_id", "eventId", "captureId"]) ??
717
+ stableId("moment", base);
718
+ const appContext =
719
+ row.item.text ||
720
+ [row.item.appName, row.item.windowTitle, row.item.title, row.item.url]
721
+ .filter((value): value is string => Boolean(value))
722
+ .join(" — ");
723
+ return sourceTexts(row.raw, appContext).map(({ sourceType, text }) => {
724
+ const { excerpt, excerptTruncated } = boundedExcerpt(text);
725
+ const id =
726
+ firstString(row.raw, ["evidenceId", "evidence_id"]) ??
727
+ stableId("evidence", `${momentId}|${sourceType}|${text}`);
728
+ return {
729
+ id,
730
+ momentId,
731
+ capturedAt: row.item.capturedAt,
732
+ sourceType,
733
+ excerpt,
734
+ excerptTruncated,
735
+ appName: row.item.appName,
736
+ windowTitle: row.item.windowTitle,
737
+ bundleId: row.item.bundleId,
738
+ url: row.item.url,
739
+ title: row.item.title,
740
+ segmentRefs: refs,
741
+ jumpTarget: {
742
+ kind: "screen-memory-moment",
743
+ momentId,
744
+ capturedAt: row.item.capturedAt,
745
+ segmentId: refs[0]?.id ?? null,
746
+ },
747
+ };
748
+ });
749
+ }
750
+
751
+ function transcriptSpan(row: LocalContextRow): LocalTranscriptSpan | null {
752
+ const segmentId = firstString(row.raw, ["segmentId", "segment_id"]);
753
+ const source = firstString(row.raw, ["source", "kind"]);
754
+ const startMs = firstFiniteNumber(row.raw, ["startMs", "start_ms"]);
755
+ const endMs = firstFiniteNumber(row.raw, ["endMs", "end_ms"]);
756
+ if (
757
+ !segmentId ||
758
+ !source ||
759
+ startMs === null ||
760
+ endMs === null ||
761
+ endMs <= startMs ||
762
+ !firstString(row.raw, ["text"])
763
+ ) {
764
+ return null;
765
+ }
766
+ return { row, segmentId, source, startMs, endMs };
767
+ }
768
+
769
+ /**
770
+ * Whisper emits phrase-sized rows, which can split an ordinary sentence in
771
+ * the middle. Join only rows that are demonstrably continuous in the same
772
+ * finalized segment and audio source. Stored sidecars remain untouched, and a
773
+ * real pause, source switch, segment boundary, or excerpt bound starts a new
774
+ * evidence item.
775
+ */
776
+ function coalesceTranscriptRows(rows: LocalContextRow[]): LocalContextRow[] {
777
+ const passthrough: LocalContextRow[] = [];
778
+ const groups = new Map<string, LocalTranscriptSpan[]>();
779
+
780
+ for (const row of rows) {
781
+ const span = transcriptSpan(row);
782
+ if (!span) {
783
+ passthrough.push(row);
784
+ continue;
785
+ }
786
+ const key = [row.item.sourceFile, span.segmentId, span.source].join("\0");
787
+ const group = groups.get(key) ?? [];
788
+ group.push(span);
789
+ groups.set(key, group);
790
+ }
791
+
792
+ const joined = [...groups.values()].flatMap((group) => {
793
+ const ordered = group.sort((a, b) => a.startMs - b.startMs);
794
+ const output: LocalContextRow[] = [];
795
+ let current: LocalTranscriptSpan | null = null;
796
+
797
+ for (const next of ordered) {
798
+ const currentText = current
799
+ ? (firstString(current.row.raw, ["text"]) ?? "")
800
+ : "";
801
+ const nextText = firstString(next.row.raw, ["text"]) ?? "";
802
+ const gapMs = current ? next.startMs - current.endMs : Infinity;
803
+ const combinedText = [currentText, nextText].filter(Boolean).join(" ");
804
+ const canJoin =
805
+ current !== null &&
806
+ gapMs >= 0 &&
807
+ gapMs <= MAX_TRANSCRIPT_JOIN_GAP_MS &&
808
+ combinedText.length <= MAX_EXCERPT_CHARS;
809
+
810
+ if (!current || !canJoin) {
811
+ if (current) output.push(current.row);
812
+ current = {
813
+ ...next,
814
+ row: {
815
+ item: { ...next.row.item },
816
+ raw: { ...next.row.raw },
817
+ },
818
+ };
819
+ continue;
820
+ }
821
+
822
+ current.endMs = next.endMs;
823
+ current.row.item.text = combinedText;
824
+ current.row.raw.text = combinedText;
825
+ if ("end_ms" in current.row.raw) current.row.raw.end_ms = next.endMs;
826
+ else current.row.raw.endMs = next.endMs;
827
+ }
828
+ if (current) output.push(current.row);
829
+ return output;
830
+ });
831
+
832
+ return [...passthrough, ...joined].sort(
833
+ (a, b) => itemTime(b.item) - itemTime(a.item),
834
+ );
835
+ }
836
+
837
+ async function readRows(files: string[]): Promise<{
838
+ rows: LocalContextRow[];
839
+ sourceRowsReadTruncated: boolean;
840
+ }> {
346
841
  const { fs } = await nodeModules();
347
- const items: ScreenMemoryContextItem[] = [];
842
+ const rows: LocalContextRow[] = [];
843
+ let sourceRowsReadTruncated = false;
348
844
 
349
845
  for (const file of files) {
350
846
  let text = "";
@@ -353,18 +849,106 @@ async function readItems(files: string[]): Promise<ScreenMemoryContextItem[]> {
353
849
  } catch {
354
850
  continue;
355
851
  }
356
- const lines = text.split(/\r?\n/).filter(Boolean).slice(-10_000);
852
+ const allLines = text.split(/\r?\n/).filter(Boolean);
853
+ if (allLines.length > MAX_SOURCE_ROWS) sourceRowsReadTruncated = true;
854
+ const lines = allLines.slice(-MAX_SOURCE_ROWS);
357
855
  for (const line of lines) {
358
856
  try {
359
- const item = normalizeContextItem(JSON.parse(line), file);
360
- if (item) items.push(item);
857
+ const raw = asRecord(JSON.parse(line));
858
+ const item = raw ? normalizeContextItem(raw, file) : null;
859
+ if (item && raw) rows.push({ item, raw });
361
860
  } catch {
362
861
  // Keep one malformed row from hiding the rest of the local context.
363
862
  }
364
863
  }
365
864
  }
366
865
 
367
- return items.sort((a, b) => itemTime(b) - itemTime(a));
866
+ return {
867
+ rows: coalesceTranscriptRows(rows),
868
+ sourceRowsReadTruncated,
869
+ };
870
+ }
871
+
872
+ async function readItems(files: string[]): Promise<ScreenMemoryContextItem[]> {
873
+ const { rows } = await readRows(files);
874
+ return rows.map(({ item }) => item);
875
+ }
876
+
877
+ async function readSegments(dataDirs: string[]): Promise<LocalSegment[]> {
878
+ const { fs, path } = await nodeModules();
879
+ const segments: LocalSegment[] = [];
880
+ for (const dir of dataDirs) {
881
+ let names: string[] = [];
882
+ try {
883
+ names = await fs.readdir(dir);
884
+ } catch {
885
+ continue;
886
+ }
887
+ for (const name of names.filter((candidate) =>
888
+ candidate.endsWith(".json"),
889
+ )) {
890
+ const raw = asRecord(await readJson(path.join(dir, name)));
891
+ const id = raw && firstString(raw, ["id"]);
892
+ if (!raw || !id) continue;
893
+ const mediaPath = firstString(raw, ["path"]);
894
+ let retained = true;
895
+ if (mediaPath) {
896
+ try {
897
+ await fs.access(mediaPath);
898
+ } catch {
899
+ retained = false;
900
+ }
901
+ }
902
+ segments.push({
903
+ id,
904
+ startedAt: firstString(raw, ["startedAt"]),
905
+ endedAt: firstString(raw, ["endedAt"]),
906
+ clean:
907
+ retained &&
908
+ raw.exclusionTainted !== true &&
909
+ raw.corrupt !== true &&
910
+ !firstString(raw, ["error"]),
911
+ });
912
+ }
913
+ }
914
+ return segments;
915
+ }
916
+
917
+ async function readOcrIndexStates(
918
+ dataDirs: string[],
919
+ ): Promise<Array<{ segmentId: string; state: ScreenMemoryOcrIndexState }>> {
920
+ const { fs, path } = await nodeModules();
921
+ const states: Array<{ segmentId: string; state: ScreenMemoryOcrIndexState }> =
922
+ [];
923
+ for (const dir of dataDirs) {
924
+ let names: string[] = [];
925
+ try {
926
+ names = await fs.readdir(dir);
927
+ } catch {
928
+ continue;
929
+ }
930
+ for (const name of names.filter(
931
+ (entry) =>
932
+ entry.endsWith(".ocr-status.json") ||
933
+ entry.endsWith(".transcript-status.json"),
934
+ )) {
935
+ const raw = asRecord(await readJson(path.join(dir, name)));
936
+ const rawState = String(raw?.state);
937
+ const state = rawState === "transcribing" ? "indexing" : rawState;
938
+ const suffix = name.endsWith(".ocr-status.json")
939
+ ? ".ocr-status.json"
940
+ : ".transcript-status.json";
941
+ const segmentId = name.slice(0, -suffix.length);
942
+ if (
943
+ ["pending", "indexing", "ready", "failed", "skipped"].includes(
944
+ String(state),
945
+ )
946
+ ) {
947
+ states.push({ segmentId, state: state as ScreenMemoryOcrIndexState });
948
+ }
949
+ }
950
+ }
951
+ return states;
368
952
  }
369
953
 
370
954
  export async function queryScreenMemoryContext(
@@ -384,18 +968,130 @@ export async function queryScreenMemoryContext(
384
968
  typeof args.sinceMinutes === "number" && Number.isFinite(args.sinceMinutes)
385
969
  ? Math.max(args.sinceMinutes, 0)
386
970
  : null;
971
+ const now = options.now?.() ?? new Date();
387
972
  const cutoff =
388
- sinceMinutes === null ? null : Date.now() - sinceMinutes * 60 * 1000;
973
+ sinceMinutes === null ? null : now.getTime() - sinceMinutes * 60 * 1000;
389
974
 
390
975
  const needle = query?.toLowerCase() ?? null;
391
- const items = (await readItems(files)).filter((item) => {
976
+ const source = await readRows(files);
977
+ const rangeRows = source.rows.filter((row) => {
978
+ const { item } = row;
392
979
  if (cutoff !== null) {
393
980
  const time = itemTime(item);
394
981
  if (!time || time < cutoff) return false;
395
982
  }
983
+ return true;
984
+ });
985
+ const privacyExcludedRows = rangeRows.filter((row) =>
986
+ isPrivacyExcludedRow(row, info.config),
987
+ );
988
+ const candidateRows = rangeRows.filter((row) => {
989
+ if (isPrivacyExcludedRow(row, info.config)) return false;
396
990
  if (!needle) return true;
397
- return JSON.stringify(item).toLowerCase().includes(needle);
991
+ const { item } = row;
992
+ return JSON.stringify({ item, raw: row.raw })
993
+ .toLowerCase()
994
+ .includes(needle);
398
995
  });
996
+ const segments = await readSegments(paths.dataDirs);
997
+ const cleanSegments = segments.filter((segment) => segment.clean);
998
+ // Modern stores bind every evidence row to retained segment metadata. Once
999
+ // segment metadata exists, refuse rows that only point at tainted, corrupt,
1000
+ // or pruned media. Legacy context-only stores (no segment metadata at all)
1001
+ // remain readable for backwards compatibility.
1002
+ const rows =
1003
+ segments.length === 0
1004
+ ? candidateRows
1005
+ : candidateRows.filter(
1006
+ (row) =>
1007
+ segmentReferences(row.raw, row.item.capturedAt, cleanSegments)
1008
+ .length > 0,
1009
+ );
1010
+ const items = rows.map(({ item }) => item);
1011
+ const ocrIndexStates = await readOcrIndexStates(paths.dataDirs);
1012
+ const evidence = rows.flatMap((row) => normalizeEvidence(row, cleanSegments));
1013
+ const returnedEvidence = evidence.slice(0, limit);
1014
+ const evidenceTimes = evidence
1015
+ .map((item) => (item.capturedAt ? Date.parse(item.capturedAt) : NaN))
1016
+ .filter(Number.isFinite);
1017
+ const coveredRange = evidenceTimes.length
1018
+ ? {
1019
+ startedAt: new Date(Math.min(...evidenceTimes)).toISOString(),
1020
+ endedAt: new Date(Math.max(...evidenceTimes)).toISOString(),
1021
+ }
1022
+ : { startedAt: null, endedAt: null };
1023
+ const requestedRange = {
1024
+ startedAt: cutoff === null ? null : new Date(cutoff).toISOString(),
1025
+ endedAt: now.toISOString(),
1026
+ };
1027
+ const gaps: ScreenMemoryCoverageGap[] = [];
1028
+ if (privacyExcludedRows.length > 0) {
1029
+ gaps.push({
1030
+ ...rowTimeRange(privacyExcludedRows, requestedRange),
1031
+ reason: "privacy-excluded-or-unretained",
1032
+ });
1033
+ }
1034
+ if (candidateRows.length > rows.length) {
1035
+ gaps.push({
1036
+ ...requestedRange,
1037
+ reason: "privacy-excluded-or-unretained",
1038
+ });
1039
+ }
1040
+ for (const index of ocrIndexStates) {
1041
+ if (index.state === "ready") continue;
1042
+ const segment = segments.find(
1043
+ (candidate) => candidate.id === index.segmentId,
1044
+ );
1045
+ const reason =
1046
+ index.state === "failed"
1047
+ ? "index-failed"
1048
+ : index.state === "skipped"
1049
+ ? "index-skipped"
1050
+ : "index-pending";
1051
+ gaps.push({
1052
+ startedAt: segment?.startedAt ?? null,
1053
+ endedAt: segment?.endedAt ?? null,
1054
+ reason,
1055
+ });
1056
+ }
1057
+ if (files.length === 0) {
1058
+ gaps.push({ ...requestedRange, reason: "no-context-files" });
1059
+ } else if (evidence.length === 0) {
1060
+ gaps.push({ ...requestedRange, reason: "no-evidence-in-requested-range" });
1061
+ } else {
1062
+ if (evidence.some((item) => !item.capturedAt)) {
1063
+ gaps.push({
1064
+ startedAt: null,
1065
+ endedAt: null,
1066
+ reason: "timestamps-unavailable",
1067
+ });
1068
+ }
1069
+ if (
1070
+ requestedRange.startedAt &&
1071
+ coveredRange.startedAt &&
1072
+ coveredRange.startedAt > requestedRange.startedAt
1073
+ ) {
1074
+ gaps.push({
1075
+ startedAt: requestedRange.startedAt,
1076
+ endedAt: coveredRange.startedAt,
1077
+ reason: "missing-before-first-evidence",
1078
+ });
1079
+ }
1080
+ const staleAfterMs = Math.max(
1081
+ info.config.sampleIntervalSeconds * 3 * 1000,
1082
+ 2 * 60 * 1000,
1083
+ );
1084
+ if (
1085
+ coveredRange.endedAt &&
1086
+ Date.parse(coveredRange.endedAt) < now.getTime() - staleAfterMs
1087
+ ) {
1088
+ gaps.push({
1089
+ startedAt: coveredRange.endedAt,
1090
+ endedAt: requestedRange.endedAt,
1091
+ reason: "capture-stale",
1092
+ });
1093
+ }
1094
+ }
399
1095
 
400
1096
  return {
401
1097
  feature: "screen-memory",
@@ -406,6 +1102,19 @@ export async function queryScreenMemoryContext(
406
1102
  sinceMinutes,
407
1103
  count: items.length,
408
1104
  items: items.slice(0, limit),
1105
+ evidence: returnedEvidence,
1106
+ coverage: { requestedRange, coveredRange, gaps },
1107
+ truncation: {
1108
+ itemLimit: limit,
1109
+ returnedItems: returnedEvidence.length,
1110
+ omittedItems: Math.max(evidence.length - returnedEvidence.length, 0),
1111
+ maxExcerptChars: MAX_EXCERPT_CHARS,
1112
+ excerptsTruncated: returnedEvidence.filter(
1113
+ (item) => item.excerptTruncated,
1114
+ ).length,
1115
+ sourceRowsReadLimit: MAX_SOURCE_ROWS,
1116
+ sourceRowsReadTruncated: source.sourceRowsReadTruncated,
1117
+ },
409
1118
  contextFiles: files,
410
1119
  note:
411
1120
  files.length === 0
@@ -414,6 +1123,131 @@ export async function queryScreenMemoryContext(
414
1123
  };
415
1124
  }
416
1125
 
1126
+ /**
1127
+ * Agent-facing retrieval boundary. Asking an agent to search Rewind is the
1128
+ * authorization. This removes filesystem paths, redacts obvious
1129
+ * credential-shaped text, and records a content-free activity receipt before
1130
+ * it is returned to an action caller.
1131
+ */
1132
+ export async function queryScreenMemoryForAgent(
1133
+ args: {
1134
+ query?: string | null;
1135
+ limit?: number | null;
1136
+ sinceMinutes?: number | null;
1137
+ } = {},
1138
+ options: ScreenMemoryLocalOptions = {},
1139
+ ): Promise<ScreenMemoryAgentQueryResult> {
1140
+ const result = await queryScreenMemoryContext(args, options);
1141
+ const items = result.items.map((item) => ({
1142
+ ...item,
1143
+ text: redactCredentialText(item.text),
1144
+ sourceFile: "local-screen-memory",
1145
+ }));
1146
+ const evidence = result.evidence.map((item) => ({
1147
+ ...item,
1148
+ excerpt: redactCredentialText(item.excerpt),
1149
+ }));
1150
+ const packet = {
1151
+ question: redactCredentialText(
1152
+ args.query?.trim().slice(0, 4_000) || "Recent Screen Memory context",
1153
+ ),
1154
+ evidence: evidence.slice(0, 20).map((item) => ({
1155
+ id: item.id,
1156
+ momentId: item.momentId,
1157
+ sourceType: item.sourceType,
1158
+ capturedAt: item.capturedAt,
1159
+ excerpt: item.excerpt.slice(0, MAX_EXCERPT_CHARS),
1160
+ })),
1161
+ };
1162
+ const paths = await resolvePaths(options);
1163
+ const { fs, path } = await nodeModules();
1164
+ const storeDir = paths.dataDirs[0];
1165
+ await fs.mkdir(storeDir, { recursive: true, mode: 0o700 });
1166
+ await fs.chmod(storeDir, 0o700);
1167
+ const logPath = path.join(storeDir, "egress.jsonl");
1168
+ if (await exists(logPath)) {
1169
+ const sanitized = (await fs.readFile(logPath, "utf8"))
1170
+ .split("\n")
1171
+ .filter(Boolean)
1172
+ .flatMap((line) => {
1173
+ try {
1174
+ const event = JSON.parse(line) as Record<string, unknown>;
1175
+ const packet = event.packet as
1176
+ | { evidence?: Array<Record<string, unknown>> }
1177
+ | null
1178
+ | undefined;
1179
+ if (!event.receipt && Array.isArray(packet?.evidence)) {
1180
+ event.receipt = {
1181
+ evidence: packet.evidence.map((item) => ({
1182
+ id: item.id,
1183
+ momentId: item.momentId,
1184
+ sourceType: item.sourceType,
1185
+ capturedAt: item.capturedAt ?? null,
1186
+ })),
1187
+ };
1188
+ }
1189
+ delete event.packet;
1190
+ delete event.reason;
1191
+ event.packetBytes =
1192
+ typeof event.packetBytes === "number" ? event.packetBytes : 0;
1193
+ return [JSON.stringify(event)];
1194
+ } catch {
1195
+ return [];
1196
+ }
1197
+ });
1198
+ const temporaryPath = `${logPath}.sanitize-${process.pid}`;
1199
+ await fs.writeFile(
1200
+ temporaryPath,
1201
+ sanitized.length > 0 ? `${sanitized.join("\n")}\n` : "",
1202
+ { encoding: "utf8", mode: 0o600 },
1203
+ );
1204
+ await fs.rename(temporaryPath, logPath);
1205
+ }
1206
+ const requestId = `egress-action-${Date.now()}-${process.pid}`;
1207
+ const prepared = {
1208
+ requestId,
1209
+ occurredAt: new Date().toISOString(),
1210
+ state: "prepared",
1211
+ operation: "agent-query",
1212
+ receipt: {
1213
+ evidence: packet.evidence.map(
1214
+ ({ id, momentId, sourceType, capturedAt }) => ({
1215
+ id,
1216
+ momentId,
1217
+ sourceType,
1218
+ capturedAt,
1219
+ }),
1220
+ ),
1221
+ },
1222
+ evidenceCount: packet.evidence.length,
1223
+ packetBytes: 0,
1224
+ error: null,
1225
+ };
1226
+ const completed = {
1227
+ ...prepared,
1228
+ occurredAt: new Date().toISOString(),
1229
+ state: "completed",
1230
+ receipt: null,
1231
+ };
1232
+ await fs.appendFile(
1233
+ logPath,
1234
+ `${JSON.stringify(prepared)}\n${JSON.stringify(completed)}\n`,
1235
+ { encoding: "utf8", mode: 0o600 },
1236
+ );
1237
+ await fs.chmod(logPath, 0o600);
1238
+ return {
1239
+ ...result,
1240
+ items,
1241
+ evidence,
1242
+ contextFiles: [],
1243
+ egress: {
1244
+ requestId,
1245
+ packet,
1246
+ note: "A content-free local activity receipt was recorded before this bounded text packet was returned.",
1247
+ },
1248
+ };
1249
+ }
1250
+
417
1251
  export async function readScreenMemoryStatus(
418
1252
  options: ScreenMemoryLocalOptions = {},
419
1253
  ): Promise<ScreenMemoryStatus> {