@try-works/dsh-memory 0.1.14 → 0.1.16
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/package.json +1 -1
- package/src/SYSTEM.template.md +39 -0
- package/src/host.js +12 -6
- package/src/tools.js +5 -2
package/package.json
CHANGED
package/src/SYSTEM.template.md
CHANGED
|
@@ -254,6 +254,45 @@ Sync failures are never swallowed: a failed pull or push is logged, recorded in
|
|
|
254
254
|
surfaced by `status()`, reported by `memory_sync`, and leaves a `state/CONFLICT.md` marker that
|
|
255
255
|
the next clean sync clears.
|
|
256
256
|
|
|
257
|
+
## 9b. The scheduled tasks layer
|
|
258
|
+
|
|
259
|
+
`global/tasks/` is not a memory layer and holds no memories. It is a portable backup of the DSH
|
|
260
|
+
scheduled tasks on this machine, and it exists because the schedule store is the only copy of them:
|
|
261
|
+
it is in no git repository, and the host DELETES a task when the session that owns it stops. A task
|
|
262
|
+
can therefore vanish with nothing to observe.
|
|
263
|
+
|
|
264
|
+
One JSON file per task, plus a regenerated `index.md`. A file holds the task's record - id, kind,
|
|
265
|
+
title, prompt, and the selector that says when it runs - and its lifecycle state. The owning session
|
|
266
|
+
and the delivery history are deliberately NOT stored: they are machine-local, and carrying a session
|
|
267
|
+
id to another machine would make a restored task fail, because the session it names does not exist
|
|
268
|
+
there.
|
|
269
|
+
|
|
270
|
+
Because the task files live in this store and are not gitignored, a push carries them to your other
|
|
271
|
+
machines, where they are re-armed on boot.
|
|
272
|
+
|
|
273
|
+
The states, and what each one means:
|
|
274
|
+
|
|
275
|
+
| state | meaning |
|
|
276
|
+
| --- | --- |
|
|
277
|
+
| `active` | armed in the live store, waiting for its next occurrence |
|
|
278
|
+
| `pending` | in the backup, not in the live store; a restore re-arms it |
|
|
279
|
+
| `paused` | held back on purpose. Every automatic pass leaves it alone |
|
|
280
|
+
| `stopped` | was armed, and its owning session stopped, so the host deleted it |
|
|
281
|
+
| `failed` | was due and has still not delivered. The host records no failure of its own, so this is what is observable |
|
|
282
|
+
| `orphaned` | armed, but the session that owns it no longer exists, so it can never fire |
|
|
283
|
+
| `skipped` | captured, but its kind is not one we can re-arm, so a restore refuses it |
|
|
284
|
+
| `done` | delivered, or its recurrence is exhausted |
|
|
285
|
+
|
|
286
|
+
Two cadences, both off the write path. The **backup** runs with the sync cadence and is additive:
|
|
287
|
+
it never deletes, so a stale backup cannot wipe a schedule. The **restore** runs once per process at
|
|
288
|
+
the first real session, because a task needs a live session to own it, and it binds each restored
|
|
289
|
+
task to that session - you are never asked which one. It matches tasks by CONTENT, not by id, since
|
|
290
|
+
the schedule service mints a fresh id on every create.
|
|
291
|
+
|
|
292
|
+
`memory_tasks` reports the whole picture and can `backup`, `restore`, `pause` or `resume` on
|
|
293
|
+
demand. Holding a task back is a decision, and it outranks every automatic pass: a `paused` task is
|
|
294
|
+
never silently re-armed.
|
|
295
|
+
|
|
257
296
|
## 10. Current state of this store
|
|
258
297
|
|
|
259
298
|
{{storeState}}
|
package/src/host.js
CHANGED
|
@@ -648,20 +648,24 @@ export function wireHost(ctx, store, cfg) {
|
|
|
648
648
|
* store is left exactly as it is, matched by CONTENT because create() mints a fresh id
|
|
649
649
|
* (schedule index.ts:256) and ids therefore cannot identify anything across a restore.
|
|
650
650
|
*/
|
|
651
|
-
async function maybeRestoreTasks(agent) {
|
|
652
|
-
|
|
653
|
-
|
|
651
|
+
async function maybeRestoreTasks(agent, { force = false } = {}) {
|
|
652
|
+
// EVERY early return REPORTS WHY. These used to return silently, and the tool that calls this
|
|
653
|
+
// reported success regardless - so a manual restore after the boot pass returned immediately at
|
|
654
|
+
// the once-per-process guard while telling the caller it had restored. An action that does
|
|
655
|
+
// nothing and says it worked is worse than one that fails.
|
|
656
|
+
if (restoreAttempted && !force) return { ran: false, reason: 'already restored in this process; pass force to run it again' }
|
|
657
|
+
if (!agent || isSubagentSession(agent.session)) return { ran: false, reason: 'a restored task needs a main session to own it' }
|
|
654
658
|
const sessionId = agent.session && agent.session.id
|
|
655
|
-
if (!sessionId) return
|
|
659
|
+
if (!sessionId) return { ran: false, reason: 'the session has no id to bind the task to' }
|
|
656
660
|
const schedule = ctx.get('schedule')
|
|
657
|
-
if (!schedule || typeof schedule.create !== 'function') return
|
|
661
|
+
if (!schedule || typeof schedule.create !== 'function') return { ran: false, reason: 'the schedule service is unavailable' }
|
|
658
662
|
restoreAttempted = true
|
|
659
663
|
try {
|
|
660
664
|
const path = cfg.schedulePath || defaultSchedulePath()
|
|
661
665
|
const live = readScheduleStore(path)
|
|
662
666
|
if (live.error !== null) {
|
|
663
667
|
logRun(store.root, 'tasks-restore', { ok: false, error: live.error })
|
|
664
|
-
return
|
|
668
|
+
return { ran: true, owner: sessionId, error: live.error }
|
|
665
669
|
}
|
|
666
670
|
const plan = planRestore(store.root, { liveTasks: live.tasks })
|
|
667
671
|
const added = []
|
|
@@ -684,8 +688,10 @@ export function wireHost(ctx, store, cfg) {
|
|
|
684
688
|
failed: failed.length,
|
|
685
689
|
detail: failed.length ? failed : undefined,
|
|
686
690
|
})
|
|
691
|
+
return { ran: true, owner: sessionId, added: added.length, present: plan.present.length, paused: plan.paused.length, refused: plan.refused.length, failed: failed.length }
|
|
687
692
|
} catch (error) {
|
|
688
693
|
logRun(store.root, 'tasks-restore', { ok: false, error: String((error && error.message) || error).slice(0, 160) })
|
|
694
|
+
return { ran: true, owner: sessionId, error: String((error && error.message) || error).slice(0, 160) }
|
|
689
695
|
}
|
|
690
696
|
}
|
|
691
697
|
|
package/src/tools.js
CHANGED
|
@@ -368,8 +368,11 @@ export function registerTools(ctx, store, extras = {}) {
|
|
|
368
368
|
changed = { written: r.written, refreshed: r.refreshed, stopped: r.stopped, skipped: r.skipped.length, error: r.error }
|
|
369
369
|
} else if (action === 'restore') {
|
|
370
370
|
if (!tasksRestore) throw new Error('memory_tasks: the restore is unavailable in this context')
|
|
371
|
-
|
|
372
|
-
|
|
371
|
+
// FORCED, because the once-per-process guard has almost certainly already been consumed by
|
|
372
|
+
// the boot restore - the manual action exists to run it again on demand.
|
|
373
|
+
// The outcome is REPORTED rather than assumed: this used to say { restored: true } whatever
|
|
374
|
+
// happened, including when nothing did.
|
|
375
|
+
changed = await tasksRestore(exec.agent, { force: true })
|
|
373
376
|
} else if (action === 'pause' || action === 'resume') {
|
|
374
377
|
if (!args.title) throw new Error('memory_tasks: ' + action + ' needs a title')
|
|
375
378
|
const { tasks } = listTasks(store.root)
|