@try-works/dsh-memory 0.1.10 → 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.
- package/STATUS.md +29 -1
- package/package.json +1 -1
- package/src/tasks.js +54 -4
package/STATUS.md
CHANGED
|
@@ -76,6 +76,27 @@ How to tell loaded from not: ask the host. `memory_recover` used to reply *"unav
|
|
|
76
76
|
context"* and now replies *"returned invalid output: value is not lossless JSON"*. The change of
|
|
77
77
|
ERROR is the proof the first fix is loaded and the tool now executes — a different failure, one
|
|
78
78
|
layer further along. The report fix in the last row is what remains pending.
|
|
79
|
+
### The tasks layer (new since the last restart)
|
|
80
|
+
|
|
81
|
+
A fifth layer, `tasks`, global only: `global/tasks/` holds one JSON file per DSH scheduled task
|
|
82
|
+
plus a regenerated `index.md`. It backs up the schedule store, which is otherwise the only copy of
|
|
83
|
+
your automated tasks and is in no git repository. The host DELETES a task when its owning session
|
|
84
|
+
stops, so a task can vanish with nothing to observe; the backup runs on the sync cadence and a
|
|
85
|
+
restore re-arms what is missing at the first session after boot.
|
|
86
|
+
|
|
87
|
+
Verified on disk: 386 tests pass. `backupTasks` pointed at `E:\dsh-memory` wrote the layer for
|
|
88
|
+
real — a second pass reported `written=0`, so it converges. `planRestore` against that layer and
|
|
89
|
+
the live `schedule.json` returned `add=0 present=2`, which is the correct answer rather than an
|
|
90
|
+
empty one: both tasks are in the live store, so a restore finds nothing to do.
|
|
91
|
+
|
|
92
|
+
NOT verified until a restart: that the plugin itself runs either pass. The cadence
|
|
93
|
+
(`maybeBackupTasks`) and the first-session restore (`maybeRestoreTasks`) have only been exercised
|
|
94
|
+
through the wiring tests, where the schedule service is a stub and the store is temporary. Writing
|
|
95
|
+
those files by calling the module is not the same claim as the plugin writing them.
|
|
96
|
+
|
|
97
|
+
Known limitation, by design: the host deletes a task when its owning session stops, so a restored
|
|
98
|
+
task lives as long as the session that adopted it. The boot restore is what re-arms them.
|
|
99
|
+
|
|
79
100
|
## NOT VERIFIED — treat these as unknown
|
|
80
101
|
|
|
81
102
|
| item | what is actually known | what would settle it |
|
|
@@ -105,6 +126,12 @@ the flag on did not change what the user sees.
|
|
|
105
126
|
|
|
106
127
|
### Blocked on a restart
|
|
107
128
|
|
|
129
|
+
- [ ] **Restart, then check the tasks layer.** It writes to ANOTHER plugin's storage through
|
|
130
|
+
that plugin's service, so it deserves a real observation rather than a passing suite. After
|
|
131
|
+
the restart, ask for `memory_tasks` and expect a report with `live: 2`, `backedUp: 2` and
|
|
132
|
+
`pending.present: 2`. Then check `logs/memory.log` for a `tasks-backup` entry and a
|
|
133
|
+
`tasks-restore` entry: the first should say `refreshed: 2`, the second `present: 2, added: 0`.
|
|
134
|
+
An `added` above zero would mean de-duplication failed and it duplicated a task.
|
|
108
135
|
- [ ] **Restart again.** The previous restart worked: `memory_recover` went from "unavailable in
|
|
109
136
|
this context" to executing. It now fails one layer further on, at output validation, which
|
|
110
137
|
is fixed on disk. Confirm by calling `memory_recover({})` and getting a report, not an error.
|
|
@@ -156,7 +183,8 @@ Kept because the pattern matters more than the list: every one of these was foun
|
|
|
156
183
|
| A test edit silently discarded | my own script re-read and overwrote the file before writing |
|
|
157
184
|
## Published
|
|
158
185
|
|
|
159
|
-
**`@try-works/dsh-memory@0.1.
|
|
186
|
+
**`@try-works/dsh-memory@0.1.10`** on npm (0.1.0 was the first release; the Memory tab and the
|
|
187
|
+
tasks layer each added several). The unscoped name `dsh-memory` was already taken by an
|
|
160
188
|
unrelated DeepSeek Harness memory plugin, so the package publishes under the existing `@try-works`
|
|
161
189
|
scope.
|
|
162
190
|
|
package/package.json
CHANGED
package/src/tasks.js
CHANGED
|
@@ -22,6 +22,7 @@ import { createHash } from 'node:crypto'
|
|
|
22
22
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
23
23
|
import { homedir } from 'node:os'
|
|
24
24
|
import { join } from 'node:path'
|
|
25
|
+
import { globalDir } from './store.js'
|
|
25
26
|
import { slugify } from './workspace-key.js'
|
|
26
27
|
|
|
27
28
|
/** Directory, under a scope root, holding one file per task plus the index. */
|
|
@@ -78,8 +79,15 @@ export function readState(value) {
|
|
|
78
79
|
return STATES.includes(value) ? value : null;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
/**
|
|
82
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Where the task files live: under the GLOBAL scope, not the store root.
|
|
84
|
+
*
|
|
85
|
+
* The layer is global-only by design - DSH tasks are not repository-scoped - so it belongs beside
|
|
86
|
+
* global/semantic.md and the rest. Resolving it against the root instead put it at <root>/tasks,
|
|
87
|
+
* a directory nothing else in the store knows about and no index or sync rule covers. Caught by
|
|
88
|
+
* looking for global/tasks in a real store and finding nothing there.
|
|
89
|
+
*/
|
|
90
|
+
export function tasksDir(root) { return join(globalDir(root), TASKS_DIR) }
|
|
83
91
|
|
|
84
92
|
/**
|
|
85
93
|
* The fields that say WHEN a task runs. Used to tell one task from another.
|
|
@@ -392,9 +400,13 @@ export function readScheduleStore(path) {
|
|
|
392
400
|
* A task whose kind we do not recognise is SKIPPED, not written: storing it would mean restoring
|
|
393
401
|
* it later without understanding it. It is reported, so the omission is visible.
|
|
394
402
|
*/
|
|
395
|
-
export function backupTasks(root, { schedulePath, now = Date.now(), nowIso } = {}) {
|
|
403
|
+
export function backupTasks(root, { schedulePath, now = Date.now(), nowIso, sessions } = {}) {
|
|
396
404
|
const at = nowIso ?? new Date(now).toISOString()
|
|
397
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
|
|
398
410
|
const before = listTasks(root)
|
|
399
411
|
if (live.error !== null) {
|
|
400
412
|
// The index is still rewritten, so a page from an earlier pass cannot sit on disk describing a
|
|
@@ -421,7 +433,11 @@ export function backupTasks(root, { schedulePath, now = Date.now(), nowIso } = {
|
|
|
421
433
|
// A held-back task stays held back across every backup: that state is the user's decision, and
|
|
422
434
|
// a routine pass must not overwrite it with a derived one.
|
|
423
435
|
const held = previous && previous.stateKnown && previous.state === TASK_STATES.PAUSED
|
|
424
|
-
|
|
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 })
|
|
425
441
|
writeTask(root, record, derived.state, previous ? previous.backedUpAt : at)
|
|
426
442
|
if (previous) refreshed += 1
|
|
427
443
|
else written += 1
|
|
@@ -534,3 +550,37 @@ export function planRestore(root, { liveTasks = [], now = Date.now() } = {}) {
|
|
|
534
550
|
}
|
|
535
551
|
return { add, present, paused, refused, unreadable }
|
|
536
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
|
+
}
|