@turns/dev 0.0.1
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/README.md +74 -0
- package/dist/agent-anchor-client.js +319 -0
- package/dist/agent-anchor.js +1685 -0
- package/dist/auth.js +258 -0
- package/dist/daemon.js +134 -0
- package/dist/deepseek-trajectories.js +196 -0
- package/dist/directory-policy.js +41 -0
- package/dist/file-browsing.js +241 -0
- package/dist/git-association.js +136 -0
- package/dist/harness-registry.js +323 -0
- package/dist/index.js +300 -0
- package/dist/local-session-sources.js +1448 -0
- package/dist/native-record-compaction.js +84 -0
- package/dist/path-resolution.js +298 -0
- package/dist/question-mcp.js +126 -0
- package/dist/queue-worker.js +1053 -0
- package/dist/secret-redaction.js +181 -0
- package/dist/upload-sessions.js +2016 -0
- package/package.json +35 -0
|
@@ -0,0 +1,1448 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import { readDeepseekTrajectoryHeader } from "./deepseek-trajectories.js";
|
|
7
|
+
const HOME = os.homedir();
|
|
8
|
+
const PROJECTION_ROOT = path.join(HOME, '.local', 'state', 'turns-dev', 'session-projections');
|
|
9
|
+
const DEEPSEEK_SESSIONS_ROOT = path.join(path.resolve(process.env.DSH_HOME || path.join(HOME, '.dsh')), 'sessions');
|
|
10
|
+
const JSON_FILE_CACHE = new Map();
|
|
11
|
+
const CURSOR_WORKER_CWD_CACHE = new Map();
|
|
12
|
+
const ANTIGRAVITY_TRANSCRIPT_CWD_CACHE = new Map();
|
|
13
|
+
const SUBAGENT_MARKER_CACHE = new Map();
|
|
14
|
+
const LINEAGE_CACHE = new Map();
|
|
15
|
+
const STORE_DESCRIPTOR_CACHE = new Map();
|
|
16
|
+
const MAX_METADATA_CACHE_ENTRIES = 16_384;
|
|
17
|
+
const OPAQUE_UUID_SOURCE = '[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}';
|
|
18
|
+
function jsonObject(value) {
|
|
19
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
20
|
+
}
|
|
21
|
+
function nativeFileStoreIncludes(harness, file) {
|
|
22
|
+
return nativeSubagentPathLineage(harness, file)?.uploadable !== false;
|
|
23
|
+
}
|
|
24
|
+
const EXTRA_FILE_STORES = {
|
|
25
|
+
claude: {
|
|
26
|
+
root: path.join(HOME, '.claude', 'projects'),
|
|
27
|
+
include: (file) => file.endsWith('.jsonl') && nativeFileStoreIncludes('claude', file),
|
|
28
|
+
},
|
|
29
|
+
cursor: {
|
|
30
|
+
root: path.join(HOME, '.cursor', 'projects'),
|
|
31
|
+
include: (file) => /[\\/]agent-transcripts[\\/]([^\\/]+)[\\/]\1\.jsonl$/i.test(file),
|
|
32
|
+
},
|
|
33
|
+
codex: {
|
|
34
|
+
root: path.join(process.env.CODEX_HOME || path.join(HOME, '.codex'), 'sessions'),
|
|
35
|
+
include: (file) => /[\\/]rollout-[^\\/]+\.jsonl$/i.test(file),
|
|
36
|
+
},
|
|
37
|
+
grok: { root: path.join(HOME, '.grok', 'sessions'), include: (file) => path.basename(file) === 'chat_history.jsonl' },
|
|
38
|
+
kimi: {
|
|
39
|
+
root: path.join(HOME, '.kimi-code', 'sessions'),
|
|
40
|
+
include: (file) => /[\\/]agents[\\/][^\\/]+[\\/]wire\.jsonl$/i.test(file),
|
|
41
|
+
},
|
|
42
|
+
pi: {
|
|
43
|
+
root: path.join(HOME, '.pi', 'agent', 'sessions'),
|
|
44
|
+
include: (file) => file.endsWith('.jsonl') && !/[\\/]agents[\\/]/i.test(file),
|
|
45
|
+
},
|
|
46
|
+
gemini: {
|
|
47
|
+
root: path.join(HOME, '.gemini', 'tmp'),
|
|
48
|
+
include: (file) => /[\\/]chats[\\/](?:session-[^\\/]+\.jsonl|[^\\/]+\.json|[^\\/]+[\\/][^\\/]+\.jsonl)$/i.test(file),
|
|
49
|
+
},
|
|
50
|
+
antigravity: {
|
|
51
|
+
root: path.join(HOME, '.gemini', 'antigravity-cli', 'brain'),
|
|
52
|
+
include: (file) => path.basename(file) === 'transcript_full.jsonl',
|
|
53
|
+
},
|
|
54
|
+
copilot: {
|
|
55
|
+
root: path.join(process.env.COPILOT_HOME || path.join(HOME, '.copilot'), 'session-state'),
|
|
56
|
+
include: (file) => path.basename(file) === 'events.jsonl',
|
|
57
|
+
},
|
|
58
|
+
muse: {
|
|
59
|
+
root: path.join(HOME, '.local', 'share', 'muse', 'sessions'),
|
|
60
|
+
include: (file) => path.basename(file) === 'session.jsonl',
|
|
61
|
+
},
|
|
62
|
+
qwen: {
|
|
63
|
+
root: path.join(HOME, '.qwen', 'projects'),
|
|
64
|
+
include: (file) => /[\\/]chats[\\/][^\\/]+\.jsonl$/i.test(file),
|
|
65
|
+
},
|
|
66
|
+
codebuddy: {
|
|
67
|
+
root: path.join(HOME, '.codebuddy', 'projects'),
|
|
68
|
+
include: (file) => file.endsWith('.jsonl') && nativeFileStoreIncludes('codebuddy', file),
|
|
69
|
+
},
|
|
70
|
+
prime: {
|
|
71
|
+
root: path.join(HOME, '.prime', 'agent', 'sessions'),
|
|
72
|
+
include: (file) => path.dirname(file) === path.join(HOME, '.prime', 'agent', 'sessions') && file.endsWith('.jsonl'),
|
|
73
|
+
},
|
|
74
|
+
omp: {
|
|
75
|
+
root: path.join(HOME, '.omp', 'agent', 'sessions'),
|
|
76
|
+
include: (file) => /^\d{4}-\d{2}-\d{2}T.+_[0-9a-f-]{36}\.jsonl$/i.test(path.basename(file)),
|
|
77
|
+
},
|
|
78
|
+
cline: {
|
|
79
|
+
root: path.join(HOME, '.cline', 'data', 'sessions'),
|
|
80
|
+
include: (file) => file.endsWith('.messages.json'),
|
|
81
|
+
},
|
|
82
|
+
vibe: {
|
|
83
|
+
root: path.join(HOME, '.vibe', 'logs', 'session'),
|
|
84
|
+
include: (file) => path.basename(file) === 'messages.jsonl',
|
|
85
|
+
},
|
|
86
|
+
poolside: {
|
|
87
|
+
root: path.join(HOME, '.local', 'state', 'poolside', 'trajectories'),
|
|
88
|
+
include: (file) => /^trajectory-[^/]+\.ndjson$/i.test(path.basename(file)),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
function safeStat(file) {
|
|
92
|
+
try {
|
|
93
|
+
return fs.statSync(file);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function walk(directory, visit) {
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const full = path.join(directory, entry.name);
|
|
109
|
+
if (entry.isDirectory())
|
|
110
|
+
walk(full, visit);
|
|
111
|
+
else if (entry.isFile())
|
|
112
|
+
visit(full);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function displayPath(file) {
|
|
116
|
+
const relative = path.relative(HOME, file);
|
|
117
|
+
return relative && !relative.startsWith('..') ? `~/${relative.replaceAll(path.sep, '/')}` : file;
|
|
118
|
+
}
|
|
119
|
+
const STRUCTURAL_SUBAGENT_STORES = {
|
|
120
|
+
claude: {
|
|
121
|
+
path: new RegExp(`^(.*)/(${OPAQUE_UUID_SOURCE})/subagents/agent-([^/]+)\\.jsonl$`, 'iu'),
|
|
122
|
+
excludedNativeSessionIds: [/^acompact(?:[-.]|$)/iu, /^aprompt[_-]?suggestion(?:[-.]|$)/iu],
|
|
123
|
+
lineage: (match) => ({
|
|
124
|
+
nativeSessionId: match[3],
|
|
125
|
+
parentNativeSessionId: match[2],
|
|
126
|
+
parentSourcePath: `${match[1]}/${match[2]}.jsonl`,
|
|
127
|
+
}),
|
|
128
|
+
},
|
|
129
|
+
codebuddy: {
|
|
130
|
+
path: new RegExp(`^(.*)/(${OPAQUE_UUID_SOURCE})/subagents/agent-([^/]+)\\.jsonl$`, 'iu'),
|
|
131
|
+
excludedNativeSessionIds: [/^acompact(?:[-.]|$)/iu, /^aprompt[_-]?suggestion(?:[-.]|$)/iu],
|
|
132
|
+
lineage: (match) => ({
|
|
133
|
+
nativeSessionId: match[3],
|
|
134
|
+
parentNativeSessionId: match[2],
|
|
135
|
+
parentSourcePath: `${match[1]}/${match[2]}.jsonl`,
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
muse: {
|
|
139
|
+
path: new RegExp(`^(.*?/(${OPAQUE_UUID_SOURCE}))/subagent/(${OPAQUE_UUID_SOURCE})/session\\.jsonl$`, 'iu'),
|
|
140
|
+
// Muse 1.x uses this namespace for one-run reminder observers, not
|
|
141
|
+
// user-delegated conversations. Cancelled observers may never persist the
|
|
142
|
+
// reminder-decision event, so the structural namespace is authoritative.
|
|
143
|
+
uploadable: false,
|
|
144
|
+
lineage: (match) => ({
|
|
145
|
+
nativeSessionId: match[3],
|
|
146
|
+
parentNativeSessionId: match[2],
|
|
147
|
+
parentSourcePath: `${match[1]}/session.jsonl`,
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
150
|
+
kimi: {
|
|
151
|
+
path: new RegExp(`^(.*?/session_(${OPAQUE_UUID_SOURCE}))/agents/(?!main/)([^/]+)/wire\\.jsonl$`, 'iu'),
|
|
152
|
+
lineage: (match) => ({
|
|
153
|
+
nativeSessionId: `${match[2]}/${match[3]}`,
|
|
154
|
+
parentNativeSessionId: match[2],
|
|
155
|
+
parentSourcePath: `${match[1]}/agents/main/wire.jsonl`,
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
gemini: {
|
|
159
|
+
path: /^(.*?\/chats)\/([^/]+)\/([^/]+)\.jsonl$/iu,
|
|
160
|
+
lineage: (match) => ({
|
|
161
|
+
nativeSessionId: match[3],
|
|
162
|
+
parentNativeSessionId: match[2],
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
cline: {
|
|
166
|
+
path: /^(.*?\/([^/]+))\/agent_([^/]+)\.messages\.json$/iu,
|
|
167
|
+
lineage: (match) => ({
|
|
168
|
+
nativeSessionId: `agent_${match[3]}`,
|
|
169
|
+
parentNativeSessionId: match[2],
|
|
170
|
+
parentSourcePath: `${match[1]}/${match[2]}.messages.json`,
|
|
171
|
+
}),
|
|
172
|
+
},
|
|
173
|
+
vibe: {
|
|
174
|
+
path: /^(.*?\/(session_[^/]+))\/agents\/([^/]+)\/messages\.jsonl$/iu,
|
|
175
|
+
lineage: (match) => ({
|
|
176
|
+
nativeSessionId: `${match[2]}/${match[3]}`,
|
|
177
|
+
parentSourcePath: `${match[1]}/messages.jsonl`,
|
|
178
|
+
}),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
/** Interpret one harness's structural child path. `uploadable` lets the same
|
|
182
|
+
* rule reject internal sidechains before discovery and lineage can diverge. */
|
|
183
|
+
export function nativeSubagentPathLineage(harness, sourcePath) {
|
|
184
|
+
const normalized = String(sourcePath || '').replaceAll('\\', '/');
|
|
185
|
+
const rule = STRUCTURAL_SUBAGENT_STORES[harness];
|
|
186
|
+
const match = rule?.path.exec(normalized);
|
|
187
|
+
if (!match)
|
|
188
|
+
return null;
|
|
189
|
+
const lineage = rule.lineage(match);
|
|
190
|
+
const uploadable = rule.uploadable !== false &&
|
|
191
|
+
!rule.excludedNativeSessionIds?.some((pattern) => pattern.test(lineage.nativeSessionId));
|
|
192
|
+
return { ...lineage, uploadable };
|
|
193
|
+
}
|
|
194
|
+
function claudeSubagentMetadataFile(descriptor) {
|
|
195
|
+
if (descriptor?.harness !== 'claude' && descriptor?.harness !== 'codebuddy')
|
|
196
|
+
return '';
|
|
197
|
+
const file = path.resolve(String(descriptor?.file || ''));
|
|
198
|
+
const name = path.basename(file);
|
|
199
|
+
if (!/^agent-.+\.jsonl$/iu.test(name))
|
|
200
|
+
return '';
|
|
201
|
+
return path.join(path.dirname(file), name.replace(/\.jsonl$/iu, '.meta.json'));
|
|
202
|
+
}
|
|
203
|
+
/** Claude keeps every descendant transcript in one flat directory. Its
|
|
204
|
+
* adjacent sidecar is the only durable source that distinguishes a direct
|
|
205
|
+
* child from a grandchild and also carries the human-readable dispatch label. */
|
|
206
|
+
function claudeSubagentMetadata(descriptor, structural) {
|
|
207
|
+
if (!structural || (descriptor?.harness !== 'claude' && descriptor?.harness !== 'codebuddy'))
|
|
208
|
+
return null;
|
|
209
|
+
const metadataFile = claudeSubagentMetadataFile(descriptor);
|
|
210
|
+
if (!metadataFile)
|
|
211
|
+
return null;
|
|
212
|
+
const metadata = jsonFile(metadataFile);
|
|
213
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
|
|
214
|
+
return null;
|
|
215
|
+
const heldParentAgentId = typeof metadata.parentAgentId === 'string' ? metadata.parentAgentId.trim() : '';
|
|
216
|
+
const parentAgentId = heldParentAgentId && heldParentAgentId !== structural.nativeSessionId && /^[^/\\\0]+$/u.test(heldParentAgentId)
|
|
217
|
+
? heldParentAgentId
|
|
218
|
+
: '';
|
|
219
|
+
const description = typeof metadata.description === 'string' ? metadata.description.trim() : '';
|
|
220
|
+
const agentType = typeof metadata.agentType === 'string' ? metadata.agentType.trim() : '';
|
|
221
|
+
const spawnDepth = Number(metadata.spawnDepth);
|
|
222
|
+
const sourcePath = String(descriptor?.sourcePath || descriptor?.file || '').replaceAll('\\', '/');
|
|
223
|
+
const parentSourcePath = parentAgentId
|
|
224
|
+
? sourcePath.replace(/agent-[^/]+\.jsonl$/iu, () => `agent-${parentAgentId}.jsonl`)
|
|
225
|
+
: '';
|
|
226
|
+
return {
|
|
227
|
+
...(parentAgentId ? { parentAgentId, parentSourcePath } : {}),
|
|
228
|
+
...(description ? { description } : {}),
|
|
229
|
+
...(agentType ? { agentType } : {}),
|
|
230
|
+
...(Number.isFinite(spawnDepth) && spawnDepth >= 1 ? { spawnDepth } : {}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function nativeSessionIdFromPath(harness, sourcePath) {
|
|
234
|
+
const normalized = String(sourcePath || '').replaceAll('\\', '/');
|
|
235
|
+
const child = nativeSubagentPathLineage(harness, normalized);
|
|
236
|
+
if (child?.uploadable !== false && child?.nativeSessionId)
|
|
237
|
+
return child.nativeSessionId;
|
|
238
|
+
const patterns = {
|
|
239
|
+
claude: new RegExp(`/projects/[^/]+/(${OPAQUE_UUID_SOURCE})\\.jsonl$`, 'iu'),
|
|
240
|
+
codebuddy: new RegExp(`/projects/[^/]+/(${OPAQUE_UUID_SOURCE})\\.jsonl$`, 'iu'),
|
|
241
|
+
codex: new RegExp(`/rollout-[^/]*-(${OPAQUE_UUID_SOURCE})\\.jsonl$`, 'iu'),
|
|
242
|
+
antigravity: new RegExp(`/brain/(${OPAQUE_UUID_SOURCE})(?:/|$)`, 'iu'),
|
|
243
|
+
kimi: new RegExp(`/session_(${OPAQUE_UUID_SOURCE})(?:/|$)`, 'iu'),
|
|
244
|
+
grok: new RegExp(`/sessions/[^/]+/(${OPAQUE_UUID_SOURCE})(?:/|$)`, 'iu'),
|
|
245
|
+
copilot: new RegExp(`/session-state/(${OPAQUE_UUID_SOURCE})(?:/|$)`, 'iu'),
|
|
246
|
+
cursor: new RegExp(`/agent-transcripts/(${OPAQUE_UUID_SOURCE})(?:/|$)`, 'iu'),
|
|
247
|
+
muse: new RegExp(`/sessions/\\d{4}/\\d{2}/\\d{2}/(${OPAQUE_UUID_SOURCE})/session\\.jsonl$`, 'iu'),
|
|
248
|
+
qwen: /\/chats\/([^/]+)\.jsonl$/iu,
|
|
249
|
+
cline: /\/([^/]+)\.messages\.json$/iu,
|
|
250
|
+
poolside: /trajectory-(?:standalone_)?([0-9a-f-]{20,})\.ndjson$/iu,
|
|
251
|
+
};
|
|
252
|
+
return patterns[harness]?.exec(normalized)?.[1] || '';
|
|
253
|
+
}
|
|
254
|
+
function descriptorNativeSessionId(descriptor) {
|
|
255
|
+
return (String(descriptor?.nativeSessionId || '').trim() ||
|
|
256
|
+
nativeSessionIdFromPath(String(descriptor?.harness || ''), descriptor?.sourcePath || descriptor?.file));
|
|
257
|
+
}
|
|
258
|
+
function fileDescriptor(harness, file) {
|
|
259
|
+
const stat = safeStat(file);
|
|
260
|
+
if (!stat?.isFile() || stat.size === 0)
|
|
261
|
+
return null;
|
|
262
|
+
return {
|
|
263
|
+
harness,
|
|
264
|
+
agent: harness,
|
|
265
|
+
file,
|
|
266
|
+
sourcePath: displayPath(file),
|
|
267
|
+
size: stat.size,
|
|
268
|
+
mtimeMs: stat.mtimeMs,
|
|
269
|
+
createdAt: (stat.birthtimeMs > 0 ? stat.birthtime : stat.mtime).toISOString(),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/** Discover top-level and subagent DeepSeek trajectories with header metadata. */
|
|
273
|
+
export function deepseekTrajectoryDescriptors(root = DEEPSEEK_SESSIONS_ROOT) {
|
|
274
|
+
const descriptors = [];
|
|
275
|
+
walk(root, (file) => {
|
|
276
|
+
if (!/^session\.jsonl(?:\.zstd)?$/u.test(path.basename(file)))
|
|
277
|
+
return;
|
|
278
|
+
const descriptor = fileDescriptor('deepseek', file);
|
|
279
|
+
if (!descriptor)
|
|
280
|
+
return;
|
|
281
|
+
let header;
|
|
282
|
+
try {
|
|
283
|
+
header = readDeepseekTrajectoryHeader(file);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const createdAtMs = Number(header.createdAt);
|
|
289
|
+
const nativeSessionId = String(header.id || '').trim();
|
|
290
|
+
const shortId = nativeSessionId.replace(/^session-/u, '').slice(0, 8);
|
|
291
|
+
descriptors.push({
|
|
292
|
+
...descriptor,
|
|
293
|
+
nativeSessionId,
|
|
294
|
+
...(String(header.cwd || '').trim() ? { workingDirectory: String(header.cwd).trim() } : {}),
|
|
295
|
+
...(Number.isFinite(createdAtMs) && createdAtMs > 0 ? { createdAt: new Date(createdAtMs).toISOString() } : {}),
|
|
296
|
+
nativeTitle: `DeepSeek ${shortId || 'session'}`,
|
|
297
|
+
isSubagent: header.origin === 'subagent' || Number(header.delegationDepth) > 0,
|
|
298
|
+
...(String(header.parentSession || '').trim()
|
|
299
|
+
? { parentNativeSessionId: String(header.parentSession).trim() }
|
|
300
|
+
: {}),
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
return descriptors;
|
|
304
|
+
}
|
|
305
|
+
function firstJsonDocument(file) {
|
|
306
|
+
const stat = safeStat(file);
|
|
307
|
+
if (!stat?.isFile())
|
|
308
|
+
return null;
|
|
309
|
+
let descriptor;
|
|
310
|
+
try {
|
|
311
|
+
descriptor = fs.openSync(file, 'r');
|
|
312
|
+
const buffer = Buffer.alloc(Math.min(Math.max(1, stat.size), 1024 * 1024));
|
|
313
|
+
const bytes = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
314
|
+
const source = buffer
|
|
315
|
+
.subarray(0, bytes)
|
|
316
|
+
.toString('utf8')
|
|
317
|
+
.replace(/^\uFEFF/u, '');
|
|
318
|
+
const firstLine = source.split(/\r?\n/u, 1)[0];
|
|
319
|
+
try {
|
|
320
|
+
return JSON.parse(firstLine);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
if (stat.size <= buffer.length) {
|
|
324
|
+
const parsed = JSON.parse(source);
|
|
325
|
+
return Array.isArray(parsed) ? parsed[0] : parsed;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
try {
|
|
334
|
+
if (descriptor !== undefined)
|
|
335
|
+
fs.closeSync(descriptor);
|
|
336
|
+
}
|
|
337
|
+
catch { }
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
function parsedObject(value) {
|
|
342
|
+
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
343
|
+
return value;
|
|
344
|
+
if (typeof value !== 'string' || !value.startsWith('{'))
|
|
345
|
+
return null;
|
|
346
|
+
try {
|
|
347
|
+
const parsed = JSON.parse(value);
|
|
348
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function recordMarksSubagent(record) {
|
|
355
|
+
const root = parsedObject(record);
|
|
356
|
+
if (!root)
|
|
357
|
+
return false;
|
|
358
|
+
const candidates = [
|
|
359
|
+
root,
|
|
360
|
+
parsedObject(root.payload),
|
|
361
|
+
parsedObject(root.session),
|
|
362
|
+
parsedObject(root.header),
|
|
363
|
+
parsedObject(root.metadata),
|
|
364
|
+
parsedObject(root.payload)?.session,
|
|
365
|
+
]
|
|
366
|
+
.map(parsedObject)
|
|
367
|
+
.filter(Boolean);
|
|
368
|
+
for (const candidate of candidates) {
|
|
369
|
+
const kindValues = [
|
|
370
|
+
candidate.thread_source,
|
|
371
|
+
candidate.threadSource,
|
|
372
|
+
candidate.session_type,
|
|
373
|
+
candidate.sessionType,
|
|
374
|
+
candidate.agent_type,
|
|
375
|
+
candidate.agentType,
|
|
376
|
+
];
|
|
377
|
+
if (kindValues.some((value) => /^(?:subagent|sub-agent|child-agent|delegate)$/iu.test(String(value || '')))) {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
if (candidate.is_subagent === true ||
|
|
381
|
+
candidate.isSubagent === true ||
|
|
382
|
+
candidate.is_sidechain === true ||
|
|
383
|
+
candidate.isSidechain === true)
|
|
384
|
+
return true;
|
|
385
|
+
const source = parsedObject(candidate.source);
|
|
386
|
+
if (source?.subagent || source?.sub_agent || source?.delegate)
|
|
387
|
+
return true;
|
|
388
|
+
if (/^(?:subagent|sub-agent|delegate)$/iu.test(String(candidate.source || '')))
|
|
389
|
+
return true;
|
|
390
|
+
const modelConfig = parsedObject(candidate.model_config || candidate.modelConfig);
|
|
391
|
+
if (modelConfig?._delegate_from)
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
/** True only when a native descriptor carries an explicit child-agent marker. */
|
|
397
|
+
export function nativeDescriptorIsSubagent(descriptor) {
|
|
398
|
+
const sourcePath = String(descriptor?.sourcePath || descriptor?.file || '').replaceAll('\\', '/');
|
|
399
|
+
const structural = nativeSubagentPathLineage(descriptor?.harness, sourcePath);
|
|
400
|
+
if (structural)
|
|
401
|
+
return true;
|
|
402
|
+
if (descriptor?.isSubagent === true || descriptor?.threadSource === 'subagent')
|
|
403
|
+
return true;
|
|
404
|
+
const file = String(descriptor?.file || '');
|
|
405
|
+
const stat = safeStat(file);
|
|
406
|
+
const fingerprint = stat?.isFile() ? `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}` : '';
|
|
407
|
+
const cached = SUBAGENT_MARKER_CACHE.get(file);
|
|
408
|
+
if (cached?.fingerprint === fingerprint) {
|
|
409
|
+
// Refresh insertion order so the bounded map behaves as a tiny LRU.
|
|
410
|
+
SUBAGENT_MARKER_CACHE.delete(file);
|
|
411
|
+
SUBAGENT_MARKER_CACHE.set(file, cached);
|
|
412
|
+
return cached.value;
|
|
413
|
+
}
|
|
414
|
+
const value = recordMarksSubagent(firstJsonDocument(file));
|
|
415
|
+
SUBAGENT_MARKER_CACHE.set(file, { fingerprint, value });
|
|
416
|
+
if (SUBAGENT_MARKER_CACHE.size > MAX_METADATA_CACHE_ENTRIES) {
|
|
417
|
+
SUBAGENT_MARKER_CACHE.delete(SUBAGENT_MARKER_CACHE.keys().next().value);
|
|
418
|
+
}
|
|
419
|
+
return value;
|
|
420
|
+
}
|
|
421
|
+
function storeFingerprint(file) {
|
|
422
|
+
return [file, `${file}-wal`]
|
|
423
|
+
.map((candidate) => {
|
|
424
|
+
const stat = safeStat(candidate);
|
|
425
|
+
return stat?.isFile() ? `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}` : '-';
|
|
426
|
+
})
|
|
427
|
+
.join('|');
|
|
428
|
+
}
|
|
429
|
+
function cachedStoreDescriptors(file, load) {
|
|
430
|
+
const fingerprint = storeFingerprint(file);
|
|
431
|
+
const cached = STORE_DESCRIPTOR_CACHE.get(file);
|
|
432
|
+
if (cached?.fingerprint === fingerprint)
|
|
433
|
+
return cached.descriptors;
|
|
434
|
+
const descriptors = load();
|
|
435
|
+
STORE_DESCRIPTOR_CACHE.set(file, { fingerprint, descriptors });
|
|
436
|
+
return descriptors;
|
|
437
|
+
}
|
|
438
|
+
function writeFileIfChanged(file, contents) {
|
|
439
|
+
try {
|
|
440
|
+
const stat = fs.statSync(file);
|
|
441
|
+
if (stat.size === Buffer.byteLength(contents) && fs.readFileSync(file, 'utf8') === contents)
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
catch { }
|
|
445
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
446
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
447
|
+
fs.writeFileSync(temporary, contents, { mode: 0o600 });
|
|
448
|
+
fs.renameSync(temporary, file);
|
|
449
|
+
}
|
|
450
|
+
function serializableRow(row) {
|
|
451
|
+
return Object.fromEntries(Object.entries(row).map(([key, value]) => [
|
|
452
|
+
key,
|
|
453
|
+
value instanceof Uint8Array
|
|
454
|
+
? { __blob__: Buffer.from(value).toString('base64'), bytes: value.byteLength }
|
|
455
|
+
: typeof value === 'bigint'
|
|
456
|
+
? value.toString()
|
|
457
|
+
: value,
|
|
458
|
+
]));
|
|
459
|
+
}
|
|
460
|
+
function projectedDescriptor(harness, nativeSessionId, sourcePath, rows, metadata = {}) {
|
|
461
|
+
if (!rows.length)
|
|
462
|
+
return null;
|
|
463
|
+
const directory = path.join(PROJECTION_ROOT, harness);
|
|
464
|
+
const cacheName = `${crypto.createHash('sha256').update(nativeSessionId).digest('hex').slice(0, 24)}.jsonl`;
|
|
465
|
+
const file = path.join(directory, cacheName);
|
|
466
|
+
const contents = `${rows.map((row) => JSON.stringify(serializableRow(row))).join('\n')}\n`;
|
|
467
|
+
writeFileIfChanged(file, contents);
|
|
468
|
+
const stat = fs.statSync(file);
|
|
469
|
+
const created = metadata.createdAt ? new Date(metadata.createdAt) : stat.birthtime;
|
|
470
|
+
return {
|
|
471
|
+
harness,
|
|
472
|
+
agent: harness,
|
|
473
|
+
file,
|
|
474
|
+
sourcePath,
|
|
475
|
+
nativeSessionId,
|
|
476
|
+
size: stat.size,
|
|
477
|
+
mtimeMs: Number(metadata.updatedAtMs) || stat.mtimeMs,
|
|
478
|
+
createdAt: Number.isNaN(+created) ? stat.birthtime.toISOString() : created.toISOString(),
|
|
479
|
+
...(metadata.workingDirectory ? { workingDirectory: String(metadata.workingDirectory) } : {}),
|
|
480
|
+
...(metadata.title ? { nativeTitle: String(metadata.title) } : {}),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function gooseDescriptors() {
|
|
484
|
+
const file = path.join(HOME, '.local', 'share', 'goose', 'sessions', 'sessions.db');
|
|
485
|
+
if (!safeStat(file)?.isFile())
|
|
486
|
+
return [];
|
|
487
|
+
return cachedStoreDescriptors(file, () => {
|
|
488
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
489
|
+
try {
|
|
490
|
+
const sessions = db
|
|
491
|
+
.prepare(`
|
|
492
|
+
SELECT * FROM sessions
|
|
493
|
+
WHERE parent_session_id IS NULL OR parent_session_id = ''
|
|
494
|
+
ORDER BY created_at, id
|
|
495
|
+
`)
|
|
496
|
+
.all();
|
|
497
|
+
const messages = db.prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY id');
|
|
498
|
+
return sessions.flatMap((session) => {
|
|
499
|
+
const id = String(session.id || '').trim();
|
|
500
|
+
if (!id)
|
|
501
|
+
return [];
|
|
502
|
+
const descriptor = projectedDescriptor('goose', id, `~/.local/share/goose/sessions/sessions.db#${encodeURIComponent(id)}`, messages.all(id), {
|
|
503
|
+
createdAt: session.created_at,
|
|
504
|
+
updatedAtMs: Date.parse(String(session.updated_at || '')),
|
|
505
|
+
workingDirectory: session.working_dir,
|
|
506
|
+
title: session.description || session.name,
|
|
507
|
+
});
|
|
508
|
+
return descriptor ? [descriptor] : [];
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
db.close();
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
function kiloDescriptors() {
|
|
517
|
+
const file = path.join(HOME, '.local', 'share', 'kilo', 'kilo.db');
|
|
518
|
+
if (!safeStat(file)?.isFile())
|
|
519
|
+
return [];
|
|
520
|
+
return cachedStoreDescriptors(file, () => {
|
|
521
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
522
|
+
try {
|
|
523
|
+
const sessions = db.prepare('SELECT * FROM session WHERE parent_id IS NULL ORDER BY time_created, id').all();
|
|
524
|
+
const events = db.prepare('SELECT * FROM event WHERE aggregate_id = ? ORDER BY seq');
|
|
525
|
+
return sessions.flatMap((session) => {
|
|
526
|
+
const id = String(session.id || '').trim();
|
|
527
|
+
if (!id)
|
|
528
|
+
return [];
|
|
529
|
+
const descriptor = projectedDescriptor('kilo', id, `~/.local/share/kilo/kilo.db#${encodeURIComponent(id)}`, events.all(id), {
|
|
530
|
+
createdAt: Number(session.time_created) || undefined,
|
|
531
|
+
updatedAtMs: Number(session.time_updated) || undefined,
|
|
532
|
+
workingDirectory: session.directory,
|
|
533
|
+
title: session.title,
|
|
534
|
+
});
|
|
535
|
+
return descriptor ? [descriptor] : [];
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
finally {
|
|
539
|
+
db.close();
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function opencodeDescriptors() {
|
|
544
|
+
const candidates = [
|
|
545
|
+
path.join(HOME, '.local', 'share', 'opencode', 'opencode.db'),
|
|
546
|
+
path.join(HOME, '.local', 'state', 'opencode', 'opencode.db'),
|
|
547
|
+
];
|
|
548
|
+
const file = candidates.find((candidate) => safeStat(candidate)?.isFile());
|
|
549
|
+
if (!file)
|
|
550
|
+
return [];
|
|
551
|
+
return cachedStoreDescriptors(file, () => {
|
|
552
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
553
|
+
try {
|
|
554
|
+
const sessions = db.prepare('SELECT * FROM session WHERE parent_id IS NULL ORDER BY time_created, id').all();
|
|
555
|
+
const events = db.prepare('SELECT * FROM event WHERE aggregate_id = ? ORDER BY seq');
|
|
556
|
+
return sessions.flatMap((session) => {
|
|
557
|
+
const id = String(session.id || '');
|
|
558
|
+
if (!id)
|
|
559
|
+
return [];
|
|
560
|
+
const descriptor = projectedDescriptor('opencode', id, `${displayPath(file)}#${encodeURIComponent(id)}`, events.all(id), {
|
|
561
|
+
createdAt: Number(session.time_created) || undefined,
|
|
562
|
+
updatedAtMs: Number(session.time_updated) || undefined,
|
|
563
|
+
workingDirectory: session.directory,
|
|
564
|
+
title: session.title,
|
|
565
|
+
});
|
|
566
|
+
return descriptor ? [descriptor] : [];
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
db.close();
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function devinDescriptors() {
|
|
575
|
+
const file = path.join(HOME, '.local', 'share', 'devin', 'cli', 'sessions.db');
|
|
576
|
+
if (!safeStat(file)?.isFile())
|
|
577
|
+
return [];
|
|
578
|
+
return cachedStoreDescriptors(file, () => {
|
|
579
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
580
|
+
try {
|
|
581
|
+
const sessions = db.prepare('SELECT * FROM sessions WHERE hidden = 0 ORDER BY created_at, id').all();
|
|
582
|
+
const nodes = db.prepare('SELECT * FROM message_nodes WHERE session_id = ? ORDER BY row_id');
|
|
583
|
+
const tools = db.prepare('SELECT * FROM tool_call_state WHERE session_id = ? ORDER BY tool_call_id');
|
|
584
|
+
return sessions.flatMap((session) => {
|
|
585
|
+
const id = String(session.id || '');
|
|
586
|
+
if (!id)
|
|
587
|
+
return [];
|
|
588
|
+
const rows = [
|
|
589
|
+
...nodes.all(id).map((node) => ({
|
|
590
|
+
type: 'devin.sqlite.message',
|
|
591
|
+
session,
|
|
592
|
+
node: {
|
|
593
|
+
...node,
|
|
594
|
+
chat_message: (() => {
|
|
595
|
+
try {
|
|
596
|
+
return JSON.parse(String(node.chat_message));
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
return node.chat_message;
|
|
600
|
+
}
|
|
601
|
+
})(),
|
|
602
|
+
},
|
|
603
|
+
})),
|
|
604
|
+
...tools.all(id).map((tool) => ({ type: 'devin.sqlite.tool', session, tool })),
|
|
605
|
+
];
|
|
606
|
+
const descriptor = projectedDescriptor('devin', id, `${displayPath(file)}#${encodeURIComponent(id)}`, rows, {
|
|
607
|
+
createdAt: Number(session.created_at) * 1000 || undefined,
|
|
608
|
+
updatedAtMs: Number(session.last_activity_at) * 1000 || undefined,
|
|
609
|
+
workingDirectory: session.working_directory,
|
|
610
|
+
title: session.title,
|
|
611
|
+
});
|
|
612
|
+
return descriptor ? [descriptor] : [];
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
finally {
|
|
616
|
+
db.close();
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function hermesDescriptors() {
|
|
621
|
+
const file = path.join(HOME, '.hermes', 'state.db');
|
|
622
|
+
if (!safeStat(file)?.isFile())
|
|
623
|
+
return [];
|
|
624
|
+
return cachedStoreDescriptors(file, () => {
|
|
625
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
626
|
+
try {
|
|
627
|
+
const sessions = db
|
|
628
|
+
.prepare("SELECT * FROM sessions WHERE parent_session_id IS NULL OR parent_session_id = '' ORDER BY started_at, id")
|
|
629
|
+
.all();
|
|
630
|
+
const messages = db.prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY id');
|
|
631
|
+
return sessions.flatMap((session) => {
|
|
632
|
+
const id = String(session.id || '');
|
|
633
|
+
if (!id)
|
|
634
|
+
return [];
|
|
635
|
+
const descriptor = projectedDescriptor('hermes', id, `${displayPath(file)}#${encodeURIComponent(id)}`, messages.all(id), {
|
|
636
|
+
createdAt: Number(session.started_at) * 1000 || undefined,
|
|
637
|
+
updatedAtMs: Number(session.last_activity_at) * 1000 || undefined,
|
|
638
|
+
workingDirectory: session.cwd,
|
|
639
|
+
title: session.title,
|
|
640
|
+
});
|
|
641
|
+
return descriptor ? [descriptor] : [];
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
finally {
|
|
645
|
+
db.close();
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
export function openclawAcpWorkingDirectory(file, sessionKey = '') {
|
|
650
|
+
if (!String(sessionKey).includes(':acp-bridge:'))
|
|
651
|
+
return '';
|
|
652
|
+
for (const line of filePrefix(file, 128 * 1024).split('\n')) {
|
|
653
|
+
if (!line)
|
|
654
|
+
continue;
|
|
655
|
+
try {
|
|
656
|
+
const record = JSON.parse(line);
|
|
657
|
+
const message = record?.message;
|
|
658
|
+
if (record?.type !== 'message' || message?.role !== 'user' || typeof message?.content !== 'string')
|
|
659
|
+
continue;
|
|
660
|
+
const match = /^\[Working directory: ([^\]\n]+)\]\n/u.exec(message.content);
|
|
661
|
+
const directory = String(match?.[1] || '').trim();
|
|
662
|
+
if (path.isAbsolute(directory))
|
|
663
|
+
return path.resolve(directory);
|
|
664
|
+
}
|
|
665
|
+
catch { }
|
|
666
|
+
}
|
|
667
|
+
return '';
|
|
668
|
+
}
|
|
669
|
+
export function openclawNativeSessionId(sessionKey = '') {
|
|
670
|
+
return /^agent:[^:]+:acp-bridge:(.+)$/u.exec(String(sessionKey))?.[1] || String(sessionKey);
|
|
671
|
+
}
|
|
672
|
+
function openclawDescriptors() {
|
|
673
|
+
const sessionsRoot = path.join(HOME, '.openclaw', 'agents', 'main', 'sessions');
|
|
674
|
+
const indexFile = path.join(sessionsRoot, 'sessions.json');
|
|
675
|
+
let index;
|
|
676
|
+
try {
|
|
677
|
+
index = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
return [];
|
|
681
|
+
}
|
|
682
|
+
return Object.entries(index || {}).flatMap(([sessionKey, metadata]) => {
|
|
683
|
+
const file = String(metadata?.sessionFile || path.join(sessionsRoot, `${metadata?.sessionId || ''}.jsonl`));
|
|
684
|
+
const descriptor = fileDescriptor('openclaw', file);
|
|
685
|
+
if (!descriptor)
|
|
686
|
+
return [];
|
|
687
|
+
const report = metadata?.systemPromptReport || {};
|
|
688
|
+
return [
|
|
689
|
+
{
|
|
690
|
+
...descriptor,
|
|
691
|
+
sourcePath: `~/.openclaw/agents/main/sessions#${encodeURIComponent(sessionKey)}`,
|
|
692
|
+
nativeSessionId: openclawNativeSessionId(sessionKey),
|
|
693
|
+
// The Gateway runs in its agent workspace and records that as the
|
|
694
|
+
// native cwd. Its ACP bridge preserves the client cwd in the trusted
|
|
695
|
+
// prompt prefix instead; use that for scoped daemon ownership.
|
|
696
|
+
workingDirectory: openclawAcpWorkingDirectory(file, sessionKey) || report.workspaceDir || metadata?.cwd,
|
|
697
|
+
nativeTitle: metadata?.title || report?.sessionKey || sessionKey,
|
|
698
|
+
createdAt: Number(metadata?.sessionStartedAt)
|
|
699
|
+
? new Date(Number(metadata.sessionStartedAt)).toISOString()
|
|
700
|
+
: descriptor.createdAt,
|
|
701
|
+
mtimeMs: Number(metadata?.updatedAt) || descriptor.mtimeMs,
|
|
702
|
+
},
|
|
703
|
+
];
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
function filePrefix(file, maxBytes) {
|
|
707
|
+
let handle;
|
|
708
|
+
try {
|
|
709
|
+
const stat = fs.statSync(file);
|
|
710
|
+
const bytesToRead = Math.min(stat.size, maxBytes);
|
|
711
|
+
handle = fs.openSync(file, 'r');
|
|
712
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
713
|
+
const bytesRead = fs.readSync(handle, buffer, 0, buffer.length, 0);
|
|
714
|
+
return buffer
|
|
715
|
+
.subarray(0, bytesRead)
|
|
716
|
+
.toString('utf8')
|
|
717
|
+
.replace(/^\uFEFF/u, '');
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
return '';
|
|
721
|
+
}
|
|
722
|
+
finally {
|
|
723
|
+
try {
|
|
724
|
+
if (handle !== undefined)
|
|
725
|
+
fs.closeSync(handle);
|
|
726
|
+
}
|
|
727
|
+
catch { }
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function recordPrefix(file, limit, maxBytes) {
|
|
731
|
+
let handle;
|
|
732
|
+
try {
|
|
733
|
+
const stat = fs.statSync(file);
|
|
734
|
+
const bytesToRead = Math.min(stat.size, maxBytes);
|
|
735
|
+
handle = fs.openSync(file, 'r');
|
|
736
|
+
const chunks = [];
|
|
737
|
+
let offset = 0;
|
|
738
|
+
let newlines = 0;
|
|
739
|
+
while (offset < bytesToRead && newlines < limit) {
|
|
740
|
+
const buffer = Buffer.alloc(Math.min(64 * 1024, bytesToRead - offset));
|
|
741
|
+
const bytesRead = fs.readSync(handle, buffer, 0, buffer.length, offset);
|
|
742
|
+
if (!bytesRead)
|
|
743
|
+
break;
|
|
744
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
745
|
+
chunks.push(chunk);
|
|
746
|
+
for (const byte of chunk)
|
|
747
|
+
if (byte === 0x0a)
|
|
748
|
+
newlines += 1;
|
|
749
|
+
offset += bytesRead;
|
|
750
|
+
}
|
|
751
|
+
return Buffer.concat(chunks)
|
|
752
|
+
.toString('utf8')
|
|
753
|
+
.replace(/^\uFEFF/u, '');
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
return '';
|
|
757
|
+
}
|
|
758
|
+
finally {
|
|
759
|
+
try {
|
|
760
|
+
if (handle !== undefined)
|
|
761
|
+
fs.closeSync(handle);
|
|
762
|
+
}
|
|
763
|
+
catch { }
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
function firstRecords(descriptor, limit = 32, maxBytes = 1024 * 1024) {
|
|
767
|
+
try {
|
|
768
|
+
const stat = fs.statSync(descriptor.file);
|
|
769
|
+
const isJsonDocument = descriptor.file.endsWith('.json');
|
|
770
|
+
const text = isJsonDocument
|
|
771
|
+
? filePrefix(descriptor.file, stat.size)
|
|
772
|
+
: recordPrefix(descriptor.file, limit, maxBytes);
|
|
773
|
+
if (isJsonDocument) {
|
|
774
|
+
const value = JSON.parse(text);
|
|
775
|
+
const values = Array.isArray(value) ? value.slice(0, limit) : [value];
|
|
776
|
+
return values.flatMap((candidate) => {
|
|
777
|
+
const object = jsonObject(candidate);
|
|
778
|
+
return object ? [object] : [];
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return text
|
|
782
|
+
.split(/\r?\n/u)
|
|
783
|
+
.filter(Boolean)
|
|
784
|
+
.slice(0, limit)
|
|
785
|
+
.flatMap((line) => {
|
|
786
|
+
try {
|
|
787
|
+
const object = jsonObject(JSON.parse(line));
|
|
788
|
+
return object ? [object] : [];
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
return [];
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
return [];
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
/** Native identity and parentage carried by a bounded transcript prefix. */
|
|
800
|
+
export function nativeDescriptorLineage(descriptor) {
|
|
801
|
+
const sourcePath = String(descriptor?.sourcePath || descriptor?.file || '').replaceAll('\\', '/');
|
|
802
|
+
const stat = safeStat(String(descriptor?.file || ''));
|
|
803
|
+
const metadataStat = safeStat(claudeSubagentMetadataFile(descriptor));
|
|
804
|
+
const fingerprint = [stat, metadataStat]
|
|
805
|
+
.map((candidate) => candidate?.isFile() ? `${candidate.dev}:${candidate.ino}:${candidate.size}:${candidate.mtimeMs}` : '-')
|
|
806
|
+
.join('|');
|
|
807
|
+
const cacheKey = [
|
|
808
|
+
descriptor?.harness,
|
|
809
|
+
sourcePath,
|
|
810
|
+
descriptor?.nativeSessionId,
|
|
811
|
+
descriptor?.parentNativeSessionId,
|
|
812
|
+
descriptor?.isSubagent,
|
|
813
|
+
descriptor?.threadSource,
|
|
814
|
+
].join('\0');
|
|
815
|
+
const cached = LINEAGE_CACHE.get(cacheKey);
|
|
816
|
+
if (cached?.fingerprint === fingerprint) {
|
|
817
|
+
LINEAGE_CACHE.delete(cacheKey);
|
|
818
|
+
LINEAGE_CACHE.set(cacheKey, cached);
|
|
819
|
+
return cached.value;
|
|
820
|
+
}
|
|
821
|
+
const structural = nativeSubagentPathLineage(descriptor?.harness, sourcePath);
|
|
822
|
+
const claudeMetadata = claudeSubagentMetadata(descriptor, structural);
|
|
823
|
+
let nativeSessionId = structural?.nativeSessionId || descriptorNativeSessionId(descriptor);
|
|
824
|
+
let parentNativeSessionId = String(descriptor?.parentNativeSessionId || claudeMetadata?.parentAgentId || structural?.parentNativeSessionId || '').trim();
|
|
825
|
+
const parentSourcePath = claudeMetadata?.parentSourcePath || structural?.parentSourcePath || '';
|
|
826
|
+
let isSubagent = Boolean(structural) || nativeDescriptorIsSubagent(descriptor);
|
|
827
|
+
const needsRecords = !nativeSessionId || (isSubagent && !parentNativeSessionId && !parentSourcePath);
|
|
828
|
+
for (const record of needsRecords ? firstRecords(descriptor) : []) {
|
|
829
|
+
const payload = parsedObject(record?.payload);
|
|
830
|
+
const session = parsedObject(record?.session);
|
|
831
|
+
const source = parsedObject(payload?.source) || parsedObject(record?.source);
|
|
832
|
+
const subagent = parsedObject(source?.subagent) || parsedObject(source?.sub_agent) || parsedObject(source?.delegate);
|
|
833
|
+
const spawn = parsedObject(subagent?.thread_spawn) || parsedObject(subagent?.threadSpawn) || subagent;
|
|
834
|
+
const parent = [
|
|
835
|
+
spawn?.parent_thread_id,
|
|
836
|
+
spawn?.parentThreadId,
|
|
837
|
+
payload?.parent_session_id,
|
|
838
|
+
payload?.parentSessionId,
|
|
839
|
+
payload?.parentSession,
|
|
840
|
+
session?.parent_session_id,
|
|
841
|
+
session?.parentSessionId,
|
|
842
|
+
session?.parentSession,
|
|
843
|
+
record?.parent_session_id,
|
|
844
|
+
record?.parentSessionId,
|
|
845
|
+
record?.parentSession,
|
|
846
|
+
].find((value) => typeof value === 'string' && value.trim());
|
|
847
|
+
if (!parentNativeSessionId && parent)
|
|
848
|
+
parentNativeSessionId = parent.trim();
|
|
849
|
+
if (!nativeSessionId) {
|
|
850
|
+
const own = [
|
|
851
|
+
payload?.id,
|
|
852
|
+
payload?.sessionId,
|
|
853
|
+
session?.id,
|
|
854
|
+
record?.sessionId,
|
|
855
|
+
record?.session_id,
|
|
856
|
+
record?.conversationId,
|
|
857
|
+
record?.conversation_id,
|
|
858
|
+
record?.threadId,
|
|
859
|
+
record?.id,
|
|
860
|
+
].find((value) => typeof value === 'string' && value.trim());
|
|
861
|
+
if (own)
|
|
862
|
+
nativeSessionId = own.trim();
|
|
863
|
+
}
|
|
864
|
+
if ((descriptor?.harness === 'claude' || descriptor?.harness === 'codebuddy') && record?.isSidechain === true) {
|
|
865
|
+
isSubagent = true;
|
|
866
|
+
const agentId = String(record.agentId || '').trim();
|
|
867
|
+
if (agentId)
|
|
868
|
+
nativeSessionId = agentId;
|
|
869
|
+
const parentSession = String(record.sessionId || '').trim();
|
|
870
|
+
if (!parentNativeSessionId && parentSession)
|
|
871
|
+
parentNativeSessionId = parentSession;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (parentNativeSessionId)
|
|
875
|
+
isSubagent = true;
|
|
876
|
+
const value = {
|
|
877
|
+
nativeSessionId,
|
|
878
|
+
parentNativeSessionId,
|
|
879
|
+
isSubagent,
|
|
880
|
+
...(structural?.uploadable === false ? { uploadable: false } : {}),
|
|
881
|
+
...(parentSourcePath ? { parentSourcePath } : {}),
|
|
882
|
+
...(claudeMetadata?.description ? { nativeTitle: claudeMetadata.description } : {}),
|
|
883
|
+
...(claudeMetadata?.agentType ? { nativeSubagentType: claudeMetadata.agentType } : {}),
|
|
884
|
+
...(claudeMetadata?.spawnDepth ? { nativeSubagentDepth: claudeMetadata.spawnDepth } : {}),
|
|
885
|
+
};
|
|
886
|
+
LINEAGE_CACHE.set(cacheKey, { fingerprint, value });
|
|
887
|
+
if (LINEAGE_CACHE.size > MAX_METADATA_CACHE_ENTRIES) {
|
|
888
|
+
LINEAGE_CACHE.delete(LINEAGE_CACHE.keys().next().value);
|
|
889
|
+
}
|
|
890
|
+
return value;
|
|
891
|
+
}
|
|
892
|
+
function sidecarJson(descriptor, basename) {
|
|
893
|
+
const transcript = path.resolve(descriptor.file);
|
|
894
|
+
if (!transcript)
|
|
895
|
+
return null;
|
|
896
|
+
return jsonFile(path.join(path.dirname(transcript), basename));
|
|
897
|
+
}
|
|
898
|
+
function jsonFile(file) {
|
|
899
|
+
try {
|
|
900
|
+
const stat = fs.statSync(file);
|
|
901
|
+
const fingerprint = `${stat.size}:${stat.mtimeMs}`;
|
|
902
|
+
const cached = JSON_FILE_CACHE.get(file);
|
|
903
|
+
if (cached?.fingerprint === fingerprint)
|
|
904
|
+
return cached.value;
|
|
905
|
+
const value = jsonObject(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
906
|
+
if (!value)
|
|
907
|
+
return null;
|
|
908
|
+
JSON_FILE_CACHE.set(file, { fingerprint, value });
|
|
909
|
+
return value;
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function absolutePathCandidate(values) {
|
|
916
|
+
for (const value of values) {
|
|
917
|
+
if (typeof value === 'string' && path.isAbsolute(value.trim()))
|
|
918
|
+
return value.trim();
|
|
919
|
+
}
|
|
920
|
+
return '';
|
|
921
|
+
}
|
|
922
|
+
function clineMetadataWorkingDirectory(descriptor) {
|
|
923
|
+
const transcript = path.resolve(descriptor.file);
|
|
924
|
+
const metadataFile = transcript.endsWith('.messages.json')
|
|
925
|
+
? `${transcript.slice(0, -'.messages.json'.length)}.json`
|
|
926
|
+
: '';
|
|
927
|
+
const metadata = metadataFile ? jsonFile(metadataFile) : null;
|
|
928
|
+
return absolutePathCandidate([metadata?.cwd, metadata?.workspace_root, metadata?.workspaceRoot]);
|
|
929
|
+
}
|
|
930
|
+
function cursorMetadataWorkingDirectory(descriptor) {
|
|
931
|
+
const transcript = path.resolve(descriptor.file);
|
|
932
|
+
const marker = `${path.sep}agent-transcripts${path.sep}`;
|
|
933
|
+
const markerIndex = transcript.lastIndexOf(marker);
|
|
934
|
+
if (markerIndex < 0)
|
|
935
|
+
return '';
|
|
936
|
+
const projectDirectory = transcript.slice(0, markerIndex);
|
|
937
|
+
const trust = jsonFile(path.join(projectDirectory, '.workspace-trusted'));
|
|
938
|
+
const trusted = absolutePathCandidate([trust?.workspacePath, trust?.workspace_path, trust?.cwd]);
|
|
939
|
+
if (trusted)
|
|
940
|
+
return trusted;
|
|
941
|
+
const workerLog = path.join(projectDirectory, 'worker.log');
|
|
942
|
+
const workerStat = safeStat(workerLog);
|
|
943
|
+
const workerFingerprint = workerStat ? `${workerStat.size}:${workerStat.mtimeMs}` : '';
|
|
944
|
+
const cachedWorker = CURSOR_WORKER_CWD_CACHE.get(workerLog);
|
|
945
|
+
if (cachedWorker?.fingerprint === workerFingerprint)
|
|
946
|
+
return cachedWorker.cwd;
|
|
947
|
+
const logged = /(?:^|\s)workspacePath=(\/[^\s]+)/u.exec(filePrefix(workerLog, 1024 * 1024))?.[1];
|
|
948
|
+
const cwd = logged && path.isAbsolute(logged) ? logged : '';
|
|
949
|
+
CURSOR_WORKER_CWD_CACHE.set(workerLog, { fingerprint: workerFingerprint, cwd });
|
|
950
|
+
return cwd;
|
|
951
|
+
}
|
|
952
|
+
function kimiMetadataWorkingDirectory(descriptor) {
|
|
953
|
+
const transcriptDirectory = path.dirname(path.resolve(descriptor.file));
|
|
954
|
+
const metadata = jsonFile(path.resolve(transcriptDirectory, '..', '..', 'state.json'));
|
|
955
|
+
const stateCandidate = absolutePathCandidate([
|
|
956
|
+
metadata?.cwd,
|
|
957
|
+
metadata?.workDir,
|
|
958
|
+
metadata?.working_directory,
|
|
959
|
+
metadata?.workingDirectory,
|
|
960
|
+
]);
|
|
961
|
+
if (stateCandidate)
|
|
962
|
+
return stateCandidate;
|
|
963
|
+
const transcript = path.resolve(descriptor.file);
|
|
964
|
+
const marker = `${path.sep}sessions${path.sep}`;
|
|
965
|
+
const markerIndex = transcript.lastIndexOf(marker);
|
|
966
|
+
if (markerIndex < 0)
|
|
967
|
+
return '';
|
|
968
|
+
const workspaceId = transcript.slice(markerIndex + marker.length).split(path.sep, 1)[0];
|
|
969
|
+
const dataDirectory = transcript.slice(0, markerIndex);
|
|
970
|
+
const workspaces = jsonFile(path.join(dataDirectory, 'workspaces.json'));
|
|
971
|
+
const workspaceEntries = jsonObject(workspaces?.workspaces);
|
|
972
|
+
const workspace = jsonObject(workspaceEntries?.[workspaceId]);
|
|
973
|
+
return absolutePathCandidate([workspace?.root]);
|
|
974
|
+
}
|
|
975
|
+
function geminiMetadataWorkingDirectory(descriptor) {
|
|
976
|
+
const transcript = path.resolve(descriptor.file);
|
|
977
|
+
if (path.basename(path.dirname(transcript)) !== 'chats')
|
|
978
|
+
return '';
|
|
979
|
+
try {
|
|
980
|
+
const candidate = fs
|
|
981
|
+
.readFileSync(path.join(path.dirname(path.dirname(transcript)), '.project_root'), 'utf8')
|
|
982
|
+
.trim();
|
|
983
|
+
return path.isAbsolute(candidate) ? candidate : '';
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
return '';
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function antigravityMetadataWorkingDirectory(descriptor) {
|
|
990
|
+
const transcript = path.resolve(descriptor.file);
|
|
991
|
+
const marker = `${path.sep}brain${path.sep}`;
|
|
992
|
+
const markerIndex = transcript.lastIndexOf(marker);
|
|
993
|
+
if (markerIndex < 0)
|
|
994
|
+
return '';
|
|
995
|
+
const relative = transcript.slice(markerIndex + marker.length);
|
|
996
|
+
const conversationId = relative.split(path.sep, 1)[0];
|
|
997
|
+
const appDirectory = transcript.slice(0, markerIndex);
|
|
998
|
+
const metadata = jsonFile(path.join(appDirectory, 'cache', 'conversation_metadata.json'));
|
|
999
|
+
const conversations = jsonObject(metadata?.conversations);
|
|
1000
|
+
const conversation = jsonObject(conversations?.[conversationId]);
|
|
1001
|
+
const summary = jsonObject(conversation?.summary);
|
|
1002
|
+
const uris = summary?.WorkspaceURIs;
|
|
1003
|
+
if (Array.isArray(uris)) {
|
|
1004
|
+
for (const uri of uris) {
|
|
1005
|
+
if (typeof uri !== 'string' || !uri.startsWith('file://'))
|
|
1006
|
+
continue;
|
|
1007
|
+
try {
|
|
1008
|
+
const candidate = decodeURIComponent(new URL(uri).pathname);
|
|
1009
|
+
if (path.isAbsolute(candidate))
|
|
1010
|
+
return candidate;
|
|
1011
|
+
}
|
|
1012
|
+
catch { }
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
const transcriptStat = safeStat(transcript);
|
|
1016
|
+
const transcriptFingerprint = transcriptStat ? `${transcriptStat.size}:${transcriptStat.mtimeMs}` : '';
|
|
1017
|
+
const cachedTranscript = ANTIGRAVITY_TRANSCRIPT_CWD_CACHE.get(transcript);
|
|
1018
|
+
if (cachedTranscript?.fingerprint === transcriptFingerprint)
|
|
1019
|
+
return cachedTranscript.cwd;
|
|
1020
|
+
const findCwd = (value) => {
|
|
1021
|
+
const object = jsonObject(value);
|
|
1022
|
+
if (!object)
|
|
1023
|
+
return '';
|
|
1024
|
+
const direct = absolutePathCandidate([object.Cwd, object.cwd]);
|
|
1025
|
+
if (direct)
|
|
1026
|
+
return direct;
|
|
1027
|
+
for (const child of Object.values(object)) {
|
|
1028
|
+
const nested = findCwd(child);
|
|
1029
|
+
if (nested)
|
|
1030
|
+
return nested;
|
|
1031
|
+
}
|
|
1032
|
+
return '';
|
|
1033
|
+
};
|
|
1034
|
+
for (const record of firstRecords(descriptor, 256, 4 * 1024 * 1024)) {
|
|
1035
|
+
const candidate = findCwd(record);
|
|
1036
|
+
if (candidate) {
|
|
1037
|
+
ANTIGRAVITY_TRANSCRIPT_CWD_CACHE.set(transcript, { fingerprint: transcriptFingerprint, cwd: candidate });
|
|
1038
|
+
return candidate;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
ANTIGRAVITY_TRANSCRIPT_CWD_CACHE.set(transcript, { fingerprint: transcriptFingerprint, cwd: '' });
|
|
1042
|
+
return '';
|
|
1043
|
+
}
|
|
1044
|
+
function museMetadataWorkingDirectory(descriptor) {
|
|
1045
|
+
let transcript = path.resolve(descriptor.file);
|
|
1046
|
+
const seen = new Set();
|
|
1047
|
+
for (let depth = 0; depth < 16 && transcript && !seen.has(transcript); depth += 1) {
|
|
1048
|
+
seen.add(transcript);
|
|
1049
|
+
const record = firstRecords({ file: transcript }).find((candidate) => candidate.payload_type === 'runtime.session.metadata');
|
|
1050
|
+
const payload = jsonObject(record?.payload);
|
|
1051
|
+
const metadata = jsonObject(payload?.record);
|
|
1052
|
+
const candidate = absolutePathCandidate([metadata?.workspace_root, metadata?.cwd]);
|
|
1053
|
+
if (!candidate)
|
|
1054
|
+
return '';
|
|
1055
|
+
const parentTranscript = path.join(candidate, 'session.jsonl');
|
|
1056
|
+
const parentSessionId = path.basename(candidate);
|
|
1057
|
+
if (!new RegExp(`^${OPAQUE_UUID_SOURCE}$`, 'iu').test(parentSessionId) || !safeStat(parentTranscript)?.isFile())
|
|
1058
|
+
return candidate;
|
|
1059
|
+
transcript = parentTranscript;
|
|
1060
|
+
}
|
|
1061
|
+
return '';
|
|
1062
|
+
}
|
|
1063
|
+
/** Bounded metadata inference owned by Turns and shared by uploader/queue. */
|
|
1064
|
+
export function nativeDescriptorWorkingDirectory(descriptor) {
|
|
1065
|
+
if (descriptor.workingDirectory?.trim())
|
|
1066
|
+
return descriptor.workingDirectory.trim();
|
|
1067
|
+
const metadataResolvers = {
|
|
1068
|
+
antigravity: antigravityMetadataWorkingDirectory,
|
|
1069
|
+
cline: clineMetadataWorkingDirectory,
|
|
1070
|
+
cursor: cursorMetadataWorkingDirectory,
|
|
1071
|
+
gemini: geminiMetadataWorkingDirectory,
|
|
1072
|
+
kimi: kimiMetadataWorkingDirectory,
|
|
1073
|
+
};
|
|
1074
|
+
const metadataCandidate = metadataResolvers[descriptor.harness]?.(descriptor) || '';
|
|
1075
|
+
if (metadataCandidate)
|
|
1076
|
+
return metadataCandidate;
|
|
1077
|
+
if (descriptor.harness === 'vibe') {
|
|
1078
|
+
const metadata = sidecarJson(descriptor, 'meta.json');
|
|
1079
|
+
const environment = jsonObject(metadata?.environment);
|
|
1080
|
+
const candidate = absolutePathCandidate([
|
|
1081
|
+
environment?.working_directory,
|
|
1082
|
+
environment?.workingDirectory,
|
|
1083
|
+
metadata?.working_directory,
|
|
1084
|
+
metadata?.workingDirectory,
|
|
1085
|
+
metadata?.cwd,
|
|
1086
|
+
]);
|
|
1087
|
+
if (candidate)
|
|
1088
|
+
return candidate;
|
|
1089
|
+
}
|
|
1090
|
+
if (descriptor.harness === 'muse') {
|
|
1091
|
+
const candidate = museMetadataWorkingDirectory(descriptor);
|
|
1092
|
+
if (candidate)
|
|
1093
|
+
return candidate;
|
|
1094
|
+
}
|
|
1095
|
+
if (descriptor.harness === 'grok') {
|
|
1096
|
+
// Grok namespaces native sessions under an URL-encoded working directory:
|
|
1097
|
+
// `~/.grok/sessions/%2Fhome%2Fkevin%2Fturns/<session>/...`. Resuming with
|
|
1098
|
+
// the transcript directory as cwd makes the ACP server search the wrong
|
|
1099
|
+
// namespace and report "Path not found".
|
|
1100
|
+
const sourcePath = String(descriptor.sourcePath || descriptor.file).replaceAll('\\', '/');
|
|
1101
|
+
const encoded = /(?:^|\/)\.grok\/sessions\/([^/]+)(?:\/|$)/u.exec(sourcePath)?.[1];
|
|
1102
|
+
if (encoded) {
|
|
1103
|
+
try {
|
|
1104
|
+
const decoded = decodeURIComponent(encoded);
|
|
1105
|
+
if (path.isAbsolute(decoded))
|
|
1106
|
+
return decoded;
|
|
1107
|
+
}
|
|
1108
|
+
catch { }
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
for (const record of firstRecords(descriptor)) {
|
|
1112
|
+
const payload = jsonObject(record.payload);
|
|
1113
|
+
const session = jsonObject(record.session);
|
|
1114
|
+
const meta = jsonObject(record.meta);
|
|
1115
|
+
const data = jsonObject(record.data);
|
|
1116
|
+
const context = jsonObject(data?.context);
|
|
1117
|
+
const sessionStart = jsonObject(record.session_start);
|
|
1118
|
+
const candidates = [
|
|
1119
|
+
record.cwd,
|
|
1120
|
+
record.working_directory,
|
|
1121
|
+
record.workingDirectory,
|
|
1122
|
+
record.workspace,
|
|
1123
|
+
payload?.cwd,
|
|
1124
|
+
session?.working_directory,
|
|
1125
|
+
session?.cwd,
|
|
1126
|
+
meta?.workdir,
|
|
1127
|
+
data?.cwd,
|
|
1128
|
+
context?.cwd,
|
|
1129
|
+
...(Array.isArray(record.working_directories) ? record.working_directories : []),
|
|
1130
|
+
...(Array.isArray(sessionStart?.working_directories) ? sessionStart.working_directories : []),
|
|
1131
|
+
];
|
|
1132
|
+
const found = absolutePathCandidate(candidates);
|
|
1133
|
+
if (found)
|
|
1134
|
+
return found;
|
|
1135
|
+
}
|
|
1136
|
+
return '';
|
|
1137
|
+
}
|
|
1138
|
+
/** Resolve the harness-native resumable session key from bounded native records
|
|
1139
|
+
* and structural path metadata. This is intentionally not the
|
|
1140
|
+
* turns.dev URL/session ID, which is machine-and-path stable. */
|
|
1141
|
+
export async function nativeDescriptorAgentSessionId(descriptor) {
|
|
1142
|
+
const subagent = nativeDescriptorIsSubagent(descriptor);
|
|
1143
|
+
// Child transcripts are observational sessions. Even when a harness exposes
|
|
1144
|
+
// a technically valid child thread ID, turns.dev never dispatches prompts
|
|
1145
|
+
// into work whose lifecycle and ownership belong to its parent agent.
|
|
1146
|
+
if (subagent)
|
|
1147
|
+
return '';
|
|
1148
|
+
const structural = descriptorNativeSessionId(descriptor);
|
|
1149
|
+
if (structural) {
|
|
1150
|
+
// Kimi persists its ACP key as `session_<uuid>`. The path parser used to
|
|
1151
|
+
// return only the captured UUID, which looks plausible but session/load
|
|
1152
|
+
// correctly rejects as an unknown ID.
|
|
1153
|
+
if (descriptor?.harness === 'kimi' && !structural.startsWith('session_'))
|
|
1154
|
+
return `session_${structural}`;
|
|
1155
|
+
return structural;
|
|
1156
|
+
}
|
|
1157
|
+
if (descriptor?.harness === 'muse') {
|
|
1158
|
+
for (const record of firstRecords(descriptor)) {
|
|
1159
|
+
const stream = jsonObject(record.stream);
|
|
1160
|
+
const sessionId = stream?.kind === 'session' ? String(stream.id || '') : '';
|
|
1161
|
+
if (sessionId)
|
|
1162
|
+
return sessionId;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
for (const record of firstRecords(descriptor)) {
|
|
1166
|
+
const payload = jsonObject(record.payload);
|
|
1167
|
+
const session = jsonObject(record.session);
|
|
1168
|
+
const stream = jsonObject(record.stream);
|
|
1169
|
+
const candidates = [
|
|
1170
|
+
record.sessionId,
|
|
1171
|
+
record.session_id,
|
|
1172
|
+
record.conversationId,
|
|
1173
|
+
record.conversation_id,
|
|
1174
|
+
record.threadId,
|
|
1175
|
+
record.id,
|
|
1176
|
+
payload?.id,
|
|
1177
|
+
payload?.sessionId,
|
|
1178
|
+
session?.id,
|
|
1179
|
+
stream?.kind === 'session' ? stream.id : '',
|
|
1180
|
+
];
|
|
1181
|
+
const found = candidates.find((value) => typeof value === 'string' && value.length >= 8);
|
|
1182
|
+
if (found)
|
|
1183
|
+
return found;
|
|
1184
|
+
}
|
|
1185
|
+
if (descriptor?.harness === 'cursor') {
|
|
1186
|
+
const cursorSessionId = /(?:^|\/)agent-transcripts\/([0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})(?:\/|$)/iu.exec(String(descriptor?.sourcePath || descriptor?.file || '').replaceAll('\\', '/'))?.[1];
|
|
1187
|
+
if (cursorSessionId)
|
|
1188
|
+
return cursorSessionId;
|
|
1189
|
+
}
|
|
1190
|
+
if (descriptor?.harness === 'vibe') {
|
|
1191
|
+
const transcript = path.resolve(String(descriptor?.file || ''));
|
|
1192
|
+
if (path.basename(transcript) === 'messages.jsonl' &&
|
|
1193
|
+
/^session_[0-9]{8}_[0-9]{6}_[0-9a-f]{8}$/iu.test(path.basename(path.dirname(transcript)))) {
|
|
1194
|
+
try {
|
|
1195
|
+
const metadata = sidecarJson(descriptor, 'meta.json');
|
|
1196
|
+
const vibeSessionId = String(metadata?.session_id || '').trim();
|
|
1197
|
+
// Vibe uses UUID-shaped opaque IDs but does not reserve the RFC version
|
|
1198
|
+
// and variant nibbles (for example `...-a328-4c84-...`).
|
|
1199
|
+
if (/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/iu.test(vibeSessionId)) {
|
|
1200
|
+
return vibeSessionId;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
catch { }
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
const fragment = String(descriptor?.sourcePath || '').split('#', 2)[1] || '';
|
|
1207
|
+
if (fragment) {
|
|
1208
|
+
try {
|
|
1209
|
+
return decodeURIComponent(fragment);
|
|
1210
|
+
}
|
|
1211
|
+
catch {
|
|
1212
|
+
return fragment;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
return '';
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Detect independently owned native sessions from process identity and open files.
|
|
1219
|
+
* Exact transcript paths carried by Turns queue-worker child environments are
|
|
1220
|
+
* subtracted so an anchor never describes itself as independently managed.
|
|
1221
|
+
*/
|
|
1222
|
+
function nativeProcessSnapshots(procRoot) {
|
|
1223
|
+
const processes = [];
|
|
1224
|
+
let entries = [];
|
|
1225
|
+
try {
|
|
1226
|
+
entries = fs.readdirSync(procRoot, { withFileTypes: true });
|
|
1227
|
+
}
|
|
1228
|
+
catch { }
|
|
1229
|
+
for (const entry of entries) {
|
|
1230
|
+
if (!entry.isDirectory() || !/^\d+$/u.test(entry.name))
|
|
1231
|
+
continue;
|
|
1232
|
+
let argv = [];
|
|
1233
|
+
let environ = '';
|
|
1234
|
+
let fdTargets = [];
|
|
1235
|
+
try {
|
|
1236
|
+
argv = fs
|
|
1237
|
+
.readFileSync(path.join(procRoot, entry.name, 'cmdline'), 'utf8')
|
|
1238
|
+
.split('\0')
|
|
1239
|
+
.filter(Boolean);
|
|
1240
|
+
}
|
|
1241
|
+
catch { }
|
|
1242
|
+
try {
|
|
1243
|
+
environ = fs.readFileSync(path.join(procRoot, entry.name, 'environ'), 'utf8');
|
|
1244
|
+
}
|
|
1245
|
+
catch { }
|
|
1246
|
+
try {
|
|
1247
|
+
fdTargets = fs.readdirSync(path.join(procRoot, entry.name, 'fd')).flatMap((descriptor) => {
|
|
1248
|
+
try {
|
|
1249
|
+
const target = fs
|
|
1250
|
+
.readlinkSync(path.join(procRoot, entry.name, 'fd', descriptor))
|
|
1251
|
+
.replace(/ \(deleted\)$/u, '');
|
|
1252
|
+
return path.isAbsolute(target) ? [target] : [];
|
|
1253
|
+
}
|
|
1254
|
+
catch {
|
|
1255
|
+
return [];
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
catch { }
|
|
1260
|
+
if (argv.length || environ)
|
|
1261
|
+
processes.push({ argv, cmdline: argv.join(' '), environ, fdTargets });
|
|
1262
|
+
}
|
|
1263
|
+
return processes;
|
|
1264
|
+
}
|
|
1265
|
+
const HARNESS_PROCESS_NAMES = {
|
|
1266
|
+
antigravity: ['agy', 'antigravity'],
|
|
1267
|
+
claude: ['claude'],
|
|
1268
|
+
cline: ['cline'],
|
|
1269
|
+
codebuddy: ['codebuddy'],
|
|
1270
|
+
codex: ['codex'],
|
|
1271
|
+
copilot: ['copilot'],
|
|
1272
|
+
cursor: ['cursor', 'cursor-agent'],
|
|
1273
|
+
deepseek: ['deepseek'],
|
|
1274
|
+
devin: ['devin'],
|
|
1275
|
+
gemini: ['gemini'],
|
|
1276
|
+
goose: ['goose'],
|
|
1277
|
+
grok: ['grok'],
|
|
1278
|
+
hermes: ['hermes'],
|
|
1279
|
+
kilo: ['kilo'],
|
|
1280
|
+
kimi: ['kimi', 'kimi-code'],
|
|
1281
|
+
muse: ['muse'],
|
|
1282
|
+
omp: ['omp'],
|
|
1283
|
+
openclaw: ['openclaw'],
|
|
1284
|
+
opencode: ['opencode'],
|
|
1285
|
+
pi: ['pi'],
|
|
1286
|
+
poolside: ['pool', 'poolside'],
|
|
1287
|
+
prime: ['prime', 'prime-agent'],
|
|
1288
|
+
qwen: ['qwen'],
|
|
1289
|
+
vibe: ['vibe', 'vibe-acp'],
|
|
1290
|
+
};
|
|
1291
|
+
const SCRIPT_RUNTIMES = new Set(['node', 'nodejs', 'bun', 'deno', 'python', 'python3']);
|
|
1292
|
+
function executableStem(value) {
|
|
1293
|
+
return path
|
|
1294
|
+
.basename(String(value || ''))
|
|
1295
|
+
.replace(/\.(?:[cm]?js|py)$/iu, '')
|
|
1296
|
+
.toLowerCase();
|
|
1297
|
+
}
|
|
1298
|
+
function canonicalProcessFile(value) {
|
|
1299
|
+
try {
|
|
1300
|
+
return fs.realpathSync(value);
|
|
1301
|
+
}
|
|
1302
|
+
catch {
|
|
1303
|
+
return path.resolve(value);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
/** Ownership requires the process itself to be the harness. Merely mentioning
|
|
1307
|
+
* `codex <session-id>` in a shell, grep, uploader log query, or tool command is
|
|
1308
|
+
* not evidence that the referenced native session is running. Script launchers
|
|
1309
|
+
* are accepted only when argv[0] is a known runtime and argv[1] is the harness
|
|
1310
|
+
* entrypoint; a shell wrapper still has a real child harness process to find. */
|
|
1311
|
+
function processRunsHarness(process, harness) {
|
|
1312
|
+
const names = new Set(HARNESS_PROCESS_NAMES[harness] || [harness]);
|
|
1313
|
+
const executable = executableStem(process.argv?.[0]);
|
|
1314
|
+
if (names.has(executable))
|
|
1315
|
+
return true;
|
|
1316
|
+
return SCRIPT_RUNTIMES.has(executable) && names.has(executableStem(process.argv?.[1]));
|
|
1317
|
+
}
|
|
1318
|
+
function queueManagedKeysFromProcesses(descriptors, processes) {
|
|
1319
|
+
const transcriptFiles = new Set();
|
|
1320
|
+
for (const process of processes) {
|
|
1321
|
+
const environment = Object.fromEntries(process.environ
|
|
1322
|
+
.split('\0')
|
|
1323
|
+
.filter(Boolean)
|
|
1324
|
+
.map((value) => {
|
|
1325
|
+
const separator = value.indexOf('=');
|
|
1326
|
+
return separator < 0 ? [value, ''] : [value.slice(0, separator), value.slice(separator + 1)];
|
|
1327
|
+
}));
|
|
1328
|
+
if (!environment.TURNS_QUEUE_WORKER_ID || !environment.TURNS_TRANSCRIPT_PATH)
|
|
1329
|
+
continue;
|
|
1330
|
+
try {
|
|
1331
|
+
transcriptFiles.add(fs.realpathSync(environment.TURNS_TRANSCRIPT_PATH));
|
|
1332
|
+
}
|
|
1333
|
+
catch {
|
|
1334
|
+
transcriptFiles.add(path.resolve(environment.TURNS_TRANSCRIPT_PATH));
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return new Set(descriptors.flatMap((descriptor) => {
|
|
1338
|
+
if (!descriptor?.file)
|
|
1339
|
+
return [];
|
|
1340
|
+
let file;
|
|
1341
|
+
try {
|
|
1342
|
+
file = fs.realpathSync(descriptor.file);
|
|
1343
|
+
}
|
|
1344
|
+
catch {
|
|
1345
|
+
file = path.resolve(descriptor.file);
|
|
1346
|
+
}
|
|
1347
|
+
return transcriptFiles.has(file) ? [`${descriptor.harness}\0${descriptor.sourcePath}`] : [];
|
|
1348
|
+
}));
|
|
1349
|
+
}
|
|
1350
|
+
export function queueManagedNativeDescriptorKeys(descriptors, procRoot = '/proc') {
|
|
1351
|
+
return queueManagedKeysFromProcesses(descriptors, nativeProcessSnapshots(procRoot));
|
|
1352
|
+
}
|
|
1353
|
+
export function externallyManagedNativeDescriptorKeys(descriptors, procRoot = '/proc') {
|
|
1354
|
+
const snapshots = nativeProcessSnapshots(procRoot);
|
|
1355
|
+
const queueManaged = queueManagedKeysFromProcesses(descriptors, snapshots);
|
|
1356
|
+
const externalProcesses = snapshots.filter((process) => !process.environ.includes('TURNS_QUEUE_WORKER_ID='));
|
|
1357
|
+
const byHarness = new Map();
|
|
1358
|
+
for (const descriptor of descriptors) {
|
|
1359
|
+
const key = `${descriptor.harness}\0${descriptor.sourcePath}`;
|
|
1360
|
+
if (queueManaged.has(key))
|
|
1361
|
+
continue;
|
|
1362
|
+
const session = descriptorNativeSessionId(descriptor);
|
|
1363
|
+
const file = descriptor?.file ? canonicalProcessFile(descriptor.file) : '';
|
|
1364
|
+
// A process with no session key is evidence only that a harness is
|
|
1365
|
+
// running, not that it owns every historical transcript for that harness.
|
|
1366
|
+
// An exact open transcript, however, identifies that one descriptor.
|
|
1367
|
+
if (!session && !file)
|
|
1368
|
+
continue;
|
|
1369
|
+
const group = byHarness.get(descriptor.harness) || [];
|
|
1370
|
+
group.push({ key, session, file });
|
|
1371
|
+
byHarness.set(descriptor.harness, group);
|
|
1372
|
+
}
|
|
1373
|
+
const managed = new Set();
|
|
1374
|
+
for (const [harness, candidates] of byHarness) {
|
|
1375
|
+
for (const process of externalProcesses) {
|
|
1376
|
+
if (!processRunsHarness(process, harness))
|
|
1377
|
+
continue;
|
|
1378
|
+
const openFiles = new Set(process.fdTargets.map(canonicalProcessFile));
|
|
1379
|
+
for (const candidate of candidates) {
|
|
1380
|
+
if ((candidate.session && process.cmdline.includes(candidate.session)) ||
|
|
1381
|
+
(candidate.file && openFiles.has(candidate.file))) {
|
|
1382
|
+
managed.add(candidate.key);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return managed;
|
|
1388
|
+
}
|
|
1389
|
+
function discoverLocalSessionInventory(only = '') {
|
|
1390
|
+
const descriptors = [];
|
|
1391
|
+
for (const [harness, store] of Object.entries(EXTRA_FILE_STORES)) {
|
|
1392
|
+
walk(store.root, (file) => {
|
|
1393
|
+
if (!store.include(file))
|
|
1394
|
+
return;
|
|
1395
|
+
const descriptor = fileDescriptor(harness, file);
|
|
1396
|
+
if (descriptor)
|
|
1397
|
+
descriptors.push(descriptor);
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
descriptors.push(...gooseDescriptors(), ...kiloDescriptors(), ...opencodeDescriptors(), ...devinDescriptors(), ...hermesDescriptors(), ...openclawDescriptors(), ...deepseekTrajectoryDescriptors());
|
|
1401
|
+
const filtered = only
|
|
1402
|
+
? descriptors.filter((descriptor) => descriptor.harness.toLowerCase().includes(only.toLowerCase()) ||
|
|
1403
|
+
descriptor.sourcePath.toLowerCase().includes(only.toLowerCase()))
|
|
1404
|
+
: descriptors;
|
|
1405
|
+
const unique = new Map();
|
|
1406
|
+
for (const descriptor of filtered)
|
|
1407
|
+
unique.set(`${descriptor.harness}\0${descriptor.sourcePath}`, descriptor);
|
|
1408
|
+
return [...unique.values()].sort((left, right) => left.harness.localeCompare(right.harness) || left.sourcePath.localeCompare(right.sourcePath));
|
|
1409
|
+
}
|
|
1410
|
+
/** Roots watched by the native uploader. Direct transcript stores are watched
|
|
1411
|
+
* recursively; database-backed stores are represented by their parent
|
|
1412
|
+
* directories so a WAL append schedules a cached inventory refresh. */
|
|
1413
|
+
export function nativeSessionWatchRoots() {
|
|
1414
|
+
return [
|
|
1415
|
+
...new Set([
|
|
1416
|
+
...Object.values(EXTRA_FILE_STORES).map((store) => store.root),
|
|
1417
|
+
DEEPSEEK_SESSIONS_ROOT,
|
|
1418
|
+
path.join(HOME, '.local', 'share', 'goose', 'sessions'),
|
|
1419
|
+
path.join(HOME, '.local', 'share', 'kilo'),
|
|
1420
|
+
path.join(HOME, '.local', 'share', 'opencode'),
|
|
1421
|
+
path.join(HOME, '.local', 'state', 'opencode'),
|
|
1422
|
+
path.join(HOME, '.local', 'share', 'devin', 'cli'),
|
|
1423
|
+
path.join(HOME, '.hermes'),
|
|
1424
|
+
path.join(HOME, '.openclaw', 'agents', 'main', 'sessions'),
|
|
1425
|
+
]),
|
|
1426
|
+
].filter((root) => safeStat(root)?.isDirectory());
|
|
1427
|
+
}
|
|
1428
|
+
/** Stores whose authoritative files live directly in the root. Recursive
|
|
1429
|
+
* notifications here would wake the uploader for unrelated caches (notably
|
|
1430
|
+
* the large ~/.hermes tree). */
|
|
1431
|
+
export function nativeSessionShallowWatchRoots() {
|
|
1432
|
+
return [
|
|
1433
|
+
path.join(HOME, '.local', 'share', 'goose', 'sessions'),
|
|
1434
|
+
path.join(HOME, '.local', 'share', 'kilo'),
|
|
1435
|
+
path.join(HOME, '.local', 'share', 'opencode'),
|
|
1436
|
+
path.join(HOME, '.local', 'state', 'opencode'),
|
|
1437
|
+
path.join(HOME, '.local', 'share', 'devin', 'cli'),
|
|
1438
|
+
path.join(HOME, '.hermes'),
|
|
1439
|
+
path.join(HOME, '.openclaw', 'agents', 'main', 'sessions'),
|
|
1440
|
+
path.join(HOME, '.prime', 'agent', 'sessions'),
|
|
1441
|
+
path.join(HOME, '.omp', 'agent', 'sessions'),
|
|
1442
|
+
path.join(HOME, '.local', 'state', 'poolside', 'trajectories'),
|
|
1443
|
+
].filter((root) => safeStat(root)?.isDirectory());
|
|
1444
|
+
}
|
|
1445
|
+
/** Every native session in the harness stores on this machine. */
|
|
1446
|
+
export function discoverLocalSessions(only = '') {
|
|
1447
|
+
return discoverLocalSessionInventory(only);
|
|
1448
|
+
}
|