@agent-native/core 0.114.6 → 0.114.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
- package/corpus/core/src/cli/mcp.ts +124 -0
- package/corpus/core/src/cli/skills-content/help.ts +4 -3
- package/corpus/core/src/cli/skills-content/index.ts +1 -0
- package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
- package/corpus/core/src/cli/skills.ts +115 -13
- package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
- package/corpus/core/src/mcp-client/index.ts +9 -0
- package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
- package/corpus/core/src/server/agent-chat-plugin.ts +16 -9
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +19 -7
- package/corpus/templates/analytics/app/hooks/use-chart-tooltip-portal.ts +60 -0
- package/corpus/templates/analytics/app/routes/chart.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-07-20-dashboard-requests-no-longer-stall-without-progress.md +6 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +6 -0
- package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
- package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
- package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
- package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
- package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
- package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
- package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
- package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
- package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
- package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
- package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
- package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
- package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
- package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
- package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
- package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
- package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
- package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
- package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
- package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
- package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
- package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
- package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
- package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
- package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
- package/corpus/templates/clips/desktop/src/main.tsx +3 -0
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
- package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
- package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
- package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
- package/corpus/templates/clips/desktop/src/styles.css +956 -134
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
- package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
- package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
- package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
- package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
- package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
- package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
- package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
- package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
- package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
- package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/dist/cli/mcp-config-writers.d.ts +2 -2
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +6 -6
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -1
- package/dist/cli/mcp.js +79 -0
- package/dist/cli/mcp.js.map +1 -1
- package/dist/cli/skills-content/help.d.ts +1 -1
- package/dist/cli/skills-content/help.d.ts.map +1 -1
- package/dist/cli/skills-content/help.js +4 -3
- package/dist/cli/skills-content/help.js.map +1 -1
- package/dist/cli/skills-content/index.d.ts +1 -0
- package/dist/cli/skills-content/index.d.ts.map +1 -1
- package/dist/cli/skills-content/index.js +1 -0
- package/dist/cli/skills-content/index.js.map +1 -1
- package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
- package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
- package/dist/cli/skills-content/rewind-skill.js +53 -0
- package/dist/cli/skills-content/rewind-skill.js.map +1 -0
- package/dist/cli/skills.d.ts +6 -0
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +90 -14
- package/dist/cli/skills.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +365 -0
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp/screen-memory-stdio.js +1014 -91
- package/dist/mcp/screen-memory-stdio.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/screen-memory-local.d.ts +90 -0
- package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
- package/dist/mcp-client/screen-memory-local.js +590 -12
- package/dist/mcp-client/screen-memory-local.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-chat-plugin.d.ts +5 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +10 -9
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/mcp-config-writers.ts +6 -4
- package/src/cli/mcp.ts +124 -0
- package/src/cli/skills-content/help.ts +4 -3
- package/src/cli/skills-content/index.ts +1 -0
- package/src/cli/skills-content/rewind-skill.ts +52 -0
- package/src/cli/skills.ts +115 -13
- package/src/mcp/screen-memory-stdio.ts +1360 -104
- package/src/mcp-client/index.ts +9 -0
- package/src/mcp-client/screen-memory-local.ts +847 -13
- package/src/server/agent-chat-plugin.ts +16 -9
|
@@ -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:
|
|
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:
|
|
96
|
-
raw.retentionHours,
|
|
97
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
360
|
-
|
|
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
|
|
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 :
|
|
973
|
+
sinceMinutes === null ? null : now.getTime() - sinceMinutes * 60 * 1000;
|
|
389
974
|
|
|
390
975
|
const needle = query?.toLowerCase() ?? null;
|
|
391
|
-
const
|
|
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
|
-
|
|
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> {
|