@try-works/dsh-memory 0.1.11 → 0.1.12

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tasks.js +44 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@try-works/dsh-memory",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Git-backed, workspace-scoped memory for the DeepSeek Harness (global layer + per-workspace layers)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/tasks.js CHANGED
@@ -400,9 +400,13 @@ export function readScheduleStore(path) {
400
400
  * A task whose kind we do not recognise is SKIPPED, not written: storing it would mean restoring
401
401
  * it later without understanding it. It is reported, so the omission is visible.
402
402
  */
403
- export function backupTasks(root, { schedulePath, now = Date.now(), nowIso } = {}) {
403
+ export function backupTasks(root, { schedulePath, now = Date.now(), nowIso, sessions } = {}) {
404
404
  const at = nowIso ?? new Date(now).toISOString()
405
405
  const live = readScheduleStore(schedulePath ?? defaultSchedulePath())
406
+ // Read once per pass, not per task: it is a directory walk, and every row asks the same question.
407
+ // Injectable so a test can state the world instead of inheriting this machine's: `null` means
408
+ // the question cannot be answered and no row is called orphaned.
409
+ const sessionSet = sessions === undefined ? sessionIdsOnDisk() : sessions
406
410
  const before = listTasks(root)
407
411
  if (live.error !== null) {
408
412
  // The index is still rewritten, so a page from an earlier pass cannot sit on disk describing a
@@ -429,7 +433,11 @@ export function backupTasks(root, { schedulePath, now = Date.now(), nowIso } = {
429
433
  // A held-back task stays held back across every backup: that state is the user's decision, and
430
434
  // a routine pass must not overwrite it with a derived one.
431
435
  const held = previous && previous.stateKnown && previous.state === TASK_STATES.PAUSED
432
- const derived = deriveState({ stored: held ? TASK_STATES.PAUSED : null, live: task, now })
436
+ // Without this the ORPHANED state exists in the vocabulary and is never produced by any real
437
+ // pass - reachable from a unit test and from nothing else. `sessions === null` means the
438
+ // sessions directory is absent and we cannot tell, so the row is left as active.
439
+ const ownerResolvable = sessionSet === null || task.sessionId === undefined ? true : sessionSet.has(task.sessionId)
440
+ const derived = deriveState({ stored: held ? TASK_STATES.PAUSED : null, live: task, now, ownerResolvable })
433
441
  writeTask(root, record, derived.state, previous ? previous.backedUpAt : at)
434
442
  if (previous) refreshed += 1
435
443
  else written += 1
@@ -542,3 +550,37 @@ export function planRestore(root, { liveTasks = [], now = Date.now() } = {}) {
542
550
  }
543
551
  return { add, present, paused, refused, unreadable }
544
552
  }
553
+
554
+ /** The directory DSH keeps session records in, beside the schedule store's home. */
555
+ export function sessionsRoot(home = homedir()) {
556
+ const configured = process.env.DSH_HOME
557
+ const dshHome = configured && configured !== '' ? configured : join(home, '.dsh')
558
+ return join(dshHome, 'sessions')
559
+ }
560
+
561
+ /**
562
+ * Every session id that still has a record on disk.
563
+ *
564
+ * Used to tell ORPHANED from ACTIVE. A live task is armed against the session that created it, and
565
+ * the runtime resolves an agent from that id before every delivery (schedule runtime.ts:101) - so
566
+ * a task whose session is gone is a row that will never fire while still occupying the store. The
567
+ * backup cannot see the runtime, but it can see whether the session still exists.
568
+ *
569
+ * @returns a Set, or null when the sessions directory is absent
570
+ *
571
+ * Null rather than an empty set on purpose: no sessions directory means we cannot tell, and an
572
+ * empty set would mark every task on the machine orphaned. Refusing to guess is the rule here.
573
+ */
574
+ export function sessionIdsOnDisk(home = homedir()) {
575
+ const root = sessionsRoot(home)
576
+ if (!existsSync(root)) return null
577
+ const ids = new Set()
578
+ let projects
579
+ try { projects = readdirSync(root) } catch { return null }
580
+ for (const project of projects) {
581
+ let sessions
582
+ try { sessions = readdirSync(join(root, project)) } catch { continue }
583
+ for (const id of sessions) ids.add(id)
584
+ }
585
+ return ids
586
+ }