dsh-scratch-workspace 0.1.1 → 0.1.2
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/lib/gc.js +9 -6
- package/lib/patch.js +129 -183
- package/package.json +1 -1
package/lib/gc.js
CHANGED
|
@@ -127,9 +127,10 @@ export async function cleanOrphans({
|
|
|
127
127
|
const info = await stat(dirPath)
|
|
128
128
|
const ageMs = now - info.mtimeMs
|
|
129
129
|
|
|
130
|
-
// Case 1: Empty abandoned session folder
|
|
130
|
+
// Case 1: Empty abandoned session folder
|
|
131
131
|
const empty = await isDirectoryEmpty(dirPath)
|
|
132
|
-
|
|
132
|
+
const hasSession = knownShortIds.has(folderShort)
|
|
133
|
+
if (empty && (!hasSession || ageMs >= emptyGraceMs)) {
|
|
133
134
|
await removeSessionDir(dirPath)
|
|
134
135
|
cleaned.push({ path: dirPath, reason: 'empty-abandoned' })
|
|
135
136
|
logger?.info?.(`dsh-scratch-workspace gc: removed empty session dir ${dirPath}`)
|
|
@@ -137,7 +138,6 @@ export async function cleanOrphans({
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
// Case 2: Orphan session directory no longer associated with any session
|
|
140
|
-
const hasSession = knownShortIds.has(folderShort)
|
|
141
141
|
if (!hasSession && ageMs > orphanTtlMs) {
|
|
142
142
|
await removeSessionDir(dirPath)
|
|
143
143
|
cleaned.push({ path: dirPath, reason: 'orphan-ttl' })
|
|
@@ -172,19 +172,22 @@ export function setupAutoGc(ctx, iso, {
|
|
|
172
172
|
// Live sessions from registry
|
|
173
173
|
const registry = ctx.root?.workspaceRegistry ?? ctx.workspaceRegistry
|
|
174
174
|
const workspaces = registry?.list?.() ?? []
|
|
175
|
+
const archived = new Set(registry?.archivedSessionIds ?? [])
|
|
175
176
|
for (const ws of workspaces) {
|
|
176
|
-
for (const id of ws.sessionIds ?? [])
|
|
177
|
+
for (const id of ws.sessionIds ?? []) {
|
|
178
|
+
if (!archived.has(id)) ids.add(id)
|
|
179
|
+
}
|
|
177
180
|
}
|
|
178
181
|
// Live agents
|
|
179
182
|
const agents = ctx.agents ?? ctx.root?.agents
|
|
180
183
|
if (typeof agents?.list === 'function') {
|
|
181
|
-
for (const ag of agents.list() ?? []) if (ag?.id) ids.add(ag.id)
|
|
184
|
+
for (const ag of agents.list() ?? []) if (ag?.id && !archived.has(ag.id)) ids.add(ag.id)
|
|
182
185
|
}
|
|
183
186
|
// Stored headers if available
|
|
184
187
|
try {
|
|
185
188
|
const stored = await registry?.listStoredHeaders?.()
|
|
186
189
|
if (Array.isArray(stored)) {
|
|
187
|
-
for (const h of stored) if (h?.id) ids.add(h.id)
|
|
190
|
+
for (const h of stored) if (h?.id && !archived.has(h.id)) ids.add(h.id)
|
|
188
191
|
}
|
|
189
192
|
} catch {}
|
|
190
193
|
|
package/lib/patch.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
allocateSessionDir,
|
|
16
16
|
isMeaningfulTitle,
|
|
17
17
|
sanitizeTitleForPath,
|
|
18
|
+
SESSION_DIR_PATTERN,
|
|
18
19
|
SESSION_DIR_PREFIX,
|
|
19
20
|
setIsolated,
|
|
20
21
|
} from './isolate.js'
|
|
@@ -23,6 +24,51 @@ import { cleanOrphans, isDirectoryEmpty, removeSessionDir, shortSessionId } from
|
|
|
23
24
|
/** The registry-internal host property. */
|
|
24
25
|
const HOST_PROPERTY = 'host'
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Locate and remove a session's directory if it is empty.
|
|
29
|
+
* Uses 3-tier lookup: in-memory iso state -> registry index -> isolating roots scan by shortId.
|
|
30
|
+
* @param {string} sessionId
|
|
31
|
+
* @param {object} iso
|
|
32
|
+
* @param {object} [registry]
|
|
33
|
+
*/
|
|
34
|
+
export async function cleanEmptySessionDir(sessionId, iso, registry) {
|
|
35
|
+
if (!sessionId) return
|
|
36
|
+
let sessionDir = iso?.sessionPath?.(sessionId)
|
|
37
|
+
if (!sessionDir && typeof registry?.sessionPath === 'function') {
|
|
38
|
+
try { sessionDir = registry.sessionPath(sessionId) } catch {}
|
|
39
|
+
}
|
|
40
|
+
const short = shortSessionId(sessionId)
|
|
41
|
+
const candidateDirs = new Set()
|
|
42
|
+
if (sessionDir) candidateDirs.add(sessionDir)
|
|
43
|
+
|
|
44
|
+
if (short && typeof iso?.roots === 'function') {
|
|
45
|
+
for (const root of iso.roots()) {
|
|
46
|
+
try {
|
|
47
|
+
const entries = await readdir(root, { withFileTypes: true })
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!entry.isDirectory()) continue
|
|
50
|
+
const match = entry.name.match(SESSION_DIR_PATTERN)
|
|
51
|
+
if (match) {
|
|
52
|
+
const entryShort = (match[2] ?? match[1] ?? '').split('-')[0].toLowerCase()
|
|
53
|
+
if (entryShort === short) {
|
|
54
|
+
candidateDirs.add(join(root, entry.name))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
iso?.forgetSession?.(sessionId)
|
|
63
|
+
|
|
64
|
+
for (const dir of candidateDirs) {
|
|
65
|
+
const empty = await isDirectoryEmpty(dir).catch(() => false)
|
|
66
|
+
if (empty) {
|
|
67
|
+
await removeSessionDir(dir).catch(() => {})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
26
72
|
/**
|
|
27
73
|
* Wrap the Workspace registry's session-path index so isolated sessions resolve
|
|
28
74
|
* to the Workspace that owns their directory across all DSH versions.
|
|
@@ -81,29 +127,30 @@ export function patchRegistry(registry, iso) {
|
|
|
81
127
|
}
|
|
82
128
|
|
|
83
129
|
// Hook archiveSession to immediately clean empty session directories on archive
|
|
84
|
-
const
|
|
130
|
+
const proto = registry ? Object.getPrototypeOf(registry) : null
|
|
131
|
+
const originalArchive = proto?.archiveSession ?? registry?.archiveSession
|
|
85
132
|
if (typeof originalArchive === 'function') {
|
|
86
|
-
|
|
133
|
+
const wrappedArchive = async function archiveSession(sessionId, options) {
|
|
87
134
|
const res = await originalArchive.call(this, sessionId, options)
|
|
88
|
-
|
|
89
|
-
const sessionDir = iso.sessionPath(sessionId)
|
|
90
|
-
if (sessionDir) {
|
|
91
|
-
const empty = await isDirectoryEmpty(sessionDir).catch(() => false)
|
|
92
|
-
if (empty) {
|
|
93
|
-
iso.forgetSession(sessionId)
|
|
94
|
-
await removeSessionDir(sessionDir).catch(() => {})
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
135
|
+
await cleanEmptySessionDir(sessionId, iso, this).catch(() => {})
|
|
98
136
|
return res
|
|
99
137
|
}
|
|
138
|
+
registry.archiveSession = wrappedArchive
|
|
139
|
+
if (proto && typeof proto.archiveSession === 'function') {
|
|
140
|
+
proto.archiveSession = wrappedArchive
|
|
141
|
+
}
|
|
100
142
|
}
|
|
101
143
|
|
|
102
144
|
return () => {
|
|
103
145
|
if (typeof originalMatches === 'function') target.candidateMatches = originalMatches
|
|
104
146
|
if (typeof originalRemember === 'function') target.rememberSessionPath = originalRemember
|
|
105
147
|
if (typeof originalPath === 'function') target.sessionPath = originalPath
|
|
106
|
-
if (typeof originalArchive === 'function')
|
|
148
|
+
if (typeof originalArchive === 'function') {
|
|
149
|
+
registry.archiveSession = originalArchive
|
|
150
|
+
if (proto && typeof proto.archiveSession === 'function') {
|
|
151
|
+
proto.archiveSession = originalArchive
|
|
152
|
+
}
|
|
153
|
+
}
|
|
107
154
|
}
|
|
108
155
|
}
|
|
109
156
|
|
|
@@ -180,32 +227,25 @@ export function patchSessionCreate(ctx, iso) {
|
|
|
180
227
|
|
|
181
228
|
if (owner === undefined) return await originalCreate.call(this, request0)
|
|
182
229
|
|
|
183
|
-
// Check if request0.sessionId is an existing blank session.
|
|
184
|
-
//
|
|
185
|
-
// or is already tracked in isolation, drop it so a fresh unique session is minted,
|
|
186
|
-
// avoiding ApiSessionCwdConflict and duplicate directory naming.
|
|
230
|
+
// Check if request0.sessionId is an existing blank session minted under parent workspace cwd.
|
|
231
|
+
// If so, it cannot be adopted for sandbox cwd; drop it and mint a fresh ID.
|
|
187
232
|
let targetSessionId = request0.sessionId
|
|
188
233
|
if (targetSessionId !== undefined) {
|
|
189
|
-
const
|
|
190
|
-
|
|
234
|
+
const sessions = typeof ctx.get === 'function' ? ctx.get('sessions', false) : ctx.sessions
|
|
235
|
+
const agents = typeof ctx.get === 'function' ? ctx.get('agents', false) : ctx.agents
|
|
236
|
+
const live = sessions?.get?.(targetSessionId) ?? agents?.get?.(targetSessionId)
|
|
237
|
+
const existingCwd = live?.session?.header?.cwd
|
|
238
|
+
if (existingCwd !== undefined && existingCwd === owner) {
|
|
191
239
|
targetSessionId = undefined
|
|
192
240
|
} else {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (typeof query?.observeSession === 'function') {
|
|
202
|
-
try {
|
|
203
|
-
const obs = await query.observeSession(targetSessionId)
|
|
204
|
-
if (obs?.header?.cwd !== undefined) {
|
|
205
|
-
targetSessionId = undefined
|
|
206
|
-
}
|
|
207
|
-
} catch {}
|
|
208
|
-
}
|
|
241
|
+
const query = typeof ctx.get === 'function' ? ctx.get('sessionQuery', false) : ctx.sessionQuery
|
|
242
|
+
if (typeof query?.observeSession === 'function') {
|
|
243
|
+
try {
|
|
244
|
+
const obs = await query.observeSession(targetSessionId)
|
|
245
|
+
if (obs?.header?.cwd !== undefined && obs.header.cwd === owner) {
|
|
246
|
+
targetSessionId = undefined
|
|
247
|
+
}
|
|
248
|
+
} catch {}
|
|
209
249
|
}
|
|
210
250
|
}
|
|
211
251
|
}
|
|
@@ -245,7 +285,6 @@ export function patchSessionCreate(ctx, iso) {
|
|
|
245
285
|
if (!currentBase.includes(actualShort)) {
|
|
246
286
|
const correctDir = resolve(join(owner, `${SESSION_DIR_PREFIX}${actualShort}`))
|
|
247
287
|
await rename(fallbackCwd, correctDir).catch(() => {})
|
|
248
|
-
iso.recordRename?.(fallbackCwd, correctDir)
|
|
249
288
|
iso.recordSessionPath(created.sessionId, correctDir)
|
|
250
289
|
return created
|
|
251
290
|
}
|
|
@@ -266,7 +305,6 @@ export function patchSessionCreate(ctx, iso) {
|
|
|
266
305
|
const correctDir = resolve(join(owner, `${SESSION_DIR_PREFIX}${actualShort}`))
|
|
267
306
|
try {
|
|
268
307
|
await rename(cwd, correctDir)
|
|
269
|
-
iso.recordRename?.(cwd, correctDir)
|
|
270
308
|
cwd = correctDir
|
|
271
309
|
} catch {}
|
|
272
310
|
}
|
|
@@ -299,60 +337,40 @@ export function patchSessionCreate(ctx, iso) {
|
|
|
299
337
|
if (typeof originalDelete === 'function') {
|
|
300
338
|
controller().delete = async function del(req) {
|
|
301
339
|
const sessionId = req?.sessionId ?? (typeof req === 'string' ? req : undefined)
|
|
302
|
-
const sessionDir = sessionId ? iso.sessionPath(sessionId) : undefined
|
|
303
340
|
try {
|
|
304
341
|
return await originalDelete.call(this, req)
|
|
305
342
|
} finally {
|
|
306
|
-
if (
|
|
307
|
-
iso
|
|
308
|
-
await removeSessionDir(sessionDir)
|
|
343
|
+
if (sessionId) {
|
|
344
|
+
await cleanEmptySessionDir(sessionId, iso, registry())
|
|
309
345
|
}
|
|
310
346
|
}
|
|
311
347
|
}
|
|
312
348
|
}
|
|
313
349
|
|
|
350
|
+
// Hook workspaceController.archiveSession for frontend archive actions
|
|
351
|
+
const wsCtrl = ctx.root?.workspaceController ?? ctx.workspaceController
|
|
352
|
+
const originalWsArchive = wsCtrl?.archiveSession
|
|
353
|
+
if (typeof originalWsArchive === 'function') {
|
|
354
|
+
wsCtrl.archiveSession = async function archiveSession(request) {
|
|
355
|
+
const res = await originalWsArchive.call(this, request)
|
|
356
|
+
if (request?.sessionId) {
|
|
357
|
+
await cleanEmptySessionDir(request.sessionId, iso, registry())
|
|
358
|
+
}
|
|
359
|
+
return res
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
314
363
|
return () => {
|
|
315
364
|
controller().create = originalCreate
|
|
316
365
|
if (typeof originalRename === 'function') controller().rename = originalRename
|
|
317
366
|
if (typeof originalDelete === 'function') controller().delete = originalDelete
|
|
367
|
+
if (typeof originalWsArchive === 'function') wsCtrl.archiveSession = originalWsArchive
|
|
318
368
|
}
|
|
319
369
|
}
|
|
320
370
|
|
|
321
371
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* and caller shares the patched behavior.
|
|
325
|
-
* @param {object} obj - target instance or service
|
|
326
|
-
* @param {string} methodName - method to wrap
|
|
327
|
-
* @param {Function} wrapper - function taking original method and returning new method
|
|
328
|
-
* @returns {Function} disposer restoring all patched methods
|
|
329
|
-
*/
|
|
330
|
-
function patchMethod(obj, methodName, wrapper) {
|
|
331
|
-
if (!obj) return () => {}
|
|
332
|
-
const targets = []
|
|
333
|
-
let curr = obj
|
|
334
|
-
while (curr && curr !== Object.prototype) {
|
|
335
|
-
if (Object.prototype.hasOwnProperty.call(curr, methodName) || typeof curr[methodName] === 'function') {
|
|
336
|
-
targets.push({ target: curr, original: curr[methodName] })
|
|
337
|
-
}
|
|
338
|
-
curr = Object.getPrototypeOf(curr)
|
|
339
|
-
}
|
|
340
|
-
if (!targets.some(t => t.target === obj) && typeof obj[methodName] === 'function') {
|
|
341
|
-
targets.push({ target: obj, original: obj[methodName] })
|
|
342
|
-
}
|
|
343
|
-
for (const item of targets) {
|
|
344
|
-
item.target[methodName] = wrapper(item.original)
|
|
345
|
-
}
|
|
346
|
-
return () => {
|
|
347
|
-
for (const item of targets) {
|
|
348
|
-
item.target[methodName] = item.original
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Wrap ctx.fs (resolve, stat, lstat) so tool operations (write, edit, read, etc.) automatically
|
|
355
|
-
* translate pre-rename directory paths to their new renamed directory paths across all contexts.
|
|
372
|
+
* Wrap ctx.fs.resolve so tool operations (write, edit, read, etc.) automatically
|
|
373
|
+
* translate pre-rename directory paths to their new renamed directory paths.
|
|
356
374
|
* @param ctx - Cordis context
|
|
357
375
|
* @param iso - shared isolation state
|
|
358
376
|
* @returns disposer
|
|
@@ -369,68 +387,25 @@ export function patchFs(ctx, iso) {
|
|
|
369
387
|
}
|
|
370
388
|
|
|
371
389
|
const fs = getFs()
|
|
372
|
-
if (!fs) return () => {}
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
return async function resolve(path, options) {
|
|
380
|
-
const targetPath = typeof path === 'string' ? (iso.rewritePath?.(path) ?? path) : path
|
|
381
|
-
let targetOpts = options
|
|
382
|
-
if (options?.cwd && typeof options.cwd === 'string') {
|
|
383
|
-
const rewrittenCwd = iso.rewritePath?.(options.cwd) ?? options.cwd
|
|
384
|
-
if (rewrittenCwd !== options.cwd) {
|
|
385
|
-
targetOpts = { ...options, cwd: rewrittenCwd }
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return await originalResolve.call(this, targetPath, targetOpts)
|
|
389
|
-
}
|
|
390
|
-
}))
|
|
391
|
-
|
|
392
|
-
// Wrap fs.stat
|
|
393
|
-
disposers.push(patchMethod(fs, 'stat', (originalStat) => {
|
|
394
|
-
if (typeof originalStat !== 'function') return originalStat
|
|
395
|
-
return async function stat(target, signal) {
|
|
396
|
-
let effectiveTarget = target
|
|
397
|
-
if (target && typeof target === 'object' && typeof target.targetKey === 'string') {
|
|
398
|
-
const rewritten = iso.rewritePath?.(target.targetKey)
|
|
399
|
-
if (rewritten && rewritten !== target.targetKey) {
|
|
400
|
-
effectiveTarget = { ...target, targetKey: rewritten }
|
|
401
|
-
}
|
|
402
|
-
} else if (typeof target === 'string') {
|
|
403
|
-
effectiveTarget = iso.rewritePath?.(target) ?? target
|
|
404
|
-
}
|
|
405
|
-
return await originalStat.call(this, effectiveTarget, signal)
|
|
406
|
-
}
|
|
407
|
-
}))
|
|
408
|
-
|
|
409
|
-
// Wrap fs.lstat
|
|
410
|
-
disposers.push(patchMethod(fs, 'lstat', (originalLstat) => {
|
|
411
|
-
if (typeof originalLstat !== 'function') return originalLstat
|
|
412
|
-
return async function lstat(path, opts, signal) {
|
|
413
|
-
const targetPath = typeof path === 'string' ? (iso.rewritePath?.(path) ?? path) : path
|
|
414
|
-
let targetOpts = opts
|
|
415
|
-
if (opts?.cwd && typeof opts.cwd === 'string') {
|
|
416
|
-
const rewrittenCwd = iso.rewritePath?.(opts.cwd) ?? opts.cwd
|
|
417
|
-
if (rewrittenCwd !== opts.cwd) {
|
|
418
|
-
targetOpts = { ...opts, cwd: rewrittenCwd }
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return await originalLstat.call(this, targetPath, targetOpts, signal)
|
|
390
|
+
if (!fs || typeof fs.resolve !== 'function') return () => {}
|
|
391
|
+
|
|
392
|
+
const originalResolve = fs.resolve
|
|
393
|
+
fs.resolve = async function resolve(path, options) {
|
|
394
|
+
let target = path
|
|
395
|
+
if (typeof path === 'string') {
|
|
396
|
+
target = iso.rewritePath?.(path) ?? path
|
|
422
397
|
}
|
|
423
|
-
|
|
398
|
+
return await originalResolve.call(this, target, options)
|
|
399
|
+
}
|
|
424
400
|
|
|
425
401
|
return () => {
|
|
426
|
-
|
|
402
|
+
fs.resolve = originalResolve
|
|
427
403
|
}
|
|
428
404
|
}
|
|
429
405
|
|
|
430
406
|
/**
|
|
431
407
|
* Wrap workingDirectory service so all tool executions (write, edit, bash, etc.)
|
|
432
408
|
* immediately track renamed session working directories without throwing ENOENT.
|
|
433
|
-
* Patches get, ensure, and directory across instance and prototype hierarchy.
|
|
434
409
|
* @param ctx - Cordis context
|
|
435
410
|
* @param iso - shared isolation state
|
|
436
411
|
* @returns disposer
|
|
@@ -449,76 +424,48 @@ export function patchWorkingDirectory(ctx, iso) {
|
|
|
449
424
|
const wd = getWd()
|
|
450
425
|
if (!wd) return () => {}
|
|
451
426
|
|
|
452
|
-
const
|
|
427
|
+
const originalGet = wd.get
|
|
428
|
+
const originalEnsure = wd.ensure
|
|
453
429
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (typeof originalGet !== 'function') return originalGet
|
|
457
|
-
return function get(session) {
|
|
430
|
+
if (typeof originalGet === 'function') {
|
|
431
|
+
wd.get = function get(session) {
|
|
458
432
|
const sessionId = session?.id
|
|
459
433
|
if (sessionId) {
|
|
460
434
|
const isolated = iso.sessionPath(sessionId)
|
|
461
435
|
if (isolated !== undefined) {
|
|
462
|
-
if (session?.header) session.header.cwd = isolated
|
|
463
436
|
return isolated
|
|
464
437
|
}
|
|
465
438
|
}
|
|
466
|
-
|
|
467
|
-
const rewritten = iso.rewritePath?.(session.header.cwd)
|
|
468
|
-
if (rewritten && rewritten !== session.header.cwd) {
|
|
469
|
-
session.header.cwd = rewritten
|
|
470
|
-
return rewritten
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
const raw = originalGet.call(this, session)
|
|
474
|
-
return typeof raw === 'string' ? (iso.rewritePath?.(raw) ?? raw) : raw
|
|
439
|
+
return originalGet.call(this, session)
|
|
475
440
|
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
if (isolated !== undefined) {
|
|
491
|
-
const session = agent?.session
|
|
492
|
-
if (session) {
|
|
493
|
-
if (session.header) session.header.cwd = isolated
|
|
494
|
-
if (typeof session.append === 'function') {
|
|
495
|
-
try {
|
|
496
|
-
const handle = this.workingDirectoryProjectionHandle
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (typeof originalEnsure === 'function') {
|
|
444
|
+
wd.ensure = async function ensure(agent, signal) {
|
|
445
|
+
const sessionId = agent?.session?.id
|
|
446
|
+
if (sessionId) {
|
|
447
|
+
const isolated = iso.sessionPath(sessionId)
|
|
448
|
+
if (isolated !== undefined) {
|
|
449
|
+
const session = agent?.session
|
|
450
|
+
if (session) {
|
|
451
|
+
if (typeof session.append === 'function') {
|
|
452
|
+
const handle = wd.workingDirectoryProjectionHandle
|
|
497
453
|
const currentProj = handle?.stateOf?.(session)
|
|
498
454
|
if (currentProj !== isolated) {
|
|
499
455
|
session.append('working-directory/change', { cwd: isolated })
|
|
500
456
|
}
|
|
501
|
-
}
|
|
457
|
+
}
|
|
502
458
|
}
|
|
459
|
+
return isolated
|
|
503
460
|
}
|
|
504
|
-
return isolated
|
|
505
461
|
}
|
|
506
462
|
return await originalEnsure.call(this, agent, signal)
|
|
507
463
|
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Wrap wd.directory
|
|
511
|
-
disposers.push(patchMethod(wd, 'directory', (originalDirectory) => {
|
|
512
|
-
if (typeof originalDirectory !== 'function') return originalDirectory
|
|
513
|
-
return async function directory(path, cwd, signal) {
|
|
514
|
-
const targetPath = typeof path === 'string' ? (iso.rewritePath?.(path) ?? path) : path
|
|
515
|
-
const targetCwd = typeof cwd === 'string' ? (iso.rewritePath?.(cwd) ?? cwd) : cwd
|
|
516
|
-
return await originalDirectory.call(this, targetPath, targetCwd, signal)
|
|
517
|
-
}
|
|
518
|
-
}))
|
|
464
|
+
}
|
|
519
465
|
|
|
520
466
|
return () => {
|
|
521
|
-
|
|
467
|
+
if (typeof originalGet === 'function') wd.get = originalGet
|
|
468
|
+
if (typeof originalEnsure === 'function') wd.ensure = originalEnsure
|
|
522
469
|
}
|
|
523
470
|
}
|
|
524
471
|
|
|
@@ -581,14 +528,10 @@ export async function renameSessionDirToTitle(sessionId, title, iso, registry, c
|
|
|
581
528
|
liveAgent?.session
|
|
582
529
|
|
|
583
530
|
if (targetSession) {
|
|
584
|
-
if (targetSession.header) targetSession.header.cwd = targetDir
|
|
585
531
|
if (typeof targetSession.append === 'function') {
|
|
586
532
|
targetSession.append('working-directory/change', { cwd: targetDir })
|
|
587
533
|
}
|
|
588
534
|
}
|
|
589
|
-
if (liveAgent?.session?.header) {
|
|
590
|
-
liveAgent.session.header.cwd = targetDir
|
|
591
|
-
}
|
|
592
535
|
|
|
593
536
|
// 6. Post-rename cleanup guard: If currentDir is ever re-created by an in-flight tool call,
|
|
594
537
|
// merge files to targetDir and delete currentDir!
|
|
@@ -646,8 +589,11 @@ export function isolatedSessionsApi(ctx, iso) {
|
|
|
646
589
|
async cleanOrphans(options = {}) {
|
|
647
590
|
const registryInstance = registry()
|
|
648
591
|
const known = new Set()
|
|
592
|
+
const archived = new Set(registryInstance?.archivedSessionIds ?? [])
|
|
649
593
|
for (const ws of registryInstance?.list?.() ?? []) {
|
|
650
|
-
for (const id of ws.sessionIds ?? [])
|
|
594
|
+
for (const id of ws.sessionIds ?? []) {
|
|
595
|
+
if (!archived.has(id)) known.add(id)
|
|
596
|
+
}
|
|
651
597
|
}
|
|
652
598
|
return await cleanOrphans({
|
|
653
599
|
roots: iso.roots(),
|
package/package.json
CHANGED