@turns/dev 0.0.5 → 0.0.6
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/dist/agent-anchor.js +3 -0
- package/dist/file-browsing.js +10 -27
- package/dist/harness-registry.js +8 -0
- package/dist/local-session-sources.js +167 -4
- package/dist/path-resolution.js +14 -9
- package/dist/queue-worker.js +3 -9
- package/dist/upload-sessions.js +32 -11
- package/package.json +1 -1
package/dist/agent-anchor.js
CHANGED
|
@@ -868,6 +868,9 @@ class AcpTransport extends LineTransport {
|
|
|
868
868
|
prompt: [{ type: 'text', text }],
|
|
869
869
|
});
|
|
870
870
|
}
|
|
871
|
+
if (this.profile.steer === 'session/prompt') {
|
|
872
|
+
return this.prompt(text, 12 * 60 * 60_000);
|
|
873
|
+
}
|
|
871
874
|
return this.prompt(`/steer ${text}`, 12 * 60 * 60_000);
|
|
872
875
|
}
|
|
873
876
|
}
|
package/dist/file-browsing.js
CHANGED
|
@@ -3,24 +3,20 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { workingDirectoryIsAllowed } from "./directory-policy.js";
|
|
6
|
-
import {
|
|
6
|
+
import { uploadFileStream } from "./path-resolution.js";
|
|
7
7
|
/**
|
|
8
8
|
* Answering "what is in that directory?" from the machine that knows.
|
|
9
9
|
*
|
|
10
10
|
* A reader's browser cannot reach this machine, and this machine will not open
|
|
11
11
|
* a port to let it try. Directories are small structured answers, so the daemon
|
|
12
|
-
* sends their listing on the control socket
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* canonicalised before the allowlist is consulted, so a symlink pointing out
|
|
18
|
-
* of an allowed tree is not a way to read the rest of the disk.
|
|
12
|
+
* sends their listing on the control socket for the server to normalize into
|
|
13
|
+
* SQLite. Regular files still stream to object storage; nothing stays open
|
|
14
|
+
* once the one browse upload finishes. Every path is canonicalised before the
|
|
15
|
+
* allowlist is consulted, so a symlink pointing out of an allowed tree is not
|
|
16
|
+
* a way to read the rest of the disk.
|
|
19
17
|
*/
|
|
20
18
|
/** Kept in step with `MAX_DIRECTORY_ENTRIES` in the Worker's shared listing. */
|
|
21
19
|
export const MAX_DIRECTORY_ENTRIES = 2_000;
|
|
22
|
-
/** Kept in step with `DIRECTORY_INDEX_NAME` in the Worker's shared listing. */
|
|
23
|
-
export const DIRECTORY_INDEX_NAME = '_index.json';
|
|
24
20
|
/** One MB below the Worker request-body ceiling, matching the R2 endpoint. */
|
|
25
21
|
export const MAX_BROWSE_FILE_BYTES = 199 * 1024 * 1024;
|
|
26
22
|
/** The derived label used in reader-facing errors. */
|
|
@@ -177,10 +173,6 @@ export function readDirectory(directory) {
|
|
|
177
173
|
// clock inside the body would make every read produce different bytes.
|
|
178
174
|
return { path: directory, entries, truncated: names.length > MAX_DIRECTORY_ENTRIES };
|
|
179
175
|
}
|
|
180
|
-
/** Where one directory's listing is stored, as a path in the machine's space. */
|
|
181
|
-
export function directoryIndexPath(directory) {
|
|
182
|
-
return `${String(directory || '').replace(/\/+$/u, '')}/${DIRECTORY_INDEX_NAME}`;
|
|
183
|
-
}
|
|
184
176
|
export async function browseMachinePath({ requested, machine, baseUrl, options, workingDirectory = '', fetchImpl = fetch, }) {
|
|
185
177
|
const resolved = resolveBrowsePath(requested, {
|
|
186
178
|
workingDirectory,
|
|
@@ -192,16 +184,6 @@ export async function browseMachinePath({ requested, machine, baseUrl, options,
|
|
|
192
184
|
if (body.byteLength > MAX_BROWSE_FILE_BYTES)
|
|
193
185
|
throw new Error(`Directory listing is too large: ${resolved.path}`);
|
|
194
186
|
const etag = crypto.createHash('sha256').update(body).digest('hex');
|
|
195
|
-
const cache = uploadFileBytes({
|
|
196
|
-
baseUrl,
|
|
197
|
-
options,
|
|
198
|
-
machine,
|
|
199
|
-
absolutePath: directoryIndexPath(resolved.path),
|
|
200
|
-
contentType: 'application/json',
|
|
201
|
-
contentEtag: etag,
|
|
202
|
-
body,
|
|
203
|
-
fetchImpl,
|
|
204
|
-
});
|
|
205
187
|
return {
|
|
206
188
|
path: resolved.path,
|
|
207
189
|
kind: 'directory',
|
|
@@ -209,7 +191,6 @@ export async function browseMachinePath({ requested, machine, baseUrl, options,
|
|
|
209
191
|
contentType: 'application/json',
|
|
210
192
|
listing,
|
|
211
193
|
etag,
|
|
212
|
-
cache,
|
|
213
194
|
};
|
|
214
195
|
}
|
|
215
196
|
if (resolved.stat.size > MAX_BROWSE_FILE_BYTES) {
|
|
@@ -217,8 +198,9 @@ export async function browseMachinePath({ requested, machine, baseUrl, options,
|
|
|
217
198
|
}
|
|
218
199
|
const sample = readFileTypeSample(resolved.path, resolved.stat.size);
|
|
219
200
|
const contentType = browseContentType(resolved.path, sample, sample.byteLength === resolved.stat.size);
|
|
201
|
+
const modifiedAt = isoTime(resolved.stat.mtimeMs);
|
|
220
202
|
if (resolved.stat.size === 0)
|
|
221
|
-
return { path: resolved.path, kind: 'file', bytes: 0, contentType };
|
|
203
|
+
return { path: resolved.path, kind: 'file', bytes: 0, contentType, modifiedAt };
|
|
222
204
|
await uploadFileStream({
|
|
223
205
|
baseUrl,
|
|
224
206
|
options,
|
|
@@ -226,7 +208,8 @@ export async function browseMachinePath({ requested, machine, baseUrl, options,
|
|
|
226
208
|
absolutePath: resolved.path,
|
|
227
209
|
contentType,
|
|
228
210
|
bytes: resolved.stat.size,
|
|
211
|
+
modifiedAt,
|
|
229
212
|
fetchImpl,
|
|
230
213
|
});
|
|
231
|
-
return { path: resolved.path, kind: 'file', bytes: resolved.stat.size, contentType };
|
|
214
|
+
return { path: resolved.path, kind: 'file', bytes: resolved.stat.size, contentType, modifiedAt };
|
|
232
215
|
}
|
package/dist/harness-registry.js
CHANGED
|
@@ -41,6 +41,10 @@ export const ACP_PROFILES = {
|
|
|
41
41
|
command: 'opencode',
|
|
42
42
|
args: ['acp'],
|
|
43
43
|
homePaths: [['.opencode', 'bin', 'opencode']],
|
|
44
|
+
// OpenCode 1.18.21 gates its native task(background=true) schema behind
|
|
45
|
+
// this flag. Older versions ignore it; current versions expose a real
|
|
46
|
+
// independently addressable subagent lifecycle to daemon-launched turns.
|
|
47
|
+
env: { OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS: 'true' },
|
|
44
48
|
options: [{ id: 'mode', value: 'build' }],
|
|
45
49
|
mcp: true,
|
|
46
50
|
},
|
|
@@ -101,6 +105,10 @@ export const ACP_PROFILES = {
|
|
|
101
105
|
command: 'pool',
|
|
102
106
|
args: ['acp'],
|
|
103
107
|
resumeMethods: ['session/load'],
|
|
108
|
+
// Pool 1.0.16 implements ACP's same-session, concurrent prompt semantics:
|
|
109
|
+
// a second session/prompt while the first is running is folded into that
|
|
110
|
+
// turn. It is not the `/steer` slash command used by Hermes.
|
|
111
|
+
steer: 'session/prompt',
|
|
104
112
|
// Poolside exposes permission policy as an ACP config option rather than
|
|
105
113
|
// a session mode. Apply it per anchor so bypass runs cannot inherit its
|
|
106
114
|
// interactive `default` (always ask) selection.
|
|
@@ -3,20 +3,24 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
6
7
|
import { readDeepseekTrajectoryHeader } from "./deepseek-trajectories.js";
|
|
7
8
|
import { lettaSessionIdentity } from "./harness-registry.js";
|
|
8
9
|
import { readLettaSessionContext } from "./letta-session-context.js";
|
|
9
10
|
const HOME = os.homedir();
|
|
11
|
+
const CLAUDE_LIVE_SESSIONS_ROOT = path.join(HOME, '.claude', 'sessions');
|
|
10
12
|
const PROJECTION_ROOT = path.join(HOME, '.local', 'state', 'turns-dev', 'session-projections');
|
|
11
13
|
const DEEPSEEK_SESSIONS_ROOT = path.join(path.resolve(process.env.DSH_HOME || path.join(HOME, '.dsh')), 'sessions');
|
|
12
14
|
const JSON_FILE_CACHE = new Map();
|
|
13
15
|
const CURSOR_WORKER_CWD_CACHE = new Map();
|
|
14
16
|
const ANTIGRAVITY_TRANSCRIPT_CWD_CACHE = new Map();
|
|
17
|
+
const GROK_SPAWNED_SUBAGENT_CACHE = new Map();
|
|
15
18
|
const SUBAGENT_MARKER_CACHE = new Map();
|
|
16
19
|
const LINEAGE_CACHE = new Map();
|
|
17
20
|
const STORE_DESCRIPTOR_CACHE = new Map();
|
|
18
21
|
const MAX_METADATA_CACHE_ENTRIES = 16_384;
|
|
19
22
|
const OPAQUE_UUID_SOURCE = '[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}';
|
|
23
|
+
const OPAQUE_UUID = new RegExp(`^${OPAQUE_UUID_SOURCE}$`, 'iu');
|
|
20
24
|
function jsonObject(value) {
|
|
21
25
|
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
22
26
|
}
|
|
@@ -63,7 +67,8 @@ const EXTRA_FILE_STORES = {
|
|
|
63
67
|
},
|
|
64
68
|
qwen: {
|
|
65
69
|
root: path.join(HOME, '.qwen', 'projects'),
|
|
66
|
-
include: (file) => /[\\/]chats[\\/][^\\/]+\.jsonl$/i.test(file)
|
|
70
|
+
include: (file) => /[\\/]chats[\\/][^\\/]+\.jsonl$/i.test(file) ||
|
|
71
|
+
/[\\/]subagents[\\/][^\\/]+[\\/]agent-[^\\/]+\.jsonl$/i.test(file),
|
|
67
72
|
},
|
|
68
73
|
codebuddy: {
|
|
69
74
|
root: path.join(HOME, '.codebuddy', 'projects'),
|
|
@@ -187,6 +192,14 @@ const STRUCTURAL_SUBAGENT_STORES = {
|
|
|
187
192
|
parentSourcePath: `${match[1]}/messages.jsonl`,
|
|
188
193
|
}),
|
|
189
194
|
},
|
|
195
|
+
qwen: {
|
|
196
|
+
path: /^(.*)\/subagents\/([^/]+)\/(agent-[^/]+)\.jsonl$/iu,
|
|
197
|
+
lineage: (match) => ({
|
|
198
|
+
nativeSessionId: match[3],
|
|
199
|
+
parentNativeSessionId: match[2],
|
|
200
|
+
parentSourcePath: `${match[1]}/chats/${match[2]}.jsonl`,
|
|
201
|
+
}),
|
|
202
|
+
},
|
|
190
203
|
};
|
|
191
204
|
/** Interpret one harness's structural child path. `uploadable` lets the same
|
|
192
205
|
* rule reject internal sidechains before discovery and lineage can diverge. */
|
|
@@ -385,6 +398,95 @@ function parsedObject(value) {
|
|
|
385
398
|
return null;
|
|
386
399
|
}
|
|
387
400
|
}
|
|
401
|
+
function grokSummaryFile(descriptor) {
|
|
402
|
+
if (descriptor?.harness !== 'grok' || path.basename(String(descriptor?.file || '')) !== 'chat_history.jsonl') {
|
|
403
|
+
return '';
|
|
404
|
+
}
|
|
405
|
+
return path.join(path.dirname(path.resolve(descriptor.file)), 'summary.json');
|
|
406
|
+
}
|
|
407
|
+
function grokSummary(descriptor) {
|
|
408
|
+
const file = grokSummaryFile(descriptor);
|
|
409
|
+
return file ? jsonFile(file) : null;
|
|
410
|
+
}
|
|
411
|
+
/** Grok 1.0.5 stores native subagents beside their parent with the same
|
|
412
|
+
* transcript shape. The adjacent summary is the child's durable type marker. */
|
|
413
|
+
function grokSummaryMarksSubagent(descriptor) {
|
|
414
|
+
return (String(grokSummary(descriptor)?.session_kind || '')
|
|
415
|
+
.trim()
|
|
416
|
+
.toLowerCase() === 'subagent');
|
|
417
|
+
}
|
|
418
|
+
/** Child ids announced by one Grok transcript's successful spawn results.
|
|
419
|
+
* Results can occur anywhere in a long session, so scan the complete JSONL and
|
|
420
|
+
* cache by the append-only transcript's physical identity. */
|
|
421
|
+
export function grokSpawnedSubagentIds(descriptor) {
|
|
422
|
+
if (descriptor?.harness !== 'grok')
|
|
423
|
+
return [];
|
|
424
|
+
const file = String(descriptor?.file || '');
|
|
425
|
+
const stat = safeStat(file);
|
|
426
|
+
if (!stat?.isFile())
|
|
427
|
+
return [];
|
|
428
|
+
const fingerprint = `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
|
|
429
|
+
const cached = GROK_SPAWNED_SUBAGENT_CACHE.get(file);
|
|
430
|
+
if (cached?.fingerprint === fingerprint) {
|
|
431
|
+
GROK_SPAWNED_SUBAGENT_CACHE.delete(file);
|
|
432
|
+
GROK_SPAWNED_SUBAGENT_CACHE.set(file, cached);
|
|
433
|
+
return cached.ids;
|
|
434
|
+
}
|
|
435
|
+
const ids = new Set();
|
|
436
|
+
const inspect = (line) => {
|
|
437
|
+
if (!line.includes('Subagent started in background.'))
|
|
438
|
+
return;
|
|
439
|
+
try {
|
|
440
|
+
const record = jsonObject(JSON.parse(line));
|
|
441
|
+
if (record?.type !== 'tool_result' || typeof record.content !== 'string')
|
|
442
|
+
return;
|
|
443
|
+
const id = /^Subagent started in background\.\r?\nsubagent_id:\s*([^\s]+)(?:\r?\n|$)/u.exec(record.content)?.[1];
|
|
444
|
+
if (id && OPAQUE_UUID.test(id))
|
|
445
|
+
ids.add(id);
|
|
446
|
+
}
|
|
447
|
+
catch { }
|
|
448
|
+
};
|
|
449
|
+
let handle;
|
|
450
|
+
try {
|
|
451
|
+
handle = fs.openSync(file, 'r');
|
|
452
|
+
const decoder = new StringDecoder('utf8');
|
|
453
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
454
|
+
let pending = '';
|
|
455
|
+
let offset = 0;
|
|
456
|
+
while (offset < stat.size) {
|
|
457
|
+
const bytesRead = fs.readSync(handle, buffer, 0, buffer.length, offset);
|
|
458
|
+
if (!bytesRead)
|
|
459
|
+
break;
|
|
460
|
+
offset += bytesRead;
|
|
461
|
+
pending += decoder.write(buffer.subarray(0, bytesRead));
|
|
462
|
+
let newline = pending.indexOf('\n');
|
|
463
|
+
while (newline >= 0) {
|
|
464
|
+
inspect(pending.slice(0, newline).replace(/\r$/u, ''));
|
|
465
|
+
pending = pending.slice(newline + 1);
|
|
466
|
+
newline = pending.indexOf('\n');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
pending += decoder.end();
|
|
470
|
+
if (pending)
|
|
471
|
+
inspect(pending.replace(/\r$/u, ''));
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
finally {
|
|
477
|
+
try {
|
|
478
|
+
if (handle !== undefined)
|
|
479
|
+
fs.closeSync(handle);
|
|
480
|
+
}
|
|
481
|
+
catch { }
|
|
482
|
+
}
|
|
483
|
+
const value = [...ids];
|
|
484
|
+
GROK_SPAWNED_SUBAGENT_CACHE.set(file, { fingerprint, ids: value });
|
|
485
|
+
if (GROK_SPAWNED_SUBAGENT_CACHE.size > MAX_METADATA_CACHE_ENTRIES) {
|
|
486
|
+
GROK_SPAWNED_SUBAGENT_CACHE.delete(GROK_SPAWNED_SUBAGENT_CACHE.keys().next().value);
|
|
487
|
+
}
|
|
488
|
+
return value;
|
|
489
|
+
}
|
|
388
490
|
function recordMarksSubagent(record) {
|
|
389
491
|
const root = parsedObject(record);
|
|
390
492
|
if (!root)
|
|
@@ -435,6 +537,8 @@ export function nativeDescriptorIsSubagent(descriptor) {
|
|
|
435
537
|
return true;
|
|
436
538
|
if (descriptor?.isSubagent === true || descriptor?.threadSource === 'subagent')
|
|
437
539
|
return true;
|
|
540
|
+
if (grokSummaryMarksSubagent(descriptor))
|
|
541
|
+
return true;
|
|
438
542
|
const file = String(descriptor?.file || '');
|
|
439
543
|
const stat = safeStat(file);
|
|
440
544
|
const fingerprint = stat?.isFile() ? `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}` : '';
|
|
@@ -835,7 +939,8 @@ export function nativeDescriptorLineage(descriptor) {
|
|
|
835
939
|
const sourcePath = String(descriptor?.sourcePath || descriptor?.file || '').replaceAll('\\', '/');
|
|
836
940
|
const stat = safeStat(String(descriptor?.file || ''));
|
|
837
941
|
const metadataStat = safeStat(claudeSubagentMetadataFile(descriptor));
|
|
838
|
-
const
|
|
942
|
+
const grokSummaryStat = safeStat(grokSummaryFile(descriptor));
|
|
943
|
+
const fingerprint = [stat, metadataStat, grokSummaryStat]
|
|
839
944
|
.map((candidate) => candidate?.isFile() ? `${candidate.dev}:${candidate.ino}:${candidate.size}:${candidate.mtimeMs}` : '-')
|
|
840
945
|
.join('|');
|
|
841
946
|
const cacheKey = [
|
|
@@ -1309,7 +1414,7 @@ function nativeProcessSnapshots(procRoot) {
|
|
|
1309
1414
|
}
|
|
1310
1415
|
catch { }
|
|
1311
1416
|
if (argv.length || environ)
|
|
1312
|
-
processes.push({ argv, cmdline: argv.join(' '), environ, fdTargets });
|
|
1417
|
+
processes.push({ pid: entry.name, argv, cmdline: argv.join(' '), environ, fdTargets });
|
|
1313
1418
|
}
|
|
1314
1419
|
return processes;
|
|
1315
1420
|
}
|
|
@@ -1368,6 +1473,55 @@ function processRunsHarness(process, harness) {
|
|
|
1368
1473
|
return true;
|
|
1369
1474
|
return SCRIPT_RUNTIMES.has(executable) && names.has(executableStem(process.argv?.[1]));
|
|
1370
1475
|
}
|
|
1476
|
+
function nativeProcessStartTime(procRoot, pid) {
|
|
1477
|
+
try {
|
|
1478
|
+
const stat = fs.readFileSync(path.join(procRoot, pid, 'stat'), 'utf8');
|
|
1479
|
+
const commandEnd = stat.lastIndexOf(')');
|
|
1480
|
+
if (commandEnd < 0)
|
|
1481
|
+
return '';
|
|
1482
|
+
// Fields after the command start at proc(5) field 3; starttime is field 22.
|
|
1483
|
+
return stat.slice(commandEnd + 1).trim().split(/\s+/u)[19] || '';
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
return '';
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
/** Claude records an exact PID -> native-session association here even for a
|
|
1490
|
+
* plain `claude` launch whose command line contains no session ID. Validate the
|
|
1491
|
+
* kernel start time so a stale registration cannot attach to a reused PID. */
|
|
1492
|
+
function registeredClaudeSessionIds(processes, procRoot, sessionsRoot) {
|
|
1493
|
+
const externalClaudeByPid = new Map(processes
|
|
1494
|
+
.filter((process) => !process.environ.includes('TURNS_QUEUE_WORKER_ID=') && processRunsHarness(process, 'claude'))
|
|
1495
|
+
.map((process) => [process.pid, process]));
|
|
1496
|
+
if (!externalClaudeByPid.size)
|
|
1497
|
+
return new Set();
|
|
1498
|
+
let entries = [];
|
|
1499
|
+
try {
|
|
1500
|
+
entries = fs.readdirSync(sessionsRoot, { withFileTypes: true });
|
|
1501
|
+
}
|
|
1502
|
+
catch {
|
|
1503
|
+
return new Set();
|
|
1504
|
+
}
|
|
1505
|
+
const sessionIds = new Set();
|
|
1506
|
+
for (const entry of entries) {
|
|
1507
|
+
const pid = /^(\d+)\.json$/u.exec(entry.name)?.[1];
|
|
1508
|
+
if (!pid || !entry.isFile() || !externalClaudeByPid.has(pid))
|
|
1509
|
+
continue;
|
|
1510
|
+
try {
|
|
1511
|
+
const registration = JSON.parse(fs.readFileSync(path.join(sessionsRoot, entry.name), 'utf8'));
|
|
1512
|
+
const sessionId = String(registration?.sessionId || '').trim();
|
|
1513
|
+
const registeredStart = String(registration?.procStart || '').trim();
|
|
1514
|
+
if (String(registration?.pid) === pid &&
|
|
1515
|
+
OPAQUE_UUID.test(sessionId) &&
|
|
1516
|
+
registeredStart &&
|
|
1517
|
+
registeredStart === nativeProcessStartTime(procRoot, pid)) {
|
|
1518
|
+
sessionIds.add(sessionId);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
catch { }
|
|
1522
|
+
}
|
|
1523
|
+
return sessionIds;
|
|
1524
|
+
}
|
|
1371
1525
|
function queueManagedKeysFromProcesses(descriptors, processes) {
|
|
1372
1526
|
const transcriptFiles = new Set();
|
|
1373
1527
|
for (const process of processes) {
|
|
@@ -1403,7 +1557,7 @@ function queueManagedKeysFromProcesses(descriptors, processes) {
|
|
|
1403
1557
|
export function queueManagedNativeDescriptorKeys(descriptors, procRoot = '/proc') {
|
|
1404
1558
|
return queueManagedKeysFromProcesses(descriptors, nativeProcessSnapshots(procRoot));
|
|
1405
1559
|
}
|
|
1406
|
-
export function externallyManagedNativeDescriptorKeys(descriptors, procRoot = '/proc') {
|
|
1560
|
+
export function externallyManagedNativeDescriptorKeys(descriptors, procRoot = '/proc', claudeSessionsRoot = CLAUDE_LIVE_SESSIONS_ROOT) {
|
|
1407
1561
|
const snapshots = nativeProcessSnapshots(procRoot);
|
|
1408
1562
|
const queueManaged = queueManagedKeysFromProcesses(descriptors, snapshots);
|
|
1409
1563
|
const externalProcesses = snapshots.filter((process) => !process.environ.includes('TURNS_QUEUE_WORKER_ID='));
|
|
@@ -1424,7 +1578,16 @@ export function externallyManagedNativeDescriptorKeys(descriptors, procRoot = '/
|
|
|
1424
1578
|
byHarness.set(descriptor.harness, group);
|
|
1425
1579
|
}
|
|
1426
1580
|
const managed = new Set();
|
|
1581
|
+
const registeredClaude = byHarness.has('claude')
|
|
1582
|
+
? registeredClaudeSessionIds(snapshots, procRoot, claudeSessionsRoot)
|
|
1583
|
+
: new Set();
|
|
1427
1584
|
for (const [harness, candidates] of byHarness) {
|
|
1585
|
+
if (harness === 'claude' && registeredClaude.size) {
|
|
1586
|
+
for (const candidate of candidates) {
|
|
1587
|
+
if (candidate.session && registeredClaude.has(candidate.session))
|
|
1588
|
+
managed.add(candidate.key);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1428
1591
|
for (const process of externalProcesses) {
|
|
1429
1592
|
if (!processRunsHarness(process, harness))
|
|
1430
1593
|
continue;
|
package/dist/path-resolution.js
CHANGED
|
@@ -145,7 +145,12 @@ export function resolvePathCandidate(candidate, workingDirectory, { allowedDirec
|
|
|
145
145
|
continue;
|
|
146
146
|
if (allowedDirectories.length && !insideAllowedDirectory(file, allowedDirectories))
|
|
147
147
|
continue;
|
|
148
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
file,
|
|
150
|
+
bytes: stat.size,
|
|
151
|
+
contentType: agentImageContentType(file),
|
|
152
|
+
modifiedAt: Number.isFinite(stat.mtimeMs) ? new Date(stat.mtimeMs).toISOString() : '',
|
|
153
|
+
};
|
|
149
154
|
}
|
|
150
155
|
return null;
|
|
151
156
|
}
|
|
@@ -167,13 +172,11 @@ function uploadHeaders(options) {
|
|
|
167
172
|
/**
|
|
168
173
|
* Put bytes in the account's private prefix, under the path they came from.
|
|
169
174
|
*
|
|
170
|
-
* The path is the object's identity rather than a description of it
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* addressed by machine and path alone — the session that happened to prompt the
|
|
174
|
-
* upload is not part of what was stored, and nothing records it.
|
|
175
|
+
* The path is the object's identity rather than a description of it. It is
|
|
176
|
+
* addressed by machine and path alone — the session that happened to prompt
|
|
177
|
+
* the upload is not part of what was stored.
|
|
175
178
|
*/
|
|
176
|
-
export async function uploadFileBytes({ baseUrl, options, machine, absolutePath, contentType,
|
|
179
|
+
export async function uploadFileBytes({ baseUrl, options, machine, absolutePath, contentType, modifiedAt, body, fetchImpl = fetch, }) {
|
|
177
180
|
const url = new URL(`/api/file/${encodeURIComponent(machine)}/${encodeURIComponent(absolutePath)}`, String(baseUrl));
|
|
178
181
|
const response = await fetchImpl(url, {
|
|
179
182
|
method: 'PUT',
|
|
@@ -181,7 +184,7 @@ export async function uploadFileBytes({ baseUrl, options, machine, absolutePath,
|
|
|
181
184
|
...uploadHeaders(options),
|
|
182
185
|
'content-type': contentType,
|
|
183
186
|
'content-length': String(body.byteLength),
|
|
184
|
-
...(
|
|
187
|
+
...(modifiedAt ? { 'x-turns-file-modified-at': modifiedAt } : {}),
|
|
185
188
|
},
|
|
186
189
|
body,
|
|
187
190
|
});
|
|
@@ -190,7 +193,7 @@ export async function uploadFileBytes({ baseUrl, options, machine, absolutePath,
|
|
|
190
193
|
return true;
|
|
191
194
|
}
|
|
192
195
|
/** Stream one on-disk file to R2 without retaining its body in the daemon. */
|
|
193
|
-
export async function uploadFileStream({ baseUrl, options, machine, absolutePath, contentType, bytes, fetchImpl = fetch, }) {
|
|
196
|
+
export async function uploadFileStream({ baseUrl, options, machine, absolutePath, contentType, bytes, modifiedAt, fetchImpl = fetch, }) {
|
|
194
197
|
const url = new URL(`/api/file/${encodeURIComponent(machine)}/${encodeURIComponent(absolutePath)}`, String(baseUrl));
|
|
195
198
|
const body = fs.createReadStream(absolutePath);
|
|
196
199
|
try {
|
|
@@ -200,6 +203,7 @@ export async function uploadFileStream({ baseUrl, options, machine, absolutePath
|
|
|
200
203
|
...uploadHeaders(options),
|
|
201
204
|
'content-type': contentType,
|
|
202
205
|
'content-length': String(bytes),
|
|
206
|
+
...(modifiedAt ? { 'x-turns-file-modified-at': modifiedAt } : {}),
|
|
203
207
|
},
|
|
204
208
|
body,
|
|
205
209
|
// Node requires this opt-in for a streaming request body.
|
|
@@ -226,6 +230,7 @@ export async function uploadResolvedFile({ baseUrl, options, machine, resolved,
|
|
|
226
230
|
machine,
|
|
227
231
|
absolutePath: resolved.file,
|
|
228
232
|
contentType: resolved.contentType,
|
|
233
|
+
modifiedAt: resolved.modifiedAt,
|
|
229
234
|
body,
|
|
230
235
|
fetchImpl,
|
|
231
236
|
});
|
package/dist/queue-worker.js
CHANGED
|
@@ -1037,8 +1037,8 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1037
1037
|
*
|
|
1038
1038
|
* Unlike the rest of these handlers this touches no harness: browsing is a
|
|
1039
1039
|
* property of the machine, so it answers for a stopped session as readily as
|
|
1040
|
-
* for a running one. Directory bytes answer directly
|
|
1041
|
-
*
|
|
1040
|
+
* for a running one. Directory bytes answer directly and the server indexes
|
|
1041
|
+
* them in its Durable Object; only regular file bodies are uploaded to R2.
|
|
1042
1042
|
*/
|
|
1043
1043
|
const handleFileBrowse = async (message, replyRpc) => {
|
|
1044
1044
|
const sessionId = String(message.sessionId || '');
|
|
@@ -1050,18 +1050,12 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1050
1050
|
options,
|
|
1051
1051
|
workingDirectory: await browseWorkingDirectory(sessionId),
|
|
1052
1052
|
});
|
|
1053
|
-
const { cache, ...stored } = browsed.kind === 'directory' ? browsed : { ...browsed, cache: undefined };
|
|
1054
1053
|
replyRpc.notify({
|
|
1055
1054
|
type: 'session.file.browse.result',
|
|
1056
1055
|
requestId: message.requestId,
|
|
1057
1056
|
sessionId,
|
|
1058
1057
|
ok: true,
|
|
1059
|
-
...
|
|
1060
|
-
});
|
|
1061
|
-
// The direct listing is already useful. Its R2 copy is only the fallback
|
|
1062
|
-
// for a later offline browse, so storage latency must not hold this reply.
|
|
1063
|
-
void cache?.catch((error) => {
|
|
1064
|
-
console.warn(`Directory browse fallback upload failed: ${redactor.redactText(error?.message || String(error))}`);
|
|
1058
|
+
...browsed,
|
|
1065
1059
|
});
|
|
1066
1060
|
}
|
|
1067
1061
|
catch (error) {
|
package/dist/upload-sessions.js
CHANGED
|
@@ -6,12 +6,12 @@ import path from 'node:path';
|
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import WebSocket from 'ws';
|
|
9
|
-
import { createPathResolver, pathCandidates } from "./path-resolution.js";
|
|
10
9
|
import { streamDeepseekTrajectoryLines } from "./deepseek-trajectories.js";
|
|
11
10
|
import { normalizeAllowedDirectories, workingDirectoryIsAllowed } from "./directory-policy.js";
|
|
12
11
|
import { discoverGitAssociation } from "./git-association.js";
|
|
13
|
-
import { discoverLocalSessions, externallyManagedNativeDescriptorKeys, nativeDescriptorAgentSessionId, nativeDescriptorLineage, nativeDescriptorWorkingDirectory, nativeSessionShallowWatchRoots, nativeSessionWatchRoots, } from "./local-session-sources.js";
|
|
12
|
+
import { discoverLocalSessions, externallyManagedNativeDescriptorKeys, grokSpawnedSubagentIds, nativeDescriptorAgentSessionId, nativeDescriptorLineage, nativeDescriptorWorkingDirectory, nativeSessionShallowWatchRoots, nativeSessionWatchRoots, } from "./local-session-sources.js";
|
|
14
13
|
import { compactNativeRaw } from "./native-record-compaction.js";
|
|
14
|
+
import { createPathResolver, pathCandidates } from "./path-resolution.js";
|
|
15
15
|
import { httpBaseUrl, watchQueuedPrompts } from "./queue-worker.js";
|
|
16
16
|
import { createSecretRedactor, REDACTION_VERSION } from "./secret-redaction.js";
|
|
17
17
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -292,6 +292,14 @@ export function linkNativeSessionParents(machine, descriptors) {
|
|
|
292
292
|
descriptor,
|
|
293
293
|
lineage: nativeDescriptorLineage(descriptor),
|
|
294
294
|
}));
|
|
295
|
+
const storeNamespace = (descriptor) => {
|
|
296
|
+
const sourcePath = String(descriptor?.sourcePath || '').replaceAll('\\', '/');
|
|
297
|
+
if (descriptor?.harness === 'gemini')
|
|
298
|
+
return /^(.*?\/chats)(?:\/|$)/iu.exec(sourcePath)?.[1] || '';
|
|
299
|
+
if (descriptor?.harness === 'grok')
|
|
300
|
+
return /^(.*?\/sessions\/[^/]+)(?:\/|$)/iu.exec(sourcePath)?.[1] || '';
|
|
301
|
+
return '';
|
|
302
|
+
};
|
|
295
303
|
const byNativeId = new Map();
|
|
296
304
|
for (const record of records) {
|
|
297
305
|
if (!record.lineage.nativeSessionId)
|
|
@@ -301,12 +309,18 @@ export function linkNativeSessionParents(machine, descriptors) {
|
|
|
301
309
|
candidates.push(record.descriptor);
|
|
302
310
|
byNativeId.set(key, candidates);
|
|
303
311
|
}
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
if (descriptor
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
312
|
+
const grokParentsByChildId = new Map();
|
|
313
|
+
for (const record of records) {
|
|
314
|
+
if (record.descriptor.harness !== 'grok')
|
|
315
|
+
continue;
|
|
316
|
+
for (const childId of grokSpawnedSubagentIds(record.descriptor)) {
|
|
317
|
+
if (childId === record.lineage.nativeSessionId)
|
|
318
|
+
continue;
|
|
319
|
+
const candidates = grokParentsByChildId.get(childId) || [];
|
|
320
|
+
candidates.push(record.descriptor);
|
|
321
|
+
grokParentsByChildId.set(childId, candidates);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
310
324
|
return records.flatMap(({ descriptor, lineage }) => {
|
|
311
325
|
// Internal harness sidechains such as Claude's compaction worker are still
|
|
312
326
|
// children for identity/resume safety, but never become uploaded sessions.
|
|
@@ -329,19 +343,26 @@ export function linkNativeSessionParents(machine, descriptors) {
|
|
|
329
343
|
? [...localCandidates].sort((left, right) => Number(right.mtimeMs || 0) - Number(left.mtimeMs || 0) ||
|
|
330
344
|
String(right.sourcePath).localeCompare(String(left.sourcePath)))[0]
|
|
331
345
|
: null;
|
|
346
|
+
const grokParentCandidates = descriptor.harness === 'grok' ? grokParentsByChildId.get(lineage.nativeSessionId) || [] : [];
|
|
347
|
+
const grokNamespace = storeNamespace(descriptor);
|
|
348
|
+
const localGrokParents = grokNamespace
|
|
349
|
+
? grokParentCandidates.filter((candidate) => storeNamespace(candidate) === grokNamespace)
|
|
350
|
+
: grokParentCandidates;
|
|
351
|
+
const grokParent = localGrokParents.length === 1 ? localGrokParents[0] : null;
|
|
332
352
|
// An exact structural parent path is stronger than a harness-wide native
|
|
333
353
|
// ID match. This matters for Claude descendants, whose short runtime agent
|
|
334
354
|
// IDs can recur in another project while the adjacent sidecar names the
|
|
335
355
|
// correct transcript in this session's flat subagents directory.
|
|
336
|
-
const parentSourcePath = lineage.parentSourcePath || parent?.sourcePath;
|
|
356
|
+
const parentSourcePath = lineage.parentSourcePath || grokParent?.sourcePath || parent?.sourcePath;
|
|
357
|
+
const isSubagent = lineage.isSubagent || Boolean(grokParent);
|
|
337
358
|
// An unresolved child cannot satisfy the sidebar contract. Leave it out of
|
|
338
359
|
// native upload until its store exposes enough lineage to nest it safely.
|
|
339
|
-
if (
|
|
360
|
+
if (isSubagent && !parentSourcePath)
|
|
340
361
|
return [];
|
|
341
362
|
return [
|
|
342
363
|
{
|
|
343
364
|
...descriptor,
|
|
344
|
-
isSubagent
|
|
365
|
+
isSubagent,
|
|
345
366
|
...(lineage.nativeTitle ? { nativeTitle: lineage.nativeTitle } : {}),
|
|
346
367
|
...(lineage.nativeSubagentType ? { nativeSubagentType: lineage.nativeSubagentType } : {}),
|
|
347
368
|
...(lineage.nativeSubagentDepth ? { nativeSubagentDepth: lineage.nativeSubagentDepth } : {}),
|