throughline 0.3.24 → 0.4.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.
- package/.claude/commands/tl.md +6 -21
- package/.codex-sidecar.yml +62 -0
- package/CHANGELOG.md +632 -0
- package/README.ja.md +71 -46
- package/README.md +420 -76
- package/bin/throughline.mjs +169 -7
- package/codex/skills/throughline/SKILL.md +157 -0
- package/codex/skills/throughline/agents/openai.yaml +7 -0
- package/docs/INHERITANCE_ON_CLEAR_ONLY.md +159 -0
- package/docs/L1_L2_L3_REDESIGN.md +415 -0
- package/docs/PUBLIC_RELEASE_PLAN.md +185 -0
- package/docs/THROUGHLINE_CLEAR_AUTO_HANDOFF_PLAN.md +286 -0
- package/docs/THROUGHLINE_CODEX_DUAL_SUPPORT.md +249 -0
- package/docs/THROUGHLINE_CODEX_FIRST_ROADMAP.md +555 -0
- package/docs/THROUGHLINE_CODEX_MONITOR_IMPLEMENTATION_PLAN.md +220 -0
- package/docs/THROUGHLINE_CODEX_TRIM_IMPLEMENTATION_PLAN.md +528 -0
- package/docs/THROUGHLINE_CODEX_TRIM_ROLLBACK_FIX_PLAN.md +672 -0
- package/docs/archive/CONCEPT.md +476 -0
- package/docs/archive/EXPERIMENT.md +371 -0
- package/docs/archive/README.md +22 -0
- package/docs/archive/SESSION_LINKING_DESIGN.md +231 -0
- package/docs/archive/THROUGHLINE_NEXT_STEPS.md +134 -0
- package/docs/throughline-codex-trim-rollback-incident-report.md +306 -0
- package/docs/throughline-handoff-context.example.json +57 -0
- package/docs/throughline-rollback-context-trim-insight.md +455 -0
- package/package.json +6 -2
- package/src/baton.mjs +17 -45
- package/src/baton.test.mjs +4 -41
- package/src/cli/codex-capture.mjs +95 -0
- package/src/cli/codex-handoff-model-smoke.mjs +292 -0
- package/src/cli/codex-handoff-model-smoke.test.mjs +262 -0
- package/src/cli/codex-handoff-smoke.mjs +163 -0
- package/src/cli/codex-handoff-smoke.test.mjs +149 -0
- package/src/cli/codex-handoff-start.mjs +291 -0
- package/src/cli/codex-handoff-start.test.mjs +194 -0
- package/src/cli/codex-hook.mjs +276 -0
- package/src/cli/codex-hook.test.mjs +293 -0
- package/src/cli/codex-host-primitive-audit.mjs +110 -0
- package/src/cli/codex-host-primitive-audit.test.mjs +75 -0
- package/src/cli/codex-restore-smoke.mjs +357 -0
- package/src/cli/codex-restore-source-audit.mjs +304 -0
- package/src/cli/codex-resume.mjs +138 -0
- package/src/cli/codex-rollback-model-visible-smoke.mjs +373 -0
- package/src/cli/codex-rollback-model-visible-smoke.test.mjs +255 -0
- package/src/cli/codex-sidecar-diagnostics.mjs +48 -0
- package/src/cli/codex-sidecar-dry-run.mjs +85 -0
- package/src/cli/codex-summarize.mjs +224 -0
- package/src/cli/codex-threads.mjs +89 -0
- package/src/cli/codex-visibility-smoke.mjs +196 -0
- package/src/cli/codex-vscode-restore-smoke.mjs +226 -0
- package/src/cli/codex-vscode-rollback-smoke.mjs +114 -0
- package/src/cli/doctor.mjs +503 -1
- package/src/cli/doctor.test.mjs +542 -3
- package/src/cli/handoff-preview.mjs +78 -0
- package/src/cli/help.test.mjs +64 -0
- package/src/cli/install.mjs +226 -3
- package/src/cli/install.test.mjs +205 -4
- package/src/cli/trim.mjs +564 -0
- package/src/codex-app-server.mjs +1816 -0
- package/src/codex-app-server.test.mjs +512 -0
- package/src/codex-auto-refresh.mjs +194 -0
- package/src/codex-auto-refresh.test.mjs +182 -0
- package/src/codex-capture.mjs +235 -0
- package/src/codex-capture.test.mjs +393 -0
- package/src/codex-handoff-model-smoke.mjs +114 -0
- package/src/codex-handoff-model-smoke.test.mjs +89 -0
- package/src/codex-handoff-smoke.mjs +124 -0
- package/src/codex-handoff-smoke.test.mjs +103 -0
- package/src/codex-handoff.mjs +331 -0
- package/src/codex-handoff.test.mjs +220 -0
- package/src/codex-host-primitive-audit.mjs +374 -0
- package/src/codex-host-primitive-audit.test.mjs +208 -0
- package/src/codex-restore-smoke.test.mjs +639 -0
- package/src/codex-restore-source-audit.mjs +1348 -0
- package/src/codex-restore-source-audit.test.mjs +623 -0
- package/src/codex-resume.test.mjs +242 -0
- package/src/codex-rollout-memory.mjs +711 -0
- package/src/codex-rollout-memory.test.mjs +610 -0
- package/src/codex-sidecar-cli.test.mjs +75 -0
- package/src/codex-sidecar.mjs +246 -0
- package/src/codex-sidecar.test.mjs +172 -0
- package/src/codex-summarize.test.mjs +143 -0
- package/src/codex-thread-identity.mjs +23 -0
- package/src/codex-thread-index.mjs +173 -0
- package/src/codex-thread-index.test.mjs +164 -0
- package/src/codex-usage.mjs +110 -0
- package/src/codex-usage.test.mjs +140 -0
- package/src/codex-visibility-smoke.test.mjs +222 -0
- package/src/codex-vscode-restore-smoke.mjs +206 -0
- package/src/codex-vscode-restore-smoke.test.mjs +325 -0
- package/src/codex-vscode-rollback-smoke.mjs +90 -0
- package/src/codex-vscode-rollback-smoke.test.mjs +290 -0
- package/src/db-schema.test.mjs +96 -0
- package/src/db.mjs +14 -1
- package/src/haiku-summarizer.mjs +267 -26
- package/src/haiku-summarizer.test.mjs +282 -0
- package/src/handoff-preview.test.mjs +108 -0
- package/src/handoff-record.mjs +294 -0
- package/src/handoff-record.test.mjs +226 -0
- package/src/hook-entrypoints.test.mjs +286 -0
- package/src/package-files.test.mjs +19 -0
- package/src/prompt-submit.mjs +9 -6
- package/src/resume-context.mjs +58 -171
- package/src/resume-context.test.mjs +177 -0
- package/src/session-start.mjs +85 -26
- package/src/state-file.mjs +50 -6
- package/src/state-file.test.mjs +50 -0
- package/src/token-monitor.mjs +14 -10
- package/src/token-monitor.test.mjs +27 -0
- package/src/trim-cli.test.mjs +1584 -0
- package/src/trim-model.mjs +584 -0
- package/src/trim-model.test.mjs +568 -0
- package/src/turn-processor.mjs +17 -10
- package/src/vscode-task.mjs +33 -10
- package/src/vscode-task.test.mjs +19 -9
- package/src/cli/save-inflight.mjs +0 -81
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, delimiter, extname, join } from 'node:path';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
|
|
6
|
+
import { buildCodexRolloutTrimSource } from './codex-rollout-memory.mjs';
|
|
7
|
+
import { defaultCodexHome } from './codex-thread-index.mjs';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MAX_STORAGE_FILES = 5000;
|
|
10
|
+
const DEFAULT_MAX_STORAGE_FILE_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
const DEFAULT_MAX_STORAGE_MATCHES = 50;
|
|
12
|
+
const DEFAULT_MAX_EXTENSION_FILES = 5000;
|
|
13
|
+
const DEFAULT_MAX_EXTENSION_FILE_BYTES = 2 * 1024 * 1024;
|
|
14
|
+
const DEFAULT_MAX_EXTENSION_MATCHES = 100;
|
|
15
|
+
const DEFAULT_MAX_EXTENSION_SOURCE_SNIPPETS = 40;
|
|
16
|
+
const DEFAULT_EXTENSION_SOURCE_SNIPPET_CHARS = 240;
|
|
17
|
+
const DEFAULT_MAX_SETTINGS_FILES = 100;
|
|
18
|
+
const DEFAULT_MAX_SETTINGS_FILE_BYTES = 256 * 1024;
|
|
19
|
+
const DEFAULT_MAX_SETTINGS_MATCHES = 20;
|
|
20
|
+
const DEFAULT_MAX_LOG_FILES = 2000;
|
|
21
|
+
const DEFAULT_MAX_LOG_FILE_BYTES = 2 * 1024 * 1024;
|
|
22
|
+
const DEFAULT_MAX_LOG_MATCHES = 50;
|
|
23
|
+
const MIN_RESTORE_TEXT_NEEDLE_CHARS = 20;
|
|
24
|
+
|
|
25
|
+
const VSCODE_EXTENSION_RESTORE_PATTERNS = Object.freeze([
|
|
26
|
+
{ id: 'thread_read', label: 'Codex app-server thread/read', pattern: 'thread/read' },
|
|
27
|
+
{ id: 'thread_resume', label: 'Codex app-server thread/resume', pattern: 'thread/resume' },
|
|
28
|
+
{ id: 'thread_turns_list', label: 'Codex app-server thread/turns/list', pattern: 'thread/turns/list' },
|
|
29
|
+
{ id: 'thread_compact_start', label: 'Codex app-server thread/compact/start', pattern: 'thread/compact/start' },
|
|
30
|
+
{ id: 'thread_rollback', label: 'Codex app-server thread/rollback', pattern: 'thread/rollback' },
|
|
31
|
+
{ id: 'mark_need_resume_after_reconnect', label: 'VS Code reconnect marks conversations needing resume', pattern: 'markAllConversationsNeedResumeAfterReconnect' },
|
|
32
|
+
{ id: 'needs_resume', label: 'VS Code needs_resume state', pattern: 'needs_resume' },
|
|
33
|
+
{ id: 'persisted_atom', label: 'Codex webview persisted atom prefix', pattern: 'codex:persisted-atom:' },
|
|
34
|
+
{ id: 'follow_up_queue_setting', label: 'Codex follow-up queue setting', pattern: 'chatgpt.followUpQueueMode' },
|
|
35
|
+
{ id: 'send_follow_up_message', label: 'Codex send follow-up message action', pattern: 'send-follow-up-message' },
|
|
36
|
+
{ id: 'steering_user_message', label: 'Codex steering user message item', pattern: 'steeringUserMessage' },
|
|
37
|
+
{ id: 'compacted_replacement_history', label: 'Codex compacted replacement_history text', pattern: 'replacement_history' },
|
|
38
|
+
{
|
|
39
|
+
id: 'patch_apply_failure_log',
|
|
40
|
+
label: 'VS Code patch apply failure log source',
|
|
41
|
+
pattern: 'Failed to apply patches for',
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const VSCODE_EXTENSION_SOURCE_FACT_PATTERNS = Object.freeze([
|
|
46
|
+
{
|
|
47
|
+
id: 'thread_resume_uses_null_history',
|
|
48
|
+
label: 'thread/resume request passes history:null',
|
|
49
|
+
requiredPatterns: ['thread/resume', 'history:null'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'thread_resume_uses_rollout_path',
|
|
53
|
+
label: 'thread/resume request can pass rolloutPath as path',
|
|
54
|
+
requiredPatterns: ['thread/resume', 'rolloutPath'],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'reconnect_command_marks_threads_need_resume',
|
|
58
|
+
label: 'reconnect command marks conversations as needing resume',
|
|
59
|
+
requiredPatterns: [
|
|
60
|
+
'mark-all-conversations-need-resume-after-reconnect-for-host',
|
|
61
|
+
'markAllConversationsNeedResumeAfterReconnect',
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'steering_user_message_has_restore_message',
|
|
66
|
+
label: 'steeringUserMessage carries restoreMessage',
|
|
67
|
+
requiredPatterns: ['steeringUserMessage', 'restoreMessage'],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'owner_broadcasts_thread_state_patches',
|
|
71
|
+
label: 'owner broadcasts conversation state patches over thread-stream-state-changed',
|
|
72
|
+
requiredPatterns: ['broadcastIpcStatePatches', 'thread-stream-state-changed', 'patches:t'],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'follower_applies_thread_state_patches',
|
|
76
|
+
label: 'follower applies thread-stream-state-changed patches to conversation state',
|
|
77
|
+
requiredPatterns: ['handleThreadStreamStateChanged', 'sn(n,t.patches)'],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'patch_apply_failure_logged_in_thread_stream_handler',
|
|
81
|
+
label: 'thread-stream-state-changed patch apply failure is logged',
|
|
82
|
+
requiredPatterns: ['handleThreadStreamStateChanged', 'Failed to apply patches for'],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'replacement_history_filter_candidate',
|
|
86
|
+
label: 'replacement_history appears with a filter candidate',
|
|
87
|
+
nearPatternSets: [{ patterns: ['replacement_history', 'filter'] }],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'replacement_history_tombstone_candidate',
|
|
91
|
+
label: 'replacement_history appears with a tombstone candidate',
|
|
92
|
+
nearPatternSets: [{ patterns: ['replacement_history', 'tombstone'] }],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'restore_message_suppression_candidate',
|
|
96
|
+
label: 'restoreMessage appears with a suppression candidate',
|
|
97
|
+
nearPatternSets: [{ patterns: ['restoreMessage', 'suppress'] }],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'restore_message_exclusion_candidate',
|
|
101
|
+
label: 'restoreMessage appears with an exclusion candidate',
|
|
102
|
+
nearPatternSets: [{ patterns: ['restoreMessage', 'exclude'] }],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'restore_message_projection_candidate',
|
|
106
|
+
label: 'restoreMessage appears with a projection candidate',
|
|
107
|
+
nearPatternSets: [{ patterns: ['restoreMessage', 'projection'] }],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'rolled_back_tombstone_candidate',
|
|
111
|
+
label: 'thread_rolled_back appears with a tombstone candidate',
|
|
112
|
+
nearPatternSets: [{ patterns: ['thread_rolled_back', 'tombstone'] }],
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const VSCODE_ROLLBACK_NON_RESURRECTION_SOURCE_FACT_IDS = Object.freeze([
|
|
117
|
+
'replacement_history_filter_candidate',
|
|
118
|
+
'replacement_history_tombstone_candidate',
|
|
119
|
+
'restore_message_suppression_candidate',
|
|
120
|
+
'restore_message_exclusion_candidate',
|
|
121
|
+
'restore_message_projection_candidate',
|
|
122
|
+
'rolled_back_tombstone_candidate',
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const VSCODE_LOG_SIGNAL_PATTERNS = Object.freeze([
|
|
126
|
+
{
|
|
127
|
+
id: 'patch_apply_failure',
|
|
128
|
+
label: 'VS Code failed to apply conversation patches for thread',
|
|
129
|
+
buildPattern: (threadId) => `Failed to apply patches for conversationId=${threadId}`,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'thread_stream_state_broadcast',
|
|
133
|
+
label: 'VS Code thread stream state broadcast',
|
|
134
|
+
pattern: 'thread-stream-state-changed',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'replacement_history',
|
|
138
|
+
label: 'Codex compacted replacement_history text in logs',
|
|
139
|
+
pattern: 'replacement_history',
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
export function runCodexRestoreSourceAudit({
|
|
144
|
+
threadId,
|
|
145
|
+
codexHome = defaultCodexHome(),
|
|
146
|
+
projectPath = process.cwd(),
|
|
147
|
+
vscodeStorageRoots = defaultVsCodeStorageRoots(),
|
|
148
|
+
vscodeExtensionRoots = defaultVsCodeExtensionRoots(),
|
|
149
|
+
vscodeSettingsRoots = defaultVsCodeSettingsRoots(),
|
|
150
|
+
vscodeLogRoots = defaultVsCodeLogRoots(),
|
|
151
|
+
maxStorageFiles = DEFAULT_MAX_STORAGE_FILES,
|
|
152
|
+
maxStorageFileBytes = DEFAULT_MAX_STORAGE_FILE_BYTES,
|
|
153
|
+
maxStorageMatches = DEFAULT_MAX_STORAGE_MATCHES,
|
|
154
|
+
maxExtensionFiles = DEFAULT_MAX_EXTENSION_FILES,
|
|
155
|
+
maxExtensionFileBytes = DEFAULT_MAX_EXTENSION_FILE_BYTES,
|
|
156
|
+
maxExtensionMatches = DEFAULT_MAX_EXTENSION_MATCHES,
|
|
157
|
+
maxExtensionSourceSnippets = DEFAULT_MAX_EXTENSION_SOURCE_SNIPPETS,
|
|
158
|
+
maxSettingsFiles = DEFAULT_MAX_SETTINGS_FILES,
|
|
159
|
+
maxSettingsFileBytes = DEFAULT_MAX_SETTINGS_FILE_BYTES,
|
|
160
|
+
maxSettingsMatches = DEFAULT_MAX_SETTINGS_MATCHES,
|
|
161
|
+
maxLogFiles = DEFAULT_MAX_LOG_FILES,
|
|
162
|
+
maxLogFileBytes = DEFAULT_MAX_LOG_FILE_BYTES,
|
|
163
|
+
maxLogMatches = DEFAULT_MAX_LOG_MATCHES,
|
|
164
|
+
} = {}) {
|
|
165
|
+
assertNonEmptyString(threadId, 'threadId');
|
|
166
|
+
assertNonEmptyString(codexHome, 'codexHome');
|
|
167
|
+
assertNonEmptyString(projectPath, 'projectPath');
|
|
168
|
+
assertNonNegativeInteger(maxStorageFiles, 'maxStorageFiles');
|
|
169
|
+
assertPositiveInteger(maxStorageFileBytes, 'maxStorageFileBytes');
|
|
170
|
+
assertNonNegativeInteger(maxStorageMatches, 'maxStorageMatches');
|
|
171
|
+
assertNonNegativeInteger(maxExtensionFiles, 'maxExtensionFiles');
|
|
172
|
+
assertPositiveInteger(maxExtensionFileBytes, 'maxExtensionFileBytes');
|
|
173
|
+
assertNonNegativeInteger(maxExtensionMatches, 'maxExtensionMatches');
|
|
174
|
+
assertNonNegativeInteger(maxExtensionSourceSnippets, 'maxExtensionSourceSnippets');
|
|
175
|
+
assertNonNegativeInteger(maxSettingsFiles, 'maxSettingsFiles');
|
|
176
|
+
assertPositiveInteger(maxSettingsFileBytes, 'maxSettingsFileBytes');
|
|
177
|
+
assertNonNegativeInteger(maxSettingsMatches, 'maxSettingsMatches');
|
|
178
|
+
assertNonNegativeInteger(maxLogFiles, 'maxLogFiles');
|
|
179
|
+
assertPositiveInteger(maxLogFileBytes, 'maxLogFileBytes');
|
|
180
|
+
assertNonNegativeInteger(maxLogMatches, 'maxLogMatches');
|
|
181
|
+
|
|
182
|
+
const trimSource = buildCodexRolloutTrimSource({
|
|
183
|
+
threadId,
|
|
184
|
+
codexHome,
|
|
185
|
+
projectPath,
|
|
186
|
+
sourceReason: 'restore_source_audit_rollout',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (!trimSource) {
|
|
190
|
+
return {
|
|
191
|
+
status: 'refused',
|
|
192
|
+
reason: 'codex_rollout_source_required',
|
|
193
|
+
threadId,
|
|
194
|
+
proofScope: 'local_restore_source_inventory_only',
|
|
195
|
+
restartSafe: false,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const needles = buildRestoreNeedles({ threadId, restoreSafety: trimSource.restoreSafety });
|
|
200
|
+
const sessionIndex = inspectSessionIndex({ codexHome, threadId });
|
|
201
|
+
const stateDatabases = inspectCodexStateDatabases({ codexHome, threadId });
|
|
202
|
+
const vscodeStorage = inspectStorageRoots({
|
|
203
|
+
roots: vscodeStorageRoots,
|
|
204
|
+
needles,
|
|
205
|
+
maxFiles: maxStorageFiles,
|
|
206
|
+
maxFileBytes: maxStorageFileBytes,
|
|
207
|
+
maxMatches: maxStorageMatches,
|
|
208
|
+
});
|
|
209
|
+
const vscodeExtension = inspectVsCodeExtensionRoots({
|
|
210
|
+
roots: vscodeExtensionRoots,
|
|
211
|
+
maxFiles: maxExtensionFiles,
|
|
212
|
+
maxFileBytes: maxExtensionFileBytes,
|
|
213
|
+
maxMatches: maxExtensionMatches,
|
|
214
|
+
maxSourceSnippets: maxExtensionSourceSnippets,
|
|
215
|
+
});
|
|
216
|
+
const vscodeSettings = inspectVsCodeSettingsRoots({
|
|
217
|
+
roots: vscodeSettingsRoots,
|
|
218
|
+
maxFiles: maxSettingsFiles,
|
|
219
|
+
maxFileBytes: maxSettingsFileBytes,
|
|
220
|
+
maxMatches: maxSettingsMatches,
|
|
221
|
+
});
|
|
222
|
+
const vscodeLogs = inspectVsCodeLogRoots({
|
|
223
|
+
roots: vscodeLogRoots,
|
|
224
|
+
needles,
|
|
225
|
+
threadId,
|
|
226
|
+
maxFiles: maxLogFiles,
|
|
227
|
+
maxFileBytes: maxLogFileBytes,
|
|
228
|
+
maxMatches: maxLogMatches,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
status: 'restore-source-audit-complete',
|
|
233
|
+
reason: 'local_restore_sources_inspected',
|
|
234
|
+
proofScope: 'local_restore_source_inventory_only',
|
|
235
|
+
restartSafe: false,
|
|
236
|
+
threadId,
|
|
237
|
+
rollout: {
|
|
238
|
+
status: 'present',
|
|
239
|
+
path: trimSource.rolloutPath,
|
|
240
|
+
capturedTurns: trimSource.capturedTurns,
|
|
241
|
+
restoreSafety: trimSource.restoreSafety,
|
|
242
|
+
},
|
|
243
|
+
sessionIndex,
|
|
244
|
+
stateDatabases,
|
|
245
|
+
vscodeStorage,
|
|
246
|
+
vscodeExtension,
|
|
247
|
+
vscodeSettings,
|
|
248
|
+
vscodeLogs,
|
|
249
|
+
summary: summarizeAudit({
|
|
250
|
+
sessionIndex,
|
|
251
|
+
stateDatabases,
|
|
252
|
+
vscodeStorage,
|
|
253
|
+
vscodeExtension,
|
|
254
|
+
vscodeSettings,
|
|
255
|
+
vscodeLogs,
|
|
256
|
+
}),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function defaultVsCodeStorageRoots(env = process.env) {
|
|
261
|
+
const explicit = env.THROUGHLINE_VSCODE_STORAGE_ROOTS;
|
|
262
|
+
if (explicit) {
|
|
263
|
+
return explicit
|
|
264
|
+
.split(delimiter)
|
|
265
|
+
.map((entry) => entry.trim())
|
|
266
|
+
.filter(Boolean);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const home = homedir();
|
|
270
|
+
const roots = [
|
|
271
|
+
join(home, '.config', 'Code', 'User', 'globalStorage'),
|
|
272
|
+
join(home, '.config', 'Code', 'User', 'workspaceStorage'),
|
|
273
|
+
join(home, '.config', 'Code - Insiders', 'User', 'globalStorage'),
|
|
274
|
+
join(home, '.config', 'Code - Insiders', 'User', 'workspaceStorage'),
|
|
275
|
+
join(home, '.vscode-server', 'data', 'User', 'globalStorage'),
|
|
276
|
+
join(home, '.vscode-server', 'data', 'User', 'workspaceStorage'),
|
|
277
|
+
join(home, '.vscode-server-insiders', 'data', 'User', 'globalStorage'),
|
|
278
|
+
join(home, '.vscode-server-insiders', 'data', 'User', 'workspaceStorage'),
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
if (env.VSCODE_PORTABLE) {
|
|
282
|
+
roots.push(join(env.VSCODE_PORTABLE, 'user-data', 'User', 'globalStorage'));
|
|
283
|
+
roots.push(join(env.VSCODE_PORTABLE, 'user-data', 'User', 'workspaceStorage'));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return [...new Set(roots)];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function defaultVsCodeExtensionRoots(env = process.env) {
|
|
290
|
+
const explicit = env.THROUGHLINE_VSCODE_EXTENSION_ROOTS;
|
|
291
|
+
if (explicit) {
|
|
292
|
+
return explicit
|
|
293
|
+
.split(delimiter)
|
|
294
|
+
.map((entry) => entry.trim())
|
|
295
|
+
.filter(Boolean);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const home = homedir();
|
|
299
|
+
const roots = [
|
|
300
|
+
join(home, '.vscode', 'extensions'),
|
|
301
|
+
join(home, '.vscode-insiders', 'extensions'),
|
|
302
|
+
join(home, '.vscode-server', 'extensions'),
|
|
303
|
+
join(home, '.vscode-server-insiders', 'extensions'),
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
if (env.VSCODE_PORTABLE) {
|
|
307
|
+
roots.push(join(env.VSCODE_PORTABLE, 'extensions'));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return [...new Set(roots)];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function defaultVsCodeSettingsRoots(env = process.env) {
|
|
314
|
+
const explicit = env.THROUGHLINE_VSCODE_SETTINGS_ROOTS;
|
|
315
|
+
if (explicit) {
|
|
316
|
+
return explicit
|
|
317
|
+
.split(delimiter)
|
|
318
|
+
.map((entry) => entry.trim())
|
|
319
|
+
.filter(Boolean);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const home = homedir();
|
|
323
|
+
const roots = [
|
|
324
|
+
join(home, '.config', 'Code', 'User'),
|
|
325
|
+
join(home, '.config', 'Code - Insiders', 'User'),
|
|
326
|
+
join(home, '.vscode-server', 'data', 'User'),
|
|
327
|
+
join(home, '.vscode-server-insiders', 'data', 'User'),
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
if (env.VSCODE_PORTABLE) {
|
|
331
|
+
roots.push(join(env.VSCODE_PORTABLE, 'user-data', 'User'));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return [...new Set(roots)];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function defaultVsCodeLogRoots(env = process.env) {
|
|
338
|
+
const explicit = env.THROUGHLINE_VSCODE_LOG_ROOTS;
|
|
339
|
+
if (explicit) {
|
|
340
|
+
return explicit
|
|
341
|
+
.split(delimiter)
|
|
342
|
+
.map((entry) => entry.trim())
|
|
343
|
+
.filter(Boolean);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const home = homedir();
|
|
347
|
+
const roots = [
|
|
348
|
+
join(home, '.config', 'Code', 'logs'),
|
|
349
|
+
join(home, '.config', 'Code - Insiders', 'logs'),
|
|
350
|
+
join(home, '.vscode-server', 'data', 'logs'),
|
|
351
|
+
join(home, '.vscode-server-insiders', 'data', 'logs'),
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
if (env.VSCODE_PORTABLE) {
|
|
355
|
+
roots.push(join(env.VSCODE_PORTABLE, 'user-data', 'logs'));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return [...new Set(roots)];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function inspectSessionIndex({ codexHome, threadId }) {
|
|
362
|
+
const path = join(codexHome, 'session_index.jsonl');
|
|
363
|
+
if (!existsSync(path)) {
|
|
364
|
+
return { status: 'missing', path, containsThreadId: false };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let rows = 0;
|
|
368
|
+
let match = null;
|
|
369
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
370
|
+
if (!line.trim()) continue;
|
|
371
|
+
rows++;
|
|
372
|
+
try {
|
|
373
|
+
const row = JSON.parse(line);
|
|
374
|
+
if (row?.id === threadId) {
|
|
375
|
+
match = {
|
|
376
|
+
id: row.id,
|
|
377
|
+
threadName: row.thread_name ?? null,
|
|
378
|
+
updatedAt: row.updated_at ?? null,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
// Session index corruption is not fatal; rollout is the authoritative candidate source.
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
status: match ? 'present' : 'not-found',
|
|
388
|
+
path,
|
|
389
|
+
rows,
|
|
390
|
+
containsThreadId: Boolean(match),
|
|
391
|
+
match,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function inspectCodexStateDatabases({ codexHome, threadId }) {
|
|
396
|
+
if (!existsSync(codexHome)) {
|
|
397
|
+
return { status: 'missing', codexHome, databases: [] };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const files = readdirSync(codexHome)
|
|
401
|
+
.filter((name) => /^state(?:_\d+)?\.sqlite$/.test(name))
|
|
402
|
+
.map((name) => join(codexHome, name));
|
|
403
|
+
|
|
404
|
+
const databases = files.map((path) => inspectCodexStateDatabase({ path, threadId }));
|
|
405
|
+
const threadMatches = databases.reduce((sum, db) => sum + (db.threadRows?.length ?? 0), 0);
|
|
406
|
+
const turnBodyStores = databases.filter((db) => db.hasLikelyTurnBodyStore).map((db) => db.path);
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
status: databases.length > 0 ? 'present' : 'missing',
|
|
410
|
+
codexHome,
|
|
411
|
+
databases,
|
|
412
|
+
threadMatches,
|
|
413
|
+
hasLikelyTurnBodyStore: turnBodyStores.length > 0,
|
|
414
|
+
conclusion:
|
|
415
|
+
turnBodyStores.length > 0
|
|
416
|
+
? 'state_database_may_include_turn_bodies'
|
|
417
|
+
: 'state_database_appears_metadata_only',
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function inspectCodexStateDatabase({ path, threadId }) {
|
|
422
|
+
let db;
|
|
423
|
+
try {
|
|
424
|
+
db = new DatabaseSync(path, { readOnly: true });
|
|
425
|
+
const tables = db
|
|
426
|
+
.prepare("select name from sqlite_master where type = 'table' order by name")
|
|
427
|
+
.all()
|
|
428
|
+
.map((row) => row.name);
|
|
429
|
+
const tableColumns = Object.fromEntries(
|
|
430
|
+
tables.map((table) => [table, db.prepare(`pragma table_info(${quoteIdent(table)})`).all()]),
|
|
431
|
+
);
|
|
432
|
+
const likelyThreadBodyTables = tables.filter((table) =>
|
|
433
|
+
isLikelyThreadBodyTable(table, tableColumns[table] ?? []),
|
|
434
|
+
);
|
|
435
|
+
const contentTableMatches = likelyThreadBodyTables.map((table) =>
|
|
436
|
+
countThreadLinkedRows({
|
|
437
|
+
db,
|
|
438
|
+
table,
|
|
439
|
+
columns: tableColumns[table] ?? [],
|
|
440
|
+
threadId,
|
|
441
|
+
}),
|
|
442
|
+
);
|
|
443
|
+
const threadRows = tables.includes('threads')
|
|
444
|
+
? selectThreadRows({
|
|
445
|
+
db,
|
|
446
|
+
columns: tableColumns.threads ?? [],
|
|
447
|
+
threadId,
|
|
448
|
+
})
|
|
449
|
+
: [];
|
|
450
|
+
const matchingContentTables = contentTableMatches.filter((entry) => entry.rows > 0);
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
status: 'ok',
|
|
454
|
+
path,
|
|
455
|
+
tables,
|
|
456
|
+
likelyThreadBodyTables,
|
|
457
|
+
contentTableMatches,
|
|
458
|
+
hasLikelyTurnBodyStore: matchingContentTables.length > 0,
|
|
459
|
+
threadRows,
|
|
460
|
+
};
|
|
461
|
+
} catch (err) {
|
|
462
|
+
return {
|
|
463
|
+
status: 'error',
|
|
464
|
+
path,
|
|
465
|
+
error: err instanceof Error ? err.message : String(err),
|
|
466
|
+
};
|
|
467
|
+
} finally {
|
|
468
|
+
db?.close();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function quoteIdent(value) {
|
|
473
|
+
return `"${String(value).replaceAll('"', '""')}"`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isLikelyThreadBodyTable(table, columns) {
|
|
477
|
+
if (table === 'threads') return false;
|
|
478
|
+
const names = columns.map((column) => column.name);
|
|
479
|
+
const hasThreadLink = names.some((name) => /^(thread_id|assigned_thread_id|codex_thread_id)$/.test(name));
|
|
480
|
+
const hasBodyLikeColumn = names.some((name) =>
|
|
481
|
+
/(^|_)(message|content|body|history|item|memory|summary|row_json|result_json)($|_)/i.test(name),
|
|
482
|
+
);
|
|
483
|
+
return hasThreadLink && hasBodyLikeColumn;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function countThreadLinkedRows({ db, table, columns, threadId }) {
|
|
487
|
+
const threadColumn = columns
|
|
488
|
+
.map((column) => column.name)
|
|
489
|
+
.find((name) => /^(thread_id|assigned_thread_id|codex_thread_id)$/.test(name));
|
|
490
|
+
if (!threadColumn) return { table, threadColumn: null, rows: 0 };
|
|
491
|
+
|
|
492
|
+
const row = db
|
|
493
|
+
.prepare(
|
|
494
|
+
`select count(*) as rows from ${quoteIdent(table)} where ${quoteIdent(threadColumn)} = ?`,
|
|
495
|
+
)
|
|
496
|
+
.get(threadId);
|
|
497
|
+
return {
|
|
498
|
+
table,
|
|
499
|
+
threadColumn,
|
|
500
|
+
rows: Number(row?.rows) || 0,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function selectThreadRows({ db, columns, threadId }) {
|
|
505
|
+
const names = new Set(columns.map((column) => column.name));
|
|
506
|
+
const selected = ['id', 'rollout_path', 'source', 'cwd', 'title', 'updated_at'].filter((name) =>
|
|
507
|
+
names.has(name),
|
|
508
|
+
);
|
|
509
|
+
if (!names.has('id') || selected.length === 0) return [];
|
|
510
|
+
|
|
511
|
+
return db
|
|
512
|
+
.prepare(`select ${selected.map(quoteIdent).join(', ')} from threads where id = ? limit 5`)
|
|
513
|
+
.all(threadId);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function buildRestoreNeedles({ threadId, restoreSafety }) {
|
|
517
|
+
const needles = [{ id: 'thread_id', label: 'Codex thread id', value: threadId }];
|
|
518
|
+
let index = 1;
|
|
519
|
+
for (const entry of restoreSafety?.retainedTexts ?? []) {
|
|
520
|
+
const value = normalizeRestoreTextNeedle(entry.textPreview);
|
|
521
|
+
if (value.length < MIN_RESTORE_TEXT_NEEDLE_CHARS) continue;
|
|
522
|
+
needles.push({
|
|
523
|
+
id: `retained_rollback_text_${index++}`,
|
|
524
|
+
label: 'rollback text retained in compacted replacement history',
|
|
525
|
+
value,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return needles;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function normalizeRestoreTextNeedle(value) {
|
|
532
|
+
return String(value ?? '').replace(' [truncated]', '').replace(/\s+/g, ' ').trim();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function inspectStorageRoots({ roots, needles, maxFiles, maxFileBytes, maxMatches }) {
|
|
536
|
+
const rootResults = [];
|
|
537
|
+
let filesScanned = 0;
|
|
538
|
+
let bytesScanned = 0;
|
|
539
|
+
let truncated = false;
|
|
540
|
+
const matches = [];
|
|
541
|
+
const sqliteDatabases = [];
|
|
542
|
+
|
|
543
|
+
for (const root of roots) {
|
|
544
|
+
if (!existsSync(root)) {
|
|
545
|
+
rootResults.push({ path: root, status: 'missing' });
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const beforeFiles = filesScanned;
|
|
550
|
+
const beforeMatches = matches.length;
|
|
551
|
+
for (const file of walkFiles(root)) {
|
|
552
|
+
if (filesScanned >= maxFiles || matches.length >= maxMatches) {
|
|
553
|
+
truncated = true;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
let stat;
|
|
558
|
+
try {
|
|
559
|
+
stat = statSync(file);
|
|
560
|
+
} catch {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (!stat.isFile() || stat.size > maxFileBytes) continue;
|
|
564
|
+
|
|
565
|
+
filesScanned++;
|
|
566
|
+
bytesScanned += stat.size;
|
|
567
|
+
const hitIds = fileNeedleHits(file, needles);
|
|
568
|
+
if (hitIds.length > 0) {
|
|
569
|
+
matches.push({ path: file, size: stat.size, needles: hitIds });
|
|
570
|
+
}
|
|
571
|
+
if (isLikelySqliteStorageFile(file)) {
|
|
572
|
+
sqliteDatabases.push(inspectSqliteStorageFile({ path: file, needles }));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
rootResults.push({
|
|
577
|
+
path: root,
|
|
578
|
+
status: 'searched',
|
|
579
|
+
filesScanned: filesScanned - beforeFiles,
|
|
580
|
+
matches: matches.length - beforeMatches,
|
|
581
|
+
});
|
|
582
|
+
if (truncated) break;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
status: rootResults.some((root) => root.status === 'searched') ? 'searched' : 'missing',
|
|
587
|
+
roots: rootResults,
|
|
588
|
+
filesScanned,
|
|
589
|
+
bytesScanned,
|
|
590
|
+
matches,
|
|
591
|
+
sqliteDatabases,
|
|
592
|
+
sqliteDatabaseMatches: sqliteDatabases.reduce(
|
|
593
|
+
(sum, database) => sum + (database.matches?.length ?? 0),
|
|
594
|
+
0,
|
|
595
|
+
),
|
|
596
|
+
truncated,
|
|
597
|
+
limits: { maxFiles, maxFileBytes, maxMatches },
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function inspectVsCodeLogRoots({ roots, needles, threadId, maxFiles, maxFileBytes, maxMatches }) {
|
|
602
|
+
const rootResults = [];
|
|
603
|
+
let filesScanned = 0;
|
|
604
|
+
let bytesScanned = 0;
|
|
605
|
+
let truncated = false;
|
|
606
|
+
const matches = [];
|
|
607
|
+
const signalMatches = [];
|
|
608
|
+
const signalCounts = Object.fromEntries(VSCODE_LOG_SIGNAL_PATTERNS.map((signal) => [signal.id, 0]));
|
|
609
|
+
let threadIdMatches = 0;
|
|
610
|
+
let retainedTextMatches = 0;
|
|
611
|
+
|
|
612
|
+
for (const root of roots) {
|
|
613
|
+
if (!existsSync(root)) {
|
|
614
|
+
rootResults.push({ path: root, status: 'missing' });
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const beforeFiles = filesScanned;
|
|
619
|
+
const beforeMatches = matches.length + signalMatches.length;
|
|
620
|
+
for (const file of walkFiles(root)) {
|
|
621
|
+
if (filesScanned >= maxFiles || matches.length + signalMatches.length >= maxMatches) {
|
|
622
|
+
truncated = true;
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let stat;
|
|
627
|
+
try {
|
|
628
|
+
stat = statSync(file);
|
|
629
|
+
} catch {
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (!stat.isFile() || stat.size > maxFileBytes) continue;
|
|
633
|
+
|
|
634
|
+
let text;
|
|
635
|
+
try {
|
|
636
|
+
text = readFileSync(file, 'utf8');
|
|
637
|
+
} catch {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
filesScanned++;
|
|
642
|
+
bytesScanned += stat.size;
|
|
643
|
+
const needleHits = textNeedleHitDetails(text, needles);
|
|
644
|
+
if (needleHits.length > 0) {
|
|
645
|
+
const hitIds = needleHits.map((hit) => hit.id);
|
|
646
|
+
matches.push({ path: file, size: stat.size, needles: hitIds, needleHits });
|
|
647
|
+
threadIdMatches += needleHits
|
|
648
|
+
.filter((hit) => hit.id === 'thread_id')
|
|
649
|
+
.reduce((sum, hit) => sum + hit.count, 0);
|
|
650
|
+
retainedTextMatches += needleHits
|
|
651
|
+
.filter((hit) => hit.id.startsWith('retained_rollback_text_'))
|
|
652
|
+
.reduce((sum, hit) => sum + hit.count, 0);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const signals = logSignalHitDetails({ text, threadId, needleHits });
|
|
656
|
+
for (const signal of signals) {
|
|
657
|
+
if (matches.length + signalMatches.length >= maxMatches) {
|
|
658
|
+
truncated = true;
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
signalMatches.push({ path: file, size: stat.size, ...signal });
|
|
662
|
+
signalCounts[signal.signal] += signal.count;
|
|
663
|
+
}
|
|
664
|
+
if (truncated) break;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
rootResults.push({
|
|
668
|
+
path: root,
|
|
669
|
+
status: 'searched',
|
|
670
|
+
filesScanned: filesScanned - beforeFiles,
|
|
671
|
+
matches: matches.length + signalMatches.length - beforeMatches,
|
|
672
|
+
});
|
|
673
|
+
if (truncated) break;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
status: rootResults.some((root) => root.status === 'searched') ? 'searched' : 'missing',
|
|
678
|
+
roots: rootResults,
|
|
679
|
+
filesScanned,
|
|
680
|
+
bytesScanned,
|
|
681
|
+
matches,
|
|
682
|
+
signalMatches,
|
|
683
|
+
signals: {
|
|
684
|
+
threadIdMatches,
|
|
685
|
+
retainedTextMatches,
|
|
686
|
+
patchApplyFailures: signalCounts.patch_apply_failure ?? 0,
|
|
687
|
+
threadStreamStateSignals: signalCounts.thread_stream_state_broadcast ?? 0,
|
|
688
|
+
replacementHistorySignals: signalCounts.replacement_history ?? 0,
|
|
689
|
+
counts: signalCounts,
|
|
690
|
+
},
|
|
691
|
+
truncated,
|
|
692
|
+
limits: { maxFiles, maxFileBytes, maxMatches },
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function isLikelySqliteStorageFile(path) {
|
|
697
|
+
const name = basename(path).toLowerCase();
|
|
698
|
+
const ext = extname(name);
|
|
699
|
+
return ext === '.vscdb' || ext === '.sqlite' || ext === '.sqlite3' || ext === '.db';
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function inspectSqliteStorageFile({ path, needles }) {
|
|
703
|
+
let db;
|
|
704
|
+
try {
|
|
705
|
+
db = new DatabaseSync(path, { readOnly: true });
|
|
706
|
+
const tableNames = db
|
|
707
|
+
.prepare("select name from sqlite_master where type = 'table' order by name")
|
|
708
|
+
.all()
|
|
709
|
+
.map((row) => row.name);
|
|
710
|
+
const tables = [];
|
|
711
|
+
const matches = [];
|
|
712
|
+
for (const table of tableNames) {
|
|
713
|
+
const columns = db.prepare(`pragma table_info(${quoteIdent(table)})`).all();
|
|
714
|
+
const searchableColumns = columns
|
|
715
|
+
.map((column) => ({
|
|
716
|
+
name: column.name,
|
|
717
|
+
type: String(column.type ?? ''),
|
|
718
|
+
}))
|
|
719
|
+
.filter((column) => isSqliteSearchableStorageColumn(column));
|
|
720
|
+
tables.push({
|
|
721
|
+
name: table,
|
|
722
|
+
columns: columns.map((column) => ({
|
|
723
|
+
name: column.name,
|
|
724
|
+
type: column.type ?? '',
|
|
725
|
+
})),
|
|
726
|
+
searchableColumns: searchableColumns.map((column) => column.name),
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
for (const column of searchableColumns) {
|
|
730
|
+
for (const needle of needles) {
|
|
731
|
+
if (!needle.value) continue;
|
|
732
|
+
const row = db
|
|
733
|
+
.prepare(
|
|
734
|
+
`select count(*) as rows from ${quoteIdent(table)} where instr(cast(${quoteIdent(
|
|
735
|
+
column.name,
|
|
736
|
+
)} as text), ?) > 0`,
|
|
737
|
+
)
|
|
738
|
+
.get(needle.value);
|
|
739
|
+
const rows = Number(row?.rows) || 0;
|
|
740
|
+
if (rows > 0) {
|
|
741
|
+
matches.push({
|
|
742
|
+
table,
|
|
743
|
+
column: column.name,
|
|
744
|
+
needle: needle.id,
|
|
745
|
+
rows,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
status: 'ok',
|
|
753
|
+
path,
|
|
754
|
+
tables,
|
|
755
|
+
matches,
|
|
756
|
+
};
|
|
757
|
+
} catch (err) {
|
|
758
|
+
return {
|
|
759
|
+
status: 'error',
|
|
760
|
+
path,
|
|
761
|
+
error: err instanceof Error ? err.message : String(err),
|
|
762
|
+
};
|
|
763
|
+
} finally {
|
|
764
|
+
db?.close();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function isSqliteSearchableStorageColumn(column) {
|
|
769
|
+
const type = String(column.type ?? '').toLowerCase();
|
|
770
|
+
if (!type) return true;
|
|
771
|
+
return !/^(integer|int|real|float|double|numeric|boolean|bool|date|datetime)$/.test(type);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function inspectVsCodeExtensionRoots({ roots, maxFiles, maxFileBytes, maxMatches, maxSourceSnippets }) {
|
|
775
|
+
const rootResults = [];
|
|
776
|
+
let filesScanned = 0;
|
|
777
|
+
let bytesScanned = 0;
|
|
778
|
+
let truncated = false;
|
|
779
|
+
const matches = [];
|
|
780
|
+
let sourceSnippetCount = 0;
|
|
781
|
+
const packageSettings = [];
|
|
782
|
+
const evidence = Object.fromEntries(
|
|
783
|
+
VSCODE_EXTENSION_RESTORE_PATTERNS.map((pattern) => [pattern.id, false]),
|
|
784
|
+
);
|
|
785
|
+
const sourceFactEvidence = Object.fromEntries(
|
|
786
|
+
VSCODE_EXTENSION_SOURCE_FACT_PATTERNS.map((fact) => [fact.id, false]),
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
for (const root of roots) {
|
|
790
|
+
if (!existsSync(root)) {
|
|
791
|
+
rootResults.push({ path: root, status: 'missing' });
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const beforeFiles = filesScanned;
|
|
796
|
+
const beforeMatches = matches.length;
|
|
797
|
+
for (const file of walkVsCodeExtensionFiles(root)) {
|
|
798
|
+
if (filesScanned >= maxFiles || matches.length >= maxMatches) {
|
|
799
|
+
truncated = true;
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
let stat;
|
|
804
|
+
try {
|
|
805
|
+
stat = statSync(file);
|
|
806
|
+
} catch {
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (!stat.isFile() || stat.size > maxFileBytes) continue;
|
|
810
|
+
|
|
811
|
+
filesScanned++;
|
|
812
|
+
bytesScanned += stat.size;
|
|
813
|
+
const hit = filePatternHitDetails(file, VSCODE_EXTENSION_RESTORE_PATTERNS, {
|
|
814
|
+
maxSourceSnippets: Math.max(0, maxSourceSnippets - sourceSnippetCount),
|
|
815
|
+
});
|
|
816
|
+
if (hit.patterns.length > 0) {
|
|
817
|
+
for (const id of hit.patterns) evidence[id] = true;
|
|
818
|
+
for (const id of hit.sourceFacts) sourceFactEvidence[id] = true;
|
|
819
|
+
sourceSnippetCount += hit.sourceSnippets.length;
|
|
820
|
+
matches.push({
|
|
821
|
+
path: file,
|
|
822
|
+
size: stat.size,
|
|
823
|
+
patterns: hit.patterns,
|
|
824
|
+
sourceSnippets: hit.sourceSnippets,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
const settings = basename(file) === 'package.json' ? readVsCodeExtensionPackageSettings(file) : null;
|
|
828
|
+
if (settings?.followUpQueueModeDefault) {
|
|
829
|
+
packageSettings.push({
|
|
830
|
+
path: file,
|
|
831
|
+
followUpQueueModeDefault: settings.followUpQueueModeDefault,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
rootResults.push({
|
|
837
|
+
path: root,
|
|
838
|
+
status: 'searched',
|
|
839
|
+
filesScanned: filesScanned - beforeFiles,
|
|
840
|
+
matches: matches.length - beforeMatches,
|
|
841
|
+
});
|
|
842
|
+
if (truncated) break;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
status: rootResults.some((root) => root.status === 'searched') ? 'searched' : 'missing',
|
|
847
|
+
roots: rootResults,
|
|
848
|
+
filesScanned,
|
|
849
|
+
bytesScanned,
|
|
850
|
+
matches,
|
|
851
|
+
truncated,
|
|
852
|
+
patterns: VSCODE_EXTENSION_RESTORE_PATTERNS,
|
|
853
|
+
sourceFacts: summarizeVsCodeExtensionSourceFacts({
|
|
854
|
+
evidence,
|
|
855
|
+
sourceFactEvidence,
|
|
856
|
+
packageSettings,
|
|
857
|
+
}),
|
|
858
|
+
evidence,
|
|
859
|
+
packageSettings: {
|
|
860
|
+
followUpQueueModeDefault:
|
|
861
|
+
packageSettings.length > 0
|
|
862
|
+
? {
|
|
863
|
+
status: 'present',
|
|
864
|
+
values: [...new Set(packageSettings.map((entry) => entry.followUpQueueModeDefault))],
|
|
865
|
+
sources: packageSettings,
|
|
866
|
+
}
|
|
867
|
+
: { status: 'not-found', values: [], sources: [] },
|
|
868
|
+
},
|
|
869
|
+
restorePathSignals: summarizeVsCodeRestorePathSignals(evidence),
|
|
870
|
+
conclusion: summarizeVsCodeExtensionEvidence(evidence),
|
|
871
|
+
sourceSnippetCount,
|
|
872
|
+
limits: { maxFiles, maxFileBytes, maxMatches, maxSourceSnippets },
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function readVsCodeExtensionPackageSettings(path) {
|
|
877
|
+
let parsed;
|
|
878
|
+
try {
|
|
879
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
880
|
+
} catch {
|
|
881
|
+
return null;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const followUpQueueModeDefault =
|
|
885
|
+
parsed?.contributes?.configuration?.properties?.['chatgpt.followUpQueueMode']?.default;
|
|
886
|
+
if (typeof followUpQueueModeDefault !== 'string') return null;
|
|
887
|
+
return { followUpQueueModeDefault };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function inspectVsCodeSettingsRoots({ roots, maxFiles, maxFileBytes, maxMatches }) {
|
|
891
|
+
const rootResults = [];
|
|
892
|
+
let filesScanned = 0;
|
|
893
|
+
let bytesScanned = 0;
|
|
894
|
+
let truncated = false;
|
|
895
|
+
const matches = [];
|
|
896
|
+
|
|
897
|
+
for (const root of roots) {
|
|
898
|
+
if (!existsSync(root)) {
|
|
899
|
+
rootResults.push({ path: root, status: 'missing' });
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const beforeFiles = filesScanned;
|
|
904
|
+
const beforeMatches = matches.length;
|
|
905
|
+
for (const file of candidateSettingsFiles(root)) {
|
|
906
|
+
if (filesScanned >= maxFiles || matches.length >= maxMatches) {
|
|
907
|
+
truncated = true;
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
let stat;
|
|
912
|
+
try {
|
|
913
|
+
stat = statSync(file);
|
|
914
|
+
} catch {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (!stat.isFile() || stat.size > maxFileBytes) continue;
|
|
918
|
+
|
|
919
|
+
filesScanned++;
|
|
920
|
+
bytesScanned += stat.size;
|
|
921
|
+
const followUpQueueMode = readFollowUpQueueModeSetting(file);
|
|
922
|
+
if (followUpQueueMode.status === 'present') {
|
|
923
|
+
matches.push({ path: file, size: stat.size, followUpQueueMode: followUpQueueMode.value });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
rootResults.push({
|
|
928
|
+
path: root,
|
|
929
|
+
status: 'searched',
|
|
930
|
+
filesScanned: filesScanned - beforeFiles,
|
|
931
|
+
matches: matches.length - beforeMatches,
|
|
932
|
+
});
|
|
933
|
+
if (truncated) break;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return {
|
|
937
|
+
status: rootResults.some((root) => root.status === 'searched') ? 'searched' : 'missing',
|
|
938
|
+
roots: rootResults,
|
|
939
|
+
filesScanned,
|
|
940
|
+
bytesScanned,
|
|
941
|
+
matches,
|
|
942
|
+
truncated,
|
|
943
|
+
followUpQueueMode: {
|
|
944
|
+
status: matches.length > 0 ? 'explicit' : 'not-configured',
|
|
945
|
+
values: [...new Set(matches.map((match) => match.followUpQueueMode))],
|
|
946
|
+
},
|
|
947
|
+
limits: { maxFiles, maxFileBytes, maxMatches },
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function* candidateSettingsFiles(root) {
|
|
952
|
+
let stat;
|
|
953
|
+
try {
|
|
954
|
+
stat = statSync(root);
|
|
955
|
+
} catch {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (stat.isFile()) {
|
|
960
|
+
if (basename(root) === 'settings.json') yield root;
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (stat.isDirectory()) {
|
|
965
|
+
yield join(root, 'settings.json');
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function readFollowUpQueueModeSetting(path) {
|
|
970
|
+
let text;
|
|
971
|
+
try {
|
|
972
|
+
text = readFileSync(path, 'utf8');
|
|
973
|
+
} catch {
|
|
974
|
+
return { status: 'unreadable', value: null };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const match = text.match(/"chatgpt\.followUpQueueMode"\s*:\s*"([^"]+)"/);
|
|
978
|
+
if (!match) return { status: 'not-found', value: null };
|
|
979
|
+
return { status: 'present', value: match[1] };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function* walkFiles(root, { skipDirNames = new Set() } = {}) {
|
|
983
|
+
const stack = [root];
|
|
984
|
+
while (stack.length > 0) {
|
|
985
|
+
const dir = stack.pop();
|
|
986
|
+
let entries;
|
|
987
|
+
try {
|
|
988
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
989
|
+
} catch {
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
for (const entry of entries) {
|
|
993
|
+
const path = join(dir, entry.name);
|
|
994
|
+
if (entry.isDirectory()) {
|
|
995
|
+
if (skipDirNames.has(entry.name)) continue;
|
|
996
|
+
stack.push(path);
|
|
997
|
+
} else if (entry.isFile()) {
|
|
998
|
+
yield path;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function* walkVsCodeExtensionFiles(root) {
|
|
1005
|
+
let entries;
|
|
1006
|
+
try {
|
|
1007
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
1008
|
+
} catch {
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const likelyExtensionDirs = entries
|
|
1013
|
+
.filter((entry) => entry.isDirectory() && isLikelyCodexVsCodeExtensionName(entry.name))
|
|
1014
|
+
.map((entry) => join(root, entry.name));
|
|
1015
|
+
|
|
1016
|
+
if (likelyExtensionDirs.length === 0) {
|
|
1017
|
+
yield* walkFiles(root, { skipDirNames: new Set(['node_modules', '.git']) });
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
for (const dir of likelyExtensionDirs) {
|
|
1022
|
+
yield* walkFiles(dir, { skipDirNames: new Set(['node_modules', '.git']) });
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function isLikelyCodexVsCodeExtensionName(name) {
|
|
1027
|
+
return /(^|\.)(openai|chatgpt|codex)(\.|-|$)/i.test(name);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function fileNeedleHits(path, needles) {
|
|
1031
|
+
let buffer;
|
|
1032
|
+
try {
|
|
1033
|
+
buffer = readFileSync(path);
|
|
1034
|
+
} catch {
|
|
1035
|
+
return [];
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const hits = [];
|
|
1039
|
+
for (const needle of needles) {
|
|
1040
|
+
if (!needle.value) continue;
|
|
1041
|
+
if (buffer.indexOf(Buffer.from(needle.value, 'utf8')) !== -1) {
|
|
1042
|
+
hits.push(needle.id);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return hits;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function textNeedleHitDetails(text, needles) {
|
|
1049
|
+
const hits = [];
|
|
1050
|
+
for (const needle of needles) {
|
|
1051
|
+
if (!needle.value) continue;
|
|
1052
|
+
const count = countOccurrences(text, needle.value);
|
|
1053
|
+
if (count > 0) hits.push({ id: needle.id, count });
|
|
1054
|
+
}
|
|
1055
|
+
return hits;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function logSignalHitDetails({ text, threadId, needleHits }) {
|
|
1059
|
+
const hasThreadContext = text.includes(threadId);
|
|
1060
|
+
const hasRetainedTextContext = needleHits.some((hit) => hit.id.startsWith('retained_rollback_text_'));
|
|
1061
|
+
const signals = [];
|
|
1062
|
+
for (const signal of VSCODE_LOG_SIGNAL_PATTERNS) {
|
|
1063
|
+
const pattern = signal.buildPattern ? signal.buildPattern(threadId) : signal.pattern;
|
|
1064
|
+
if (!pattern) continue;
|
|
1065
|
+
const index = text.indexOf(pattern);
|
|
1066
|
+
if (index === -1) continue;
|
|
1067
|
+
if (!signal.buildPattern && !hasThreadContext && !hasRetainedTextContext) continue;
|
|
1068
|
+
const occurrences = signalOccurrenceDetails(text, pattern);
|
|
1069
|
+
signals.push({
|
|
1070
|
+
signal: signal.id,
|
|
1071
|
+
count: occurrences.count,
|
|
1072
|
+
firstTimestamp: occurrences.firstTimestamp,
|
|
1073
|
+
lastTimestamp: occurrences.lastTimestamp,
|
|
1074
|
+
excerpt: sourceExcerpt(text, index, pattern.length),
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
return signals;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function signalOccurrenceDetails(text, pattern) {
|
|
1081
|
+
const timestamps = [];
|
|
1082
|
+
let count = 0;
|
|
1083
|
+
let index = text.indexOf(pattern);
|
|
1084
|
+
while (index !== -1) {
|
|
1085
|
+
count++;
|
|
1086
|
+
const timestamp = timestampForLineAt(text, index);
|
|
1087
|
+
if (timestamp) timestamps.push(timestamp);
|
|
1088
|
+
index = text.indexOf(pattern, index + pattern.length);
|
|
1089
|
+
}
|
|
1090
|
+
return {
|
|
1091
|
+
count,
|
|
1092
|
+
firstTimestamp: timestamps[0] ?? null,
|
|
1093
|
+
lastTimestamp: timestamps[timestamps.length - 1] ?? null,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function timestampForLineAt(text, index) {
|
|
1098
|
+
const lineStart = text.lastIndexOf('\n', index) + 1;
|
|
1099
|
+
const lineEndIndex = text.indexOf('\n', index);
|
|
1100
|
+
const lineEnd = lineEndIndex === -1 ? text.length : lineEndIndex;
|
|
1101
|
+
const line = text.slice(lineStart, lineEnd);
|
|
1102
|
+
return line.match(/\b\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\b/)?.[0] ?? null;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function countOccurrences(text, pattern) {
|
|
1106
|
+
if (!pattern) return 0;
|
|
1107
|
+
let count = 0;
|
|
1108
|
+
let index = text.indexOf(pattern);
|
|
1109
|
+
while (index !== -1) {
|
|
1110
|
+
count++;
|
|
1111
|
+
index = text.indexOf(pattern, index + pattern.length);
|
|
1112
|
+
}
|
|
1113
|
+
return count;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function filePatternHitDetails(path, patterns, { maxSourceSnippets }) {
|
|
1117
|
+
let text;
|
|
1118
|
+
try {
|
|
1119
|
+
text = readFileSync(path, 'utf8');
|
|
1120
|
+
} catch {
|
|
1121
|
+
return { patterns: [], sourceFacts: [], sourceSnippets: [] };
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const hitIds = [];
|
|
1125
|
+
const sourceSnippets = [];
|
|
1126
|
+
for (const pattern of patterns) {
|
|
1127
|
+
const index = text.indexOf(pattern.pattern);
|
|
1128
|
+
if (index === -1) continue;
|
|
1129
|
+
hitIds.push(pattern.id);
|
|
1130
|
+
if (sourceSnippets.length < maxSourceSnippets) {
|
|
1131
|
+
sourceSnippets.push({
|
|
1132
|
+
pattern: pattern.id,
|
|
1133
|
+
excerpt: sourceExcerpt(text, index, pattern.pattern.length),
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
patterns: hitIds,
|
|
1139
|
+
sourceFacts: sourceFactHits(text),
|
|
1140
|
+
sourceSnippets,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function sourceFactHits(text) {
|
|
1145
|
+
return VSCODE_EXTENSION_SOURCE_FACT_PATTERNS
|
|
1146
|
+
.filter((fact) => sourceFactMatches(text, fact))
|
|
1147
|
+
.map((fact) => fact.id);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function sourceFactMatches(text, fact) {
|
|
1151
|
+
if (fact.requiredPatterns?.every((pattern) => text.includes(pattern))) return true;
|
|
1152
|
+
return fact.nearPatternSets?.some((set) => patternSetAppearsNear(text, set)) ?? false;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function patternSetAppearsNear(text, { patterns, windowChars = 240 }) {
|
|
1156
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return false;
|
|
1157
|
+
const [anchor, ...rest] = patterns;
|
|
1158
|
+
for (const index of patternIndexes(text, anchor)) {
|
|
1159
|
+
const start = Math.max(0, index - windowChars);
|
|
1160
|
+
const end = Math.min(text.length, index + anchor.length + windowChars);
|
|
1161
|
+
const excerpt = text.slice(start, end);
|
|
1162
|
+
if (rest.every((pattern) => excerpt.includes(pattern))) return true;
|
|
1163
|
+
}
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function* patternIndexes(text, pattern) {
|
|
1168
|
+
if (!pattern) return;
|
|
1169
|
+
let index = text.indexOf(pattern);
|
|
1170
|
+
while (index !== -1) {
|
|
1171
|
+
yield index;
|
|
1172
|
+
index = text.indexOf(pattern, index + pattern.length);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function sourceExcerpt(text, index, length) {
|
|
1177
|
+
const halfWindow = Math.floor(DEFAULT_EXTENSION_SOURCE_SNIPPET_CHARS / 2);
|
|
1178
|
+
const start = Math.max(0, index - halfWindow);
|
|
1179
|
+
const end = Math.min(text.length, index + length + halfWindow);
|
|
1180
|
+
const prefix = start > 0 ? '...' : '';
|
|
1181
|
+
const suffix = end < text.length ? '...' : '';
|
|
1182
|
+
const excerpt = text.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
1183
|
+
return `${prefix}${excerpt}${suffix}`;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function summarizeVsCodeExtensionEvidence(evidence) {
|
|
1187
|
+
if (!evidence || Object.values(evidence).every((value) => !value)) {
|
|
1188
|
+
return 'no_vscode_extension_restore_patterns_found';
|
|
1189
|
+
}
|
|
1190
|
+
if (evidence.thread_resume && evidence.mark_need_resume_after_reconnect) {
|
|
1191
|
+
return 'vscode_extension_reconnect_appears_to_resume_threads_via_app_server';
|
|
1192
|
+
}
|
|
1193
|
+
if (evidence.thread_read || evidence.thread_resume || evidence.thread_turns_list) {
|
|
1194
|
+
return 'vscode_extension_references_app_server_thread_restore_methods';
|
|
1195
|
+
}
|
|
1196
|
+
if (evidence.persisted_atom) {
|
|
1197
|
+
return 'vscode_extension_webview_persistence_patterns_found';
|
|
1198
|
+
}
|
|
1199
|
+
return 'vscode_extension_restore_related_patterns_found';
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function summarizeVsCodeRestorePathSignals(evidence) {
|
|
1203
|
+
const appServerRestore = [
|
|
1204
|
+
'thread_read',
|
|
1205
|
+
'thread_resume',
|
|
1206
|
+
'thread_turns_list',
|
|
1207
|
+
'thread_compact_start',
|
|
1208
|
+
'thread_rollback',
|
|
1209
|
+
].filter((id) => evidence[id]);
|
|
1210
|
+
const reconnect = ['mark_need_resume_after_reconnect', 'needs_resume'].filter((id) => evidence[id]);
|
|
1211
|
+
const webviewPersistence = ['persisted_atom'].filter((id) => evidence[id]);
|
|
1212
|
+
const followUpQueue = [
|
|
1213
|
+
'follow_up_queue_setting',
|
|
1214
|
+
'send_follow_up_message',
|
|
1215
|
+
'steering_user_message',
|
|
1216
|
+
].filter((id) => evidence[id]);
|
|
1217
|
+
|
|
1218
|
+
return {
|
|
1219
|
+
appServerRestore,
|
|
1220
|
+
reconnect,
|
|
1221
|
+
webviewPersistence,
|
|
1222
|
+
followUpQueue,
|
|
1223
|
+
hasAppServerRestoreSignals: appServerRestore.length > 0,
|
|
1224
|
+
hasReconnectSignals: reconnect.length > 0,
|
|
1225
|
+
hasWebviewPersistenceSignals: webviewPersistence.length > 0,
|
|
1226
|
+
hasFollowUpQueueSignals: followUpQueue.length > 0,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function summarizeVsCodeExtensionSourceFacts({ evidence, sourceFactEvidence, packageSettings }) {
|
|
1231
|
+
const followUpQueueModeDefaultValues = [
|
|
1232
|
+
...new Set(packageSettings.map((entry) => entry.followUpQueueModeDefault)),
|
|
1233
|
+
];
|
|
1234
|
+
const reconnectResumeViaAppServerRolloutPath =
|
|
1235
|
+
Boolean(sourceFactEvidence.thread_resume_uses_null_history) &&
|
|
1236
|
+
Boolean(sourceFactEvidence.thread_resume_uses_rollout_path) &&
|
|
1237
|
+
Boolean(sourceFactEvidence.reconnect_command_marks_threads_need_resume);
|
|
1238
|
+
const threadStreamPatchApplyPathPresent =
|
|
1239
|
+
Boolean(sourceFactEvidence.owner_broadcasts_thread_state_patches) &&
|
|
1240
|
+
Boolean(sourceFactEvidence.follower_applies_thread_state_patches) &&
|
|
1241
|
+
Boolean(sourceFactEvidence.patch_apply_failure_logged_in_thread_stream_handler);
|
|
1242
|
+
const rollbackNonResurrectionProjectionCandidates =
|
|
1243
|
+
VSCODE_ROLLBACK_NON_RESURRECTION_SOURCE_FACT_IDS.filter((id) =>
|
|
1244
|
+
Boolean(sourceFactEvidence[id]),
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
return {
|
|
1248
|
+
patterns: VSCODE_EXTENSION_SOURCE_FACT_PATTERNS,
|
|
1249
|
+
evidence: sourceFactEvidence,
|
|
1250
|
+
followUpQueueModeDefaultValues,
|
|
1251
|
+
reconnectResumeViaAppServerRolloutPath,
|
|
1252
|
+
threadStreamPatchApplyPathPresent,
|
|
1253
|
+
rollbackNonResurrectionProjectionPathPresent:
|
|
1254
|
+
rollbackNonResurrectionProjectionCandidates.length > 0,
|
|
1255
|
+
rollbackNonResurrectionProjectionCandidates,
|
|
1256
|
+
compactedReplacementHistoryPatternPresent: Boolean(evidence.compacted_replacement_history),
|
|
1257
|
+
hypothesis: reconnectResumeViaAppServerRolloutPath
|
|
1258
|
+
? 'reconnect_marks_threads_needing_app_server_resume_from_rollout_path'
|
|
1259
|
+
: 'source_facts_insufficient_for_reconnect_resume_path_hypothesis',
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function summarizeAudit({
|
|
1264
|
+
sessionIndex,
|
|
1265
|
+
stateDatabases,
|
|
1266
|
+
vscodeStorage,
|
|
1267
|
+
vscodeExtension,
|
|
1268
|
+
vscodeSettings,
|
|
1269
|
+
vscodeLogs,
|
|
1270
|
+
}) {
|
|
1271
|
+
return {
|
|
1272
|
+
sessionIndexContainsThreadId: sessionIndex.containsThreadId,
|
|
1273
|
+
codexStateThreadMatches: stateDatabases.threadMatches ?? 0,
|
|
1274
|
+
codexStateConclusion: stateDatabases.conclusion ?? 'not_inspected',
|
|
1275
|
+
vscodeStorageMatches: vscodeStorage.matches.length,
|
|
1276
|
+
vscodeStorageSearched: vscodeStorage.status === 'searched',
|
|
1277
|
+
vscodeStorageSqliteDatabases: vscodeStorage.sqliteDatabases?.length ?? 0,
|
|
1278
|
+
vscodeStorageSqliteDatabaseMatches: vscodeStorage.sqliteDatabaseMatches ?? 0,
|
|
1279
|
+
vscodeExtensionSearched: vscodeExtension.status === 'searched',
|
|
1280
|
+
vscodeExtensionMatches: vscodeExtension.matches.length,
|
|
1281
|
+
vscodeExtensionConclusion: vscodeExtension.conclusion,
|
|
1282
|
+
vscodeExtensionRestorePathSignals: vscodeExtension.restorePathSignals,
|
|
1283
|
+
vscodeExtensionSourceFacts: vscodeExtension.sourceFacts,
|
|
1284
|
+
vscodeExtensionFollowUpQueueModeDefault:
|
|
1285
|
+
vscodeExtension.packageSettings.followUpQueueModeDefault,
|
|
1286
|
+
vscodeExtensionSourceSnippetCount: vscodeExtension.sourceSnippetCount,
|
|
1287
|
+
vscodeSettingsSearched: vscodeSettings.status === 'searched',
|
|
1288
|
+
vscodeSettingsFollowUpQueueMode: vscodeSettings.followUpQueueMode,
|
|
1289
|
+
vscodeLogSearched: vscodeLogs.status === 'searched',
|
|
1290
|
+
vscodeLogMatches: vscodeLogs.matches.length,
|
|
1291
|
+
vscodeLogThreadIdMatches: vscodeLogs.signals?.threadIdMatches ?? 0,
|
|
1292
|
+
vscodeLogRetainedTextMatches: vscodeLogs.signals?.retainedTextMatches ?? 0,
|
|
1293
|
+
vscodeLogPatchApplyFailures: vscodeLogs.signals?.patchApplyFailures ?? 0,
|
|
1294
|
+
vscodeLogPatchApplyFailureFirstTimestamp: firstSignalTimestamp(
|
|
1295
|
+
vscodeLogs.signalMatches,
|
|
1296
|
+
'patch_apply_failure',
|
|
1297
|
+
),
|
|
1298
|
+
vscodeLogPatchApplyFailureLastTimestamp: lastSignalTimestamp(
|
|
1299
|
+
vscodeLogs.signalMatches,
|
|
1300
|
+
'patch_apply_failure',
|
|
1301
|
+
),
|
|
1302
|
+
vscodeLogThreadStreamStateSignals: vscodeLogs.signals?.threadStreamStateSignals ?? 0,
|
|
1303
|
+
vscodeLogReplacementHistorySignals: vscodeLogs.signals?.replacementHistorySignals ?? 0,
|
|
1304
|
+
vscodeThreadStreamPatchApplyPathPresent:
|
|
1305
|
+
vscodeExtension.sourceFacts?.threadStreamPatchApplyPathPresent ?? false,
|
|
1306
|
+
vscodeThreadStreamPatchFailureSignal:
|
|
1307
|
+
Boolean(vscodeExtension.sourceFacts?.threadStreamPatchApplyPathPresent) &&
|
|
1308
|
+
(vscodeLogs.signals?.patchApplyFailures ?? 0) > 0,
|
|
1309
|
+
vscodeRollbackNonResurrectionProjectionPathPresent:
|
|
1310
|
+
vscodeExtension.sourceFacts?.rollbackNonResurrectionProjectionPathPresent ?? false,
|
|
1311
|
+
vscodeRollbackNonResurrectionProjectionCandidates:
|
|
1312
|
+
vscodeExtension.sourceFacts?.rollbackNonResurrectionProjectionCandidates ?? [],
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function firstSignalTimestamp(signalMatches, signal) {
|
|
1317
|
+
const timestamps = (signalMatches ?? [])
|
|
1318
|
+
.filter((match) => match.signal === signal && match.firstTimestamp)
|
|
1319
|
+
.map((match) => match.firstTimestamp)
|
|
1320
|
+
.sort();
|
|
1321
|
+
return timestamps[0] ?? null;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function lastSignalTimestamp(signalMatches, signal) {
|
|
1325
|
+
const timestamps = (signalMatches ?? [])
|
|
1326
|
+
.filter((match) => match.signal === signal && match.lastTimestamp)
|
|
1327
|
+
.map((match) => match.lastTimestamp)
|
|
1328
|
+
.sort();
|
|
1329
|
+
return timestamps[timestamps.length - 1] ?? null;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function assertNonEmptyString(value, name) {
|
|
1333
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
1334
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function assertPositiveInteger(value, name) {
|
|
1339
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
1340
|
+
throw new Error(`${name} must be a positive integer`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function assertNonNegativeInteger(value, name) {
|
|
1345
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
1346
|
+
throw new Error(`${name} must be a non-negative integer`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|