@heixui/dsh-session-suite 0.6.1 → 0.7.0
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/client.js +29 -7
- package/cordis.patch.yml +13 -2
- package/index.js +6 -16
- package/lib/api.js +49 -0
- package/lib/memory-host.js +12 -98
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -2937,8 +2937,18 @@ window.__ModuleLoader__.load({
|
|
|
2937
2937
|
// settings page said something else entirely. 'all' now means every store.
|
|
2938
2938
|
useEffect(() => {
|
|
2939
2939
|
let alive = true
|
|
2940
|
+
// Whether the long-term memory engine is running at all.
|
|
2941
|
+
//
|
|
2942
|
+
// It is OFF by default because it collides with dsh-fish-memory: both inject at
|
|
2943
|
+
// `systemPrompt.context({ order: 900 })`, so two memory blocks per request and
|
|
2944
|
+
// double the tokens. When it is off the API answers LONGTERM_DISABLED and the
|
|
2945
|
+
// panel must show only the session half — a memory row that opens onto a
|
|
2946
|
+
// disabled engine is worse than no row.
|
|
2940
2947
|
Promise.all([
|
|
2941
|
-
call({ action: 'longterm.list', scope: 'all' }).
|
|
2948
|
+
call({ action: 'longterm.list', scope: 'all' }).then(
|
|
2949
|
+
value => ({ enabled: true, value }),
|
|
2950
|
+
error => ({ enabled: error?.code !== 'LONGTERM_DISABLED', value: [] }),
|
|
2951
|
+
),
|
|
2942
2952
|
call({ action: 'list' }).catch(() => []),
|
|
2943
2953
|
// Per-workspace breakdown, so the totals can be explained rather than
|
|
2944
2954
|
// merely displayed. The counts above are sums; without this the user
|
|
@@ -2946,15 +2956,16 @@ window.__ModuleLoader__.load({
|
|
|
2946
2956
|
// made the earlier 17/21/27/51 mismatch look like a bug.
|
|
2947
2957
|
call({ action: 'longterm.workspaces' }).catch(() => []),
|
|
2948
2958
|
])
|
|
2949
|
-
.then(([
|
|
2959
|
+
.then(([memory, sessions, workspaces]) => {
|
|
2950
2960
|
if (!alive) return
|
|
2951
2961
|
const items = Array.isArray(sessions) ? sessions : []
|
|
2952
2962
|
const stores = Array.isArray(workspaces) ? workspaces : []
|
|
2953
|
-
const memoryRows = Array.isArray(
|
|
2963
|
+
const memoryRows = Array.isArray(memory?.value) ? memory.value : []
|
|
2954
2964
|
// The global store's size, so the breakdown adds up to the total instead
|
|
2955
2965
|
// of leaving the difference unexplained.
|
|
2956
2966
|
const globalRows = memoryRows.filter(row => row.scope === 'global').length
|
|
2957
2967
|
setCounts({
|
|
2968
|
+
memoryEnabled: memory?.enabled === true,
|
|
2958
2969
|
memories: memoryRows.length,
|
|
2959
2970
|
memoryActive: memoryRows.filter(row => row.archived !== true).length,
|
|
2960
2971
|
memoryArchived: memoryRows.filter(row => row.archived === true).length,
|
|
@@ -3042,8 +3053,14 @@ window.__ModuleLoader__.load({
|
|
|
3042
3053
|
...[
|
|
3043
3054
|
['panel.statSessions', counts?.sessions, 'panel.statSessionsHint'],
|
|
3044
3055
|
['panel.statSnapshots', counts?.snapshots, 'panel.statSnapshotsHint'],
|
|
3045
|
-
|
|
3046
|
-
|
|
3056
|
+
// Memory figures only exist when the engine is on. Showing a zero here
|
|
3057
|
+
// would claim "you have no memories" when the truth is "memory is off".
|
|
3058
|
+
...(counts?.memoryEnabled === true
|
|
3059
|
+
? [
|
|
3060
|
+
['panel.statMemories', counts?.memories, 'panel.statMemoriesHint'],
|
|
3061
|
+
['panel.statArchived', counts?.archived, 'panel.statArchivedHint'],
|
|
3062
|
+
]
|
|
3063
|
+
: [['panel.statArchived', counts?.archived, 'panel.statArchivedHint']]),
|
|
3047
3064
|
].map(([label, value, hint]) =>
|
|
3048
3065
|
h(
|
|
3049
3066
|
'div',
|
|
@@ -3055,7 +3072,8 @@ window.__ModuleLoader__.load({
|
|
|
3055
3072
|
),
|
|
3056
3073
|
),
|
|
3057
3074
|
// The decomposition, so the total is checkable rather than a number to trust.
|
|
3058
|
-
|
|
3075
|
+
// Only when memory is on: it explains the memory total.
|
|
3076
|
+
counts?.memoryEnabled === true && counts?.stores?.length
|
|
3059
3077
|
? h(
|
|
3060
3078
|
'div',
|
|
3061
3079
|
{ className: 'dss-breakdown' },
|
|
@@ -3085,7 +3103,11 @@ window.__ModuleLoader__.load({
|
|
|
3085
3103
|
'div',
|
|
3086
3104
|
{ className: 'dss-nav' },
|
|
3087
3105
|
row('snapshots', t('panel.snapshots'), t('panel.snapshotsHint'), counts?.snapshots),
|
|
3088
|
-
|
|
3106
|
+
// The memory screen is omitted entirely when the engine is off: it would
|
|
3107
|
+
// open onto a disabled backend and report LONGTERM_DISABLED in red.
|
|
3108
|
+
counts?.memoryEnabled === true
|
|
3109
|
+
? row('memory', t('panel.memory'), t('panel.memoryHint'), counts?.memories)
|
|
3110
|
+
: null,
|
|
3089
3111
|
row('archived', t('panel.archived'), t('panel.archivedHint'), counts?.archived),
|
|
3090
3112
|
),
|
|
3091
3113
|
h(
|
package/cordis.patch.yml
CHANGED
|
@@ -34,8 +34,19 @@
|
|
|
34
34
|
stripStepTags: true
|
|
35
35
|
|
|
36
36
|
# ---------- long-term memory ----------
|
|
37
|
-
#
|
|
38
|
-
|
|
37
|
+
# OFF by default. The session half of this bundle (snapshots, rewind,
|
|
38
|
+
# delete, share, export, archive) does not need it and is always on.
|
|
39
|
+
#
|
|
40
|
+
# Why off: dsh-fish-memory registers the SAME injection point —
|
|
41
|
+
# `systemPrompt.context({ order: 900 })` — so with both plugins enabled
|
|
42
|
+
# every request carries two memory blocks. That doubles the injected
|
|
43
|
+
# tokens and hands the model two possibly-contradictory fact sets.
|
|
44
|
+
# fish-memory's implicit extraction demonstrably works; ours never
|
|
45
|
+
# produced a single suggestion across seven fix attempts and, in the
|
|
46
|
+
# last one, crashed the Host with a 500ms polling timer. The memory
|
|
47
|
+
# engine therefore stays available but dormant: set this to true only
|
|
48
|
+
# after disabling fish-memory's injection.
|
|
49
|
+
longTermMemory: false
|
|
39
50
|
# Model chain used for implicit extraction (one cheap call per turn).
|
|
40
51
|
# Tried in order until one answers; a 429 or an unreachable provider just
|
|
41
52
|
# moves on to the next entry. All of these are free tiers.
|
package/index.js
CHANGED
|
@@ -28,24 +28,14 @@ import { stripStepTagsFromStream } from './lib/step-tag-filter.js'
|
|
|
28
28
|
export const name = 'session-suite'
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* The HTTP carrier is the only hard requirement; everything else degrades gracefully.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* queue empty for six rounds of fixes.
|
|
38
|
-
*
|
|
39
|
-
* dsh-fish-memory (16k downloads) solves the same problem by injecting `agents`, taking
|
|
40
|
-
* one with `ctx.agents.get(sid)`, and listening on `agent.ctx` — the agent's own context.
|
|
41
|
-
* Its source records the trap in a comment: the Cordis events that exist are
|
|
42
|
-
* `agent/inbox/inserted` and `agent/turn-stopping`, NOT `user/message` /
|
|
43
|
-
* `assistant/message` (those are SessionEvent types carried by `session/event`).
|
|
44
|
-
*
|
|
45
|
-
* Everything else stays optional: the HTTP carrier is still the only hard requirement,
|
|
46
|
-
* and a Host without `agents` degrades to no implicit extraction rather than failing.
|
|
33
|
+
* 0.6.2: `agents` and `timer` were added for an implicit-extraction polling loop. That
|
|
34
|
+
* loop crashed the Host with `cannot get property "timer" without inject` on every
|
|
35
|
+
* 500ms tick. Both are removed. Do not re-add them without also proving the tick body
|
|
36
|
+
* cannot throw and that the timer is disposed with the effect.
|
|
47
37
|
*/
|
|
48
|
-
export const inject = ['connection'
|
|
38
|
+
export const inject = ['connection']
|
|
49
39
|
|
|
50
40
|
/**
|
|
51
41
|
* @param {any} ctx plugin context
|
package/lib/api.js
CHANGED
|
@@ -10,6 +10,31 @@
|
|
|
10
10
|
/** Absolute path Connection registers. */
|
|
11
11
|
export const API_PATH = '/api/session-suite'
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Every action that needs the long-term memory engine.
|
|
15
|
+
*
|
|
16
|
+
* Kept as an explicit set rather than an `action.startsWith('longterm.')` test so that a
|
|
17
|
+
* misspelled action still fails as an unknown action (400) instead of being reported as
|
|
18
|
+
* "memory is disabled" — the second answer would hide a typo.
|
|
19
|
+
*/
|
|
20
|
+
export const LONGTERM_ACTIONS = new Set([
|
|
21
|
+
'longterm.list',
|
|
22
|
+
'longterm.remember',
|
|
23
|
+
'longterm.update',
|
|
24
|
+
'longterm.remove',
|
|
25
|
+
'longterm.recall',
|
|
26
|
+
'longterm.explain',
|
|
27
|
+
'longterm.pending',
|
|
28
|
+
'longterm.settle',
|
|
29
|
+
'longterm.maintain',
|
|
30
|
+
'longterm.status',
|
|
31
|
+
'longterm.workspaces',
|
|
32
|
+
'longterm.models',
|
|
33
|
+
'longterm.stats',
|
|
34
|
+
'longterm.settings',
|
|
35
|
+
'longterm.export',
|
|
36
|
+
])
|
|
37
|
+
|
|
13
38
|
const MAX_BODY_BYTES = 256 * 1024
|
|
14
39
|
|
|
15
40
|
/**
|
|
@@ -63,6 +88,30 @@ async function handle(service, deps, request) {
|
|
|
63
88
|
* @returns {Promise<Response>} response
|
|
64
89
|
*/
|
|
65
90
|
async function respond(service, deps, action, input) {
|
|
91
|
+
// The long-term memory engine is OPTIONAL and off by default.
|
|
92
|
+
//
|
|
93
|
+
// It is off because it collides with dsh-fish-memory: both register
|
|
94
|
+
// `systemPrompt.context({ order: 900 })`, so with both installed every request
|
|
95
|
+
// carries two memory blocks — double the tokens and two possibly-contradictory
|
|
96
|
+
// sets of facts. fish-memory's implicit extraction works and ours never did, so
|
|
97
|
+
// the memory half of this bundle is opt-in and the session half is always on.
|
|
98
|
+
//
|
|
99
|
+
// When the engine is absent every longterm.* action must answer cleanly rather
|
|
100
|
+
// than throw. `service.longTerm` is undefined in that case, and `await
|
|
101
|
+
// undefined.list(...)` would surface as an opaque 500 in the panel.
|
|
102
|
+
//
|
|
103
|
+
// The guard checks the action against a KNOWN list first: an action that does
|
|
104
|
+
// not exist is still a 400 (a typo must be loud), and only a real action is
|
|
105
|
+
// reported as disabled. Returning "disabled" for a nonexistent action would
|
|
106
|
+
// silently hide a wrong action name.
|
|
107
|
+
if (!service.longTerm && LONGTERM_ACTIONS.has(action)) {
|
|
108
|
+
return json({
|
|
109
|
+
ok: false,
|
|
110
|
+
error: 'long-term memory is disabled in this profile',
|
|
111
|
+
code: 'LONGTERM_DISABLED',
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
66
115
|
switch (action) {
|
|
67
116
|
case 'ping':
|
|
68
117
|
return json({ ok: true, data: { ready: true, version: 1 } })
|
package/lib/memory-host.js
CHANGED
|
@@ -261,106 +261,20 @@ export function installMemory({ ctx, root, config, log }) {
|
|
|
261
261
|
),
|
|
262
262
|
)
|
|
263
263
|
|
|
264
|
-
// --- 2b. Per-agent extraction trigger
|
|
264
|
+
// --- 2b. Per-agent extraction trigger: REMOVED (0.6.2) -----------------
|
|
265
265
|
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
266
|
+
// The 500ms agent-poll was the source of a Host crash: its timer callback
|
|
267
|
+
// called `ctx.interval` / `ctx.get("timer")` on a context that had not
|
|
268
|
+
// injected timer, producing `TypeError: cannot get property "timer" without
|
|
269
|
+
// inject` on EVERY tick until the Host died. The WorkBuddy transcript showed
|
|
270
|
+
// `at mount, _onTimeout, listOnTimeout` from our bundle.
|
|
268
271
|
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
// by `session/event`, not Cordis events — a trap fish-memory documents in a
|
|
276
|
-
// comment after hitting it themselves.
|
|
277
|
-
//
|
|
278
|
-
// The agent is reached through `ctx.agents`, so this requires the `agents` service;
|
|
279
|
-
// index.js now injects it. Without it the feature degrades to off rather than throwing.
|
|
280
|
-
safe('per-agent extraction hook', () => {
|
|
281
|
-
const agents = ctx.get?.('agents')
|
|
282
|
-
if (!agents) return
|
|
283
|
-
|
|
284
|
-
/** @param {any} agent live agent @returns {void} */
|
|
285
|
-
const mountFor = agent => {
|
|
286
|
-
const agentCtx = agent?.ctx
|
|
287
|
-
if (!agentCtx || typeof agentCtx.on !== 'function') return
|
|
288
|
-
// One listener per agent; the context disposes it with the agent.
|
|
289
|
-
agentCtx.on('agent/turn-stopping', () => {
|
|
290
|
-
const sessionId = agent?.sessionId
|
|
291
|
-
if (typeof sessionId !== 'string' || !sessionId) {
|
|
292
|
-
extractionTrace.reason = `turn-stopping fired but agent.sessionId was ${typeof sessionId}`
|
|
293
|
-
publishExtractionTrace(engine)
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
|
-
// The model choice lives in settings.json, not in the patch config.
|
|
297
|
-
void engine
|
|
298
|
-
.settings()
|
|
299
|
-
.then(settings => {
|
|
300
|
-
const chosen = settings?.implicitModel ?? config?.implicitModel
|
|
301
|
-
if (!chosen || chosen === 'off') {
|
|
302
|
-
extractionTrace.reason = 'no implicit model is selected in settings'
|
|
303
|
-
publishExtractionTrace(engine)
|
|
304
|
-
return undefined
|
|
305
|
-
}
|
|
306
|
-
const cwd = resolveCwd(ctx, agent)
|
|
307
|
-
return extractAfterTurn({ ctx, engine, sessionId, cwd, log, config })
|
|
308
|
-
})
|
|
309
|
-
.finally(() => publishExtractionTrace(engine))
|
|
310
|
-
.catch(error => {
|
|
311
|
-
extractionTrace.stage = 'threw'
|
|
312
|
-
extractionTrace.reason = errorMessage(error)
|
|
313
|
-
publishExtractionTrace(engine)
|
|
314
|
-
log('implicit memory extraction failed', error)
|
|
315
|
-
})
|
|
316
|
-
})
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Mount by POLLING, not by an event.
|
|
320
|
-
//
|
|
321
|
-
// The first version listened for `agent/created`, which never delivered a usable
|
|
322
|
-
// agent — the hook stayed unmounted and the trace stayed null, the same symptom as
|
|
323
|
-
// every earlier attempt. dsh-fish-memory does not use an event either: it runs
|
|
324
|
-
// `ctx.effect(() => ctx.interval(tick, 500))` and calls `agents.list()` every half
|
|
325
|
-
// second, mounting on any agent it has not seen yet. That is what makes its version
|
|
326
|
-
// work, and its source notes the tradeoff — the tick is cheap because `agents.list()`
|
|
327
|
-
// reads memory.
|
|
328
|
-
//
|
|
329
|
-
// Polling also handles the case an event cannot: the agent instance is REPLACED when
|
|
330
|
-
// a session is resumed, a plugin reloads, or the Host rebuilds it. A one-shot
|
|
331
|
-
// listener dies with the old instance; a poll re-mounts on the new one.
|
|
332
|
-
const mounted = new WeakSet()
|
|
333
|
-
const tick = () => {
|
|
334
|
-
let live = []
|
|
335
|
-
try {
|
|
336
|
-
live = agents.list?.() ?? []
|
|
337
|
-
} catch {
|
|
338
|
-
return
|
|
339
|
-
}
|
|
340
|
-
for (const agent of live) {
|
|
341
|
-
if (!agent || mounted.has(agent)) continue
|
|
342
|
-
// Subagents only do one job; a memory store for them is wasted work.
|
|
343
|
-
if (agent.origin === 'subagent' || agent.delegationDepth > 0) continue
|
|
344
|
-
mounted.add(agent)
|
|
345
|
-
try {
|
|
346
|
-
mountFor(agent)
|
|
347
|
-
} catch (error) {
|
|
348
|
-
log('could not mount the extraction hook on an agent', error)
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
// First pass immediately, so an already-running session is covered without waiting.
|
|
353
|
-
tick()
|
|
354
|
-
// Then every half second. `ctx.interval` is the Cordis timer facility; fall back to
|
|
355
|
-
// setInterval when it is absent so the feature still works on a leaner Host.
|
|
356
|
-
if (typeof ctx.interval === 'function') ctx.effect(() => ctx.interval(tick, 500))
|
|
357
|
-
else if (typeof setInterval === 'function') {
|
|
358
|
-
const handle = setInterval(tick, 500)
|
|
359
|
-
handle.unref?.()
|
|
360
|
-
ctx.effect(() => () => clearInterval(handle))
|
|
361
|
-
}
|
|
362
|
-
})
|
|
363
|
-
|
|
272
|
+
// A recurring timer that can throw on every tick is not an acceptable design
|
|
273
|
+
// for a plugin that runs inside a shared Host process. This whole trigger is
|
|
274
|
+
// removed, and `agents`/`timer` are dropped from inject. Implicit extraction
|
|
275
|
+
// stays off until a trigger can be proven safe. Do not re-add a polling loop
|
|
276
|
+
// without (1) injecting timer, (2) wrapping the tick body in try/catch, and
|
|
277
|
+
// (3) clearing the timer on dispose.
|
|
364
278
|
// --- 3. Implicit extraction after a turn --------------------------------
|
|
365
279
|
safe('implicit extraction', () => {
|
|
366
280
|
// A startup marker, recorded before the listener is even registered.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heixui/dsh-session-suite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Session checkpoints for DSH: rewind a conversation with its files and memory, permanent session deletion, and shareable exports",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|