@t4r71/dsh-dual-axis 0.1.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/LICENSE +21 -0
- package/README.md +147 -0
- package/cordis.patch.yml +68 -0
- package/lib/fs.js +419 -0
- package/lib/index.js +2591 -0
- package/lib/read-guard.js +1812 -0
- package/lib/session-store.js +978 -0
- package/lib/types/axis-command.d.ts +59 -0
- package/lib/types/axis-entry.d.ts +45 -0
- package/lib/types/axis.d.ts +131 -0
- package/lib/types/config.d.ts +173 -0
- package/lib/types/containment.d.ts +27 -0
- package/lib/types/content.d.ts +68 -0
- package/lib/types/fs-fence.d.ts +196 -0
- package/lib/types/fs.d.ts +15 -0
- package/lib/types/groups.d.ts +226 -0
- package/lib/types/index.d.ts +205 -0
- package/lib/types/read-guard.d.ts +34 -0
- package/lib/types/scope-prompt.d.ts +130 -0
- package/lib/types/scope.d.ts +108 -0
- package/lib/types/session-axes.d.ts +51 -0
- package/lib/types/session-store.d.ts +496 -0
- package/package.json +103 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE per-session axis store, outside the session log.
|
|
3
|
+
*
|
|
4
|
+
* The axes cannot live in a session's own log: this package's own event type is
|
|
5
|
+
* not in 0.1.7's `KNOWN_SESSION_EVENT_TYPES`, and `Session.append` takes no
|
|
6
|
+
* envelope option, so the event cannot be marked `ignorable` and a log carrying
|
|
7
|
+
* it is refused WHOLE by `validateStoredEvents`
|
|
8
|
+
* (`packages/session/session-persistence/src/storage-contract.ts:75-77`).
|
|
9
|
+
* A session this package had written to could therefore not be opened at all
|
|
10
|
+
* after a restart.
|
|
11
|
+
*
|
|
12
|
+
* The axes live instead in this package's OWN settings namespace,
|
|
13
|
+
* `dual-axis-sessions`, partitioned by session id. That namespace is a
|
|
14
|
+
* separate Loader entry from `dual-axis` because the row's namespace carries the
|
|
15
|
+
* settings page's read/write/defaultGroups form: an undeclared field inside that
|
|
16
|
+
* row would be projected into the row's form. This namespace declares exactly one
|
|
17
|
+
* field and the settings page never renders it.
|
|
18
|
+
*
|
|
19
|
+
* `sandbox/mode` REMAINS the write axis's legal landing place in the log — see
|
|
20
|
+
* `./session-axes.ts`, which mirrors the write base onto it. The log therefore
|
|
21
|
+
* still records the containment the operating-system layer enforces; what it no
|
|
22
|
+
* longer records is the path-level axis pair, which is this module's.
|
|
23
|
+
*
|
|
24
|
+
* A session with NO record is neither an error state nor a reason to hide a
|
|
25
|
+
* control: every live decision path reads through {@link SessionAxesStore.ensure},
|
|
26
|
+
* which answers {@link seedAxesFor} — the deployment seed a newly created session
|
|
27
|
+
* is pinned with — and hands that SAME value to the ONE repair write it
|
|
28
|
+
* schedules. Such a session therefore runs under the deployment's seed from its
|
|
29
|
+
* first read, not under {@link DEFAULT_AXES} until the record lands, and the pair
|
|
30
|
+
* a picker shows before the record exists is the pair the fence and the prompt
|
|
31
|
+
* are already enforcing (the client half recomputes that seed from the same row —
|
|
32
|
+
* see `sessionAxesSeed` in `@t4r71/dsh-dual-axis-ui`).
|
|
33
|
+
* {@link DEFAULT_AXES} is now only the answer {@link SessionAxesStore.getOr} gives
|
|
34
|
+
* a caller that holds no session to build a seed from.
|
|
35
|
+
*
|
|
36
|
+
* A session with no record that has ALSO never been used is a different case, and
|
|
37
|
+
* it is the one the workspace picker creates every time: reopening the same
|
|
38
|
+
* workspace reopens the same session id, so freezing it at creation would pin the
|
|
39
|
+
* axes of a conversation nobody has had to whatever the settings row said that
|
|
40
|
+
* day. Its record is therefore written when it is USED ({@link hasContent}), not
|
|
41
|
+
* when it is created: until then every read recomputes the seed from the row as
|
|
42
|
+
* it stands NOW, and the dropdown follows the settings page in real time.
|
|
43
|
+
*
|
|
44
|
+
* @module @t4r71/dsh-dual-axis/session-store
|
|
45
|
+
*/
|
|
46
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
47
|
+
import type { Dict } from '@deepseek-ai/cosmokit';
|
|
48
|
+
import z from '@deepseek-ai/schemastery';
|
|
49
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
50
|
+
import type { AxisScope, EffectiveScopes } from './axis.ts';
|
|
51
|
+
import type { WriteNarrowing } from './groups.ts';
|
|
52
|
+
/**
|
|
53
|
+
* The settings namespace holding every session's axis pair. It is a Loader entry
|
|
54
|
+
* id (`cordis.patch.yml`), which is what `SettingsForms.write` looks up.
|
|
55
|
+
*/
|
|
56
|
+
export declare const SESSION_AXES_NAMESPACE = "dual-axis-sessions";
|
|
57
|
+
/** The single Config field: session id -> axis pair. */
|
|
58
|
+
export declare const SESSION_AXES_FIELD = "axes";
|
|
59
|
+
/**
|
|
60
|
+
* One session's stored axis pair, keyed by session id inside
|
|
61
|
+
* {@link SESSION_AXES_FIELD}. Both members are the session's own preset, group
|
|
62
|
+
* references included; a group's rules are never copied here, so editing a group
|
|
63
|
+
* still reaches every session that references it.
|
|
64
|
+
*/
|
|
65
|
+
export interface SessionAxesRecord {
|
|
66
|
+
/** The read axis preset. */
|
|
67
|
+
readonly read: AxisScope;
|
|
68
|
+
/** The write axis preset. */
|
|
69
|
+
readonly write: AxisScope;
|
|
70
|
+
/**
|
|
71
|
+
* What the write-never-exceeds-read invariant removed from that write axis, as
|
|
72
|
+
* the resolution computed it.
|
|
73
|
+
*
|
|
74
|
+
* Stored rather than recomputed by the reader because only the host can compute
|
|
75
|
+
* it: the resolution canonicalizes every root with `realpath` and derives a
|
|
76
|
+
* `workspace` base from `os.tmpdir()`. A browser surface showing a narrowing
|
|
77
|
+
* the fence does not enforce — or missing one it does — is worse than showing
|
|
78
|
+
* none, so the display reads the resolution's own output instead of a second
|
|
79
|
+
* implementation of the path algebra.
|
|
80
|
+
*
|
|
81
|
+
* Absent for a record written before this member existed, and for a record
|
|
82
|
+
* whose write range the intersection left alone: the reader then shows no
|
|
83
|
+
* narrowing, which is what the fence enforced in both cases.
|
|
84
|
+
*/
|
|
85
|
+
readonly narrowing?: WriteNarrowing;
|
|
86
|
+
}
|
|
87
|
+
/** The whole stored field: session id -> pair. */
|
|
88
|
+
export type SessionAxesField = Readonly<Record<string, SessionAxesRecord>>;
|
|
89
|
+
/**
|
|
90
|
+
* This entry's composition config. The field is `.volatile()` because 0.1.7
|
|
91
|
+
* refuses an entry with no volatile field outright
|
|
92
|
+
* (`packages/settings/settings/src/index.ts:385-386`) and because path writes
|
|
93
|
+
* are refused for every non-volatile path (`:387-389`).
|
|
94
|
+
*
|
|
95
|
+
* `z.dict(z.any())` rather than a per-session object schema: the keys are session
|
|
96
|
+
* ids, which no schema can enumerate, and the VALUES are validated by
|
|
97
|
+
* {@link parseSessionAxesField}, where an unreadable axis becomes a throw rather
|
|
98
|
+
* than an invented boundary — the same rule the row's own axes follow.
|
|
99
|
+
*
|
|
100
|
+
* Annotated rather than inferred: `z.dict`'s output names `Dict` from
|
|
101
|
+
* `@deepseek-ai/cosmokit`, and declaration emit refuses a type it can only reach
|
|
102
|
+
* through a nested `node_modules` path (TS2883). The annotation states the same
|
|
103
|
+
* type through that package's own entry.
|
|
104
|
+
*/
|
|
105
|
+
export declare const Config: z<{
|
|
106
|
+
axes: Dict<any>;
|
|
107
|
+
}>;
|
|
108
|
+
/**
|
|
109
|
+
* The pair {@link SessionAxesStore.getOr} answers for a session with no stored
|
|
110
|
+
* record, and nothing else.
|
|
111
|
+
*
|
|
112
|
+
* It is deliberately NOT what a session with no record is held to any more:
|
|
113
|
+
* every live read goes through {@link SessionAxesStore.ensure}, which answers the
|
|
114
|
+
* deployment's seed ({@link seedAxesFor}), so the pair in force is the pair the
|
|
115
|
+
* session was created with rather than the built-in one. Reaching for this
|
|
116
|
+
* constant on a decision path would hold a session to a boundary the settings
|
|
117
|
+
* page never chose — narrower than an `all` default, wider than a `deny` one.
|
|
118
|
+
* @see SessionAxesStore.getOr
|
|
119
|
+
*/
|
|
120
|
+
export declare const DEFAULT_AXES: EffectiveScopes;
|
|
121
|
+
/**
|
|
122
|
+
* The axis pair a session with no stored record is REPAIRED to — the same seed a
|
|
123
|
+
* newly created session is pinned with.
|
|
124
|
+
*
|
|
125
|
+
* Repairing to the standing pair instead (`DEFAULT_AXES`) would be narrower but wrong: a
|
|
126
|
+
* session whose creation-time write was lost would keep the built-in defaults
|
|
127
|
+
* for the rest of its life, and the settings page's seed would never reach it.
|
|
128
|
+
* Repairing to the seed keeps `pin`'s retry semantics intact — the retry just
|
|
129
|
+
* happens on the next touch of the session instead of only at creation.
|
|
130
|
+
*
|
|
131
|
+
* The seed is computed from two synchronous reads: the settings row's
|
|
132
|
+
* `read`/`write`/`defaultGroups` (seeds for a session that has none of its own)
|
|
133
|
+
* and, for a subagent child, the parent's pair AS IT STANDS NOW. A parent with
|
|
134
|
+
* no record of its own yields `undefined` from {@link inheritedAxes}, which is
|
|
135
|
+
* the same answer `pin` gets, so parent and child agree in that case too.
|
|
136
|
+
* @param ctx - host context; carries the settings service the seed is read from.
|
|
137
|
+
* @param session - the session to seed.
|
|
138
|
+
* @param fallback - the composing plugin's own config, used only when no settings
|
|
139
|
+
* service is mounted (where the namespace cannot be written anyway).
|
|
140
|
+
* @returns the pair a newly created session would have been pinned with.
|
|
141
|
+
*/
|
|
142
|
+
export declare function seedAxesFor(ctx: Context, session: Session, fallback?: {
|
|
143
|
+
readonly axes: EffectiveScopes;
|
|
144
|
+
readonly groups: readonly string[];
|
|
145
|
+
}): EffectiveScopes;
|
|
146
|
+
/**
|
|
147
|
+
* The pure core of {@link seedAxesFor}: one settings row value, plus the pair a
|
|
148
|
+
* subagent child inherits, into the pair a session with no record is seeded with.
|
|
149
|
+
*
|
|
150
|
+
* It is TOTAL on purpose, and the client half mirrors it step for step
|
|
151
|
+
* (`sessionAxesSeed` in `@t4r71/dsh-dual-axis-ui`, pinned by
|
|
152
|
+
* `tests/seed-parity.spec.ts`): an unreadable row, and an unreadable inherited
|
|
153
|
+
* pair, answer {@link DEFAULT_AXES}. That is also what a live read answers — see
|
|
154
|
+
* {@link SessionAxesStore.ensure} — so the pair a picker shows for a session with
|
|
155
|
+
* no record and the pair the host enforces are one value rather than two.
|
|
156
|
+
* @param row - the settings row's value, untrusted: a human may have edited it.
|
|
157
|
+
* @param inherited - the parent's pair for a subagent child, or `undefined`.
|
|
158
|
+
* @returns the seed pair.
|
|
159
|
+
*/
|
|
160
|
+
export declare function seedPair(row: unknown, inherited: EffectiveScopes | undefined): EffectiveScopes;
|
|
161
|
+
/**
|
|
162
|
+
* The pair a **subagent child session** inherits, or `undefined` when this is not
|
|
163
|
+
* a child or its parent holds no record.
|
|
164
|
+
*
|
|
165
|
+
* A child does not read the settings page: 0.1.7 deliberately seeds a child with
|
|
166
|
+
* its parent's explicit per-session override, and the parent's STORED pair is the
|
|
167
|
+
* only copy of that override this package has. Returning `undefined` (rather than
|
|
168
|
+
* inventing one) leaves the caller on the settings seed, which is what the child
|
|
169
|
+
* would have received had the parent never overridden anything — and is exactly
|
|
170
|
+
* the pair an un-recorded parent is itself held to.
|
|
171
|
+
*
|
|
172
|
+
* The record is looked up by the id the child's header names. The parent's own
|
|
173
|
+
* Session object is NOT required: requiring it made the answer depend on whether
|
|
174
|
+
* the parent happened to be resident in this process, and the client half — which
|
|
175
|
+
* reads the same document by the same id — cannot observe residency, so the two
|
|
176
|
+
* would disagree for a child whose parent is not materialized.
|
|
177
|
+
* @param ctx - host context carrying the axis store.
|
|
178
|
+
* @param session - the session whose parent to read.
|
|
179
|
+
* @returns the parent's pair, or `undefined`.
|
|
180
|
+
*/
|
|
181
|
+
export declare function inheritedAxes(ctx: Context, session: Session): EffectiveScopes | undefined;
|
|
182
|
+
/**
|
|
183
|
+
* The one persistence read the sweep performs.
|
|
184
|
+
*
|
|
185
|
+
* Declared here rather than imported: this package resolves every
|
|
186
|
+
* `@deepseek-ai/*` import from its own `node_modules` (tsconfig.base.json declares
|
|
187
|
+
* no `paths`), and `@deepseek-ai/dsh-session-persistence` is not one of its
|
|
188
|
+
* dependencies, so a type-only import of it would not resolve.
|
|
189
|
+
* The member is declared OPTIONAL on purpose — a composition without persistence
|
|
190
|
+
* (an in-memory session store) must not fail this plugin's load, it must simply
|
|
191
|
+
* leave the sweep unable to judge.
|
|
192
|
+
*/
|
|
193
|
+
export interface SessionPersistenceLister {
|
|
194
|
+
/**
|
|
195
|
+
* List every stored session visible to this process.
|
|
196
|
+
* @returns one entry per stored session, carrying its header.
|
|
197
|
+
*/
|
|
198
|
+
list(): Promise<readonly {
|
|
199
|
+
header: {
|
|
200
|
+
id: string;
|
|
201
|
+
};
|
|
202
|
+
}[]>;
|
|
203
|
+
}
|
|
204
|
+
declare module '@deepseek-ai/cordis' {
|
|
205
|
+
interface Context {
|
|
206
|
+
/** The mounted persistence backend, when the composition has one. */
|
|
207
|
+
sessionPersistence?: SessionPersistenceLister;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** The axes this package writes and the settings document it writes them through. */
|
|
211
|
+
export interface SessionAxesWriter {
|
|
212
|
+
/** Project every configurable entry's current form values. */
|
|
213
|
+
describe(): readonly {
|
|
214
|
+
ns: string;
|
|
215
|
+
value: unknown;
|
|
216
|
+
}[];
|
|
217
|
+
/**
|
|
218
|
+
* Reset this namespace's volatile fields, then set the supplied ones.
|
|
219
|
+
* @param ns - the entry id.
|
|
220
|
+
* @param section - the complete form values to keep.
|
|
221
|
+
* @param expectedRevision - the revision `describe` reported, or none.
|
|
222
|
+
*/
|
|
223
|
+
replace(ns: string, section: object, expectedRevision?: number): Promise<void>;
|
|
224
|
+
}
|
|
225
|
+
/** How many times one write re-reads the revision after a conflict. */
|
|
226
|
+
export declare const AXES_WRITE_ATTEMPTS = 4;
|
|
227
|
+
/** Milliseconds before attempt N is issued, N counted from zero. */
|
|
228
|
+
export declare const AXES_WRITE_BACKOFF_MS: readonly number[];
|
|
229
|
+
/**
|
|
230
|
+
* A whole-document write was refused because another writer moved the settings
|
|
231
|
+
* document between this writer's read and its write.
|
|
232
|
+
*
|
|
233
|
+
* It is thrown, never swallowed: the axis a person just picked is not in force
|
|
234
|
+
* unless it was stored, and reporting success for a lost write would leave the
|
|
235
|
+
* fence enforcing a range the picker does not show.
|
|
236
|
+
*/
|
|
237
|
+
export declare class SessionAxesConflictError extends Error {
|
|
238
|
+
/** Stable machine code for callers that map failures to their own taxonomy. */
|
|
239
|
+
readonly code = "DUAL_AXIS_AXES_CONFLICT";
|
|
240
|
+
/** The namespace whose write was refused. */
|
|
241
|
+
readonly ns = "dual-axis-sessions";
|
|
242
|
+
/** Attempts made before giving up. */
|
|
243
|
+
readonly attempts: number;
|
|
244
|
+
/**
|
|
245
|
+
* @param cause - the last conflict the settings service raised.
|
|
246
|
+
* @param attempts - attempts made before giving up.
|
|
247
|
+
*/
|
|
248
|
+
constructor(cause: unknown, attempts: number);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Read one stored record into an axis pair, or throw naming the member that is
|
|
252
|
+
* unreadable.
|
|
253
|
+
* @param id - the session id the record belongs to, for the error message.
|
|
254
|
+
* @param value - the untrusted record.
|
|
255
|
+
* @returns the validated pair.
|
|
256
|
+
* @throws When either member is not a legal axis value.
|
|
257
|
+
*/
|
|
258
|
+
export declare function parseSessionAxesRecord(id: string, value: unknown): SessionAxesRecord;
|
|
259
|
+
/**
|
|
260
|
+
* Read the whole stored field. A human may have edited the settings document, so
|
|
261
|
+
* an unreadable entry throws instead of being dropped: dropping it would silently
|
|
262
|
+
* widen that session back to the deployment defaults.
|
|
263
|
+
* @param value - the untrusted `axes` field.
|
|
264
|
+
* @returns every stored pair, by session id.
|
|
265
|
+
* @throws When the field is not an object of readable records.
|
|
266
|
+
*/
|
|
267
|
+
export declare function parseSessionAxesField(value: unknown): Record<string, SessionAxesRecord>;
|
|
268
|
+
/** What one GC sweep removed. */
|
|
269
|
+
export interface SessionAxesSweep {
|
|
270
|
+
/** Session ids whose records were dropped. */
|
|
271
|
+
readonly removed: readonly string[];
|
|
272
|
+
/** How many records the store held before the sweep. */
|
|
273
|
+
readonly before: number;
|
|
274
|
+
}
|
|
275
|
+
/** Options for {@link SessionAxesStore}. */
|
|
276
|
+
export interface SessionAxesStoreOptions {
|
|
277
|
+
/** Attempts per write; defaults to {@link AXES_WRITE_ATTEMPTS}. */
|
|
278
|
+
readonly attempts?: number;
|
|
279
|
+
/** Backoff before attempt N; defaults to {@link AXES_WRITE_BACKOFF_MS}. */
|
|
280
|
+
readonly backoffMs?: readonly number[];
|
|
281
|
+
/** Delay used by {@link SessionAxesStore.sleep}; injectable so tests do not wait. */
|
|
282
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The read and write face of {@link SESSION_AXES_NAMESPACE}, plus the sweep that
|
|
286
|
+
* keeps it from growing with the sessions that no longer exist.
|
|
287
|
+
*
|
|
288
|
+
* Reads go through `settings.describe()`, the same projection the settings page
|
|
289
|
+
* renders from, and are memoized on the namespace's `revision` — which 0.1.7 bumps
|
|
290
|
+
* exactly when that entry's stored profile override changes
|
|
291
|
+
* (`packages/settings/settings/src/index.ts:311-317`). A decision path therefore
|
|
292
|
+
* pays one `describe()` per document change, not one per tool dispatch.
|
|
293
|
+
*/
|
|
294
|
+
export declare class SessionAxesStore {
|
|
295
|
+
private readonly ctx;
|
|
296
|
+
private readonly options;
|
|
297
|
+
private cachedRevision;
|
|
298
|
+
private cachedRecords;
|
|
299
|
+
/** Parsed records by the JSON spelling that produced them. */
|
|
300
|
+
private readonly bySpelling;
|
|
301
|
+
/**
|
|
302
|
+
* Session ids with a repair write outstanding (or already landed) since the
|
|
303
|
+
* record was found missing. Prevents one repair per tool dispatch while the
|
|
304
|
+
* whole-document write is in flight; cleared when a write FAILS so the next
|
|
305
|
+
* touch retries instead of leaving the session on the defaults forever.
|
|
306
|
+
*/
|
|
307
|
+
private readonly repairing;
|
|
308
|
+
/**
|
|
309
|
+
* The last narrowing report this store handed to a write, by session id. A
|
|
310
|
+
* decision path re-resolves on every touch, so this is what keeps it from
|
|
311
|
+
* queueing the same document write over and over; it is cleared when a write
|
|
312
|
+
* lands (or fails), never on a read.
|
|
313
|
+
*/
|
|
314
|
+
private readonly narrowingMemo;
|
|
315
|
+
/** Sessions with a narrowing write outstanding right now. */
|
|
316
|
+
private readonly narrowingWriting;
|
|
317
|
+
/**
|
|
318
|
+
* @param ctx - host context; the settings service is looked up, never injected,
|
|
319
|
+
* so a composition without settings still loads this package's fence.
|
|
320
|
+
* @param options - retry policy and the sleep used between attempts.
|
|
321
|
+
*/
|
|
322
|
+
constructor(ctx: Context, options?: SessionAxesStoreOptions);
|
|
323
|
+
/**
|
|
324
|
+
* This namespace's current settings row, or `undefined` when no settings
|
|
325
|
+
* service is mounted or the entry is not composed.
|
|
326
|
+
* @returns the descriptor carrying the value and the revision.
|
|
327
|
+
*/
|
|
328
|
+
private descriptor;
|
|
329
|
+
/**
|
|
330
|
+
* Every stored pair, by session id.
|
|
331
|
+
* @returns the parsed field.
|
|
332
|
+
* @throws When the document carries an unreadable record.
|
|
333
|
+
*/
|
|
334
|
+
records(): Record<string, SessionAxesRecord>;
|
|
335
|
+
/**
|
|
336
|
+
* One session's stored pair.
|
|
337
|
+
* @param sessionId - the session whose axes to read.
|
|
338
|
+
* @returns the pair, or `undefined` when this session has no record yet.
|
|
339
|
+
*/
|
|
340
|
+
get(sessionId: string): EffectiveScopes | undefined;
|
|
341
|
+
/**
|
|
342
|
+
* One session's stored pair, or the built-in defaults.
|
|
343
|
+
*
|
|
344
|
+
* The answer for a caller that has no session to build a seed from, and the only
|
|
345
|
+
* remaining reader of {@link DEFAULT_AXES}. Every live path reads {@link ensure}
|
|
346
|
+
* instead, whose un-recorded answer is the session's own seed.
|
|
347
|
+
* @param sessionId - the session whose axes to read.
|
|
348
|
+
* @returns the pair in force.
|
|
349
|
+
*/
|
|
350
|
+
getOr(sessionId: string): EffectiveScopes;
|
|
351
|
+
/**
|
|
352
|
+
* One session's pair, with an un-recorded session scheduled for repair.
|
|
353
|
+
*
|
|
354
|
+
* This is the read every LIVE touch point uses (the model-facing prompt, the
|
|
355
|
+
* read fence, and `/axis`). A session that has a record answers it; a session
|
|
356
|
+
* that has none answers {@link seedAxesFor} — the pair `pin` writes for a newly
|
|
357
|
+
* created session — and that ONE value is both this read's answer and what the
|
|
358
|
+
* scheduled repair stores. The answer and the write therefore cannot disagree,
|
|
359
|
+
* and a session created under a saved default runs under that default from its
|
|
360
|
+
* FIRST turn instead of waiting for the record to land. The record is what
|
|
361
|
+
* answers from the moment it lands.
|
|
362
|
+
*
|
|
363
|
+
* An un-recorded session is only REPAIRED when it has been used
|
|
364
|
+
* ({@link hasContent}). Until then the seed is recomputed on every read, so the
|
|
365
|
+
* settings row stays live: a session the workspace picker reopens before anyone
|
|
366
|
+
* has sent anything follows the settings page, which is the whole point of not
|
|
367
|
+
* having a record yet. The moment a turn lands, the next read repairs the
|
|
368
|
+
* session and it freezes — the same seed it answered with, computed from the row
|
|
369
|
+
* as it then stood.
|
|
370
|
+
*
|
|
371
|
+
* The seed is a thunk because only the caller can build it (it needs the
|
|
372
|
+
* session, and the composing plugin's config for a settings-less host). It is
|
|
373
|
+
* evaluated exactly once per un-recorded read — those reads are a prompt
|
|
374
|
+
* assembly, a tool dispatch and a pick, never a hot loop — and it must have no
|
|
375
|
+
* side effect, because its value IS this read's answer.
|
|
376
|
+
* @param sessionId - the session whose axes to read.
|
|
377
|
+
* @param seed - builds the pair a repair persists AND this read answers; never
|
|
378
|
+
* called when a record exists.
|
|
379
|
+
* @param frozen - whether the session has been used, so its pair must be
|
|
380
|
+
* written rather than recomputed. Defaults to `true`: a caller that cannot
|
|
381
|
+
* judge the session's content keeps the pre-existing freeze-on-first-touch
|
|
382
|
+
* behaviour, which is the safe direction — it never widens a live session's
|
|
383
|
+
* axes behind the person's back.
|
|
384
|
+
* @returns the pair in force right now.
|
|
385
|
+
*/
|
|
386
|
+
ensure(sessionId: string, seed: () => EffectiveScopes, frozen?: boolean): EffectiveScopes;
|
|
387
|
+
/**
|
|
388
|
+
* Publish the narrowing a live read just resolved, when the record does not
|
|
389
|
+
* already carry it.
|
|
390
|
+
*
|
|
391
|
+
* This is how the client half learns what the read axis removed from the write
|
|
392
|
+
* range without re-implementing the path algebra: it reads this member out of
|
|
393
|
+
* the same document its two dropdowns already read. The value comes from the
|
|
394
|
+
* very resolution the prompt and the fence used, so the surfaced narrowing and
|
|
395
|
+
* the enforced one are one computation rather than two that must agree.
|
|
396
|
+
*
|
|
397
|
+
* Called on every touch of a session that references groups, so an edit to a
|
|
398
|
+
* group's rules is republished by the next touch instead of waiting for the
|
|
399
|
+
* record to be rewritten for another reason. A session with no record is left
|
|
400
|
+
* alone: its axes are recomputed from the settings row on every read, and a
|
|
401
|
+
* store of its own would freeze that. Writes are fire-and-forget — a caller is
|
|
402
|
+
* a synchronous decision path and its answer must not depend on a document
|
|
403
|
+
* write.
|
|
404
|
+
* @param sessionId - the session whose narrowing to publish.
|
|
405
|
+
* @param narrowing - the value {@link resolveEffectiveAxes} returned for it.
|
|
406
|
+
*/
|
|
407
|
+
refreshNarrowing(sessionId: string, narrowing: WriteNarrowing): void;
|
|
408
|
+
/**
|
|
409
|
+
* Store one narrowing report for a session that already has a record, at most
|
|
410
|
+
* once per value.
|
|
411
|
+
*
|
|
412
|
+
* A failed write only forgets what was attempted: the next touch republishes
|
|
413
|
+
* the same report, and the dropdowns keep showing the last stored one instead
|
|
414
|
+
* of nothing. It never touches the axes, so a refresh that loses its race
|
|
415
|
+
* cannot revert a pick.
|
|
416
|
+
* @param sessionId - the session to update.
|
|
417
|
+
* @param narrowing - the report to store.
|
|
418
|
+
*/
|
|
419
|
+
private writeNarrowing;
|
|
420
|
+
/**
|
|
421
|
+
* Store one session's pair, whole-document, and report a lost write loudly.
|
|
422
|
+
*
|
|
423
|
+
* A single `replace` of the field: removal needs the whole map anyway, and a
|
|
424
|
+
* per-id path write cannot prune. Every attempt re-reads the revision, so an
|
|
425
|
+
* edit that landed between the read and the write is retried against the value
|
|
426
|
+
* it produced instead of being overwritten from a stale snapshot.
|
|
427
|
+
* @param sessionId - the session the pair belongs to.
|
|
428
|
+
* @param axes - the pair to store.
|
|
429
|
+
* @throws {SessionAxesConflictError} when every attempt lost the revision race.
|
|
430
|
+
* @throws When no settings service serves this namespace.
|
|
431
|
+
*/
|
|
432
|
+
set(sessionId: string, axes: EffectiveScopes, narrowing?: WriteNarrowing): Promise<void>;
|
|
433
|
+
/**
|
|
434
|
+
* Persist one un-recorded session's seed, at most once until it lands.
|
|
435
|
+
*
|
|
436
|
+
* Fire-and-forget on purpose: every caller is a synchronous decision path
|
|
437
|
+
* (a tool dispatch, a prompt assembly) that cannot await a whole-document
|
|
438
|
+
* write, and none of them may fail because the repair could not be stored —
|
|
439
|
+
* the answer that read already gave stands either way. A failed repair is
|
|
440
|
+
* logged and re-armed.
|
|
441
|
+
* @param sessionId - the session to repair.
|
|
442
|
+
* @param seed - builds the pair to persist; may throw on an unreadable document.
|
|
443
|
+
*/
|
|
444
|
+
private scheduleRepair;
|
|
445
|
+
/**
|
|
446
|
+
* Prune records whose session no longer exists.
|
|
447
|
+
*
|
|
448
|
+
* The deletion signal is the persistence layer itself: a session whose id is
|
|
449
|
+
* absent from `sessionPersistence.list()` has no stored log. `session/disposed`
|
|
450
|
+
* is NOT that signal — residency churn disposes a session whose file is still on
|
|
451
|
+
* disk, and dropping its axes there would silently reset a session that is about
|
|
452
|
+
* to be resumed.
|
|
453
|
+
* @returns the ids dropped, or `undefined` when the sweep cannot judge (no
|
|
454
|
+
* persistence service mounted, or a listing failed): an unjudgeable sweep
|
|
455
|
+
* removes nothing.
|
|
456
|
+
*/
|
|
457
|
+
sweep(): Promise<SessionAxesSweep | undefined>;
|
|
458
|
+
/**
|
|
459
|
+
* Replace the whole field, retrying while the revision fence refuses it.
|
|
460
|
+
* @param records - the complete next field.
|
|
461
|
+
* @throws {SessionAxesConflictError} when every attempt was refused.
|
|
462
|
+
*/
|
|
463
|
+
private replaceRecords;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* The store of one host context. Consumers hold no service of their own: the
|
|
467
|
+
* fence and the prompt must keep working in a composition that never mounted the
|
|
468
|
+
* settings service, where the store simply has nothing to read.
|
|
469
|
+
* @param ctx - host context.
|
|
470
|
+
* @returns the shared store.
|
|
471
|
+
*/
|
|
472
|
+
export declare function sessionAxesStore(ctx: Context): SessionAxesStore;
|
|
473
|
+
/**
|
|
474
|
+
* `Loader.unwrapExports` returns `exports.default` and DROPS every sibling named
|
|
475
|
+
* export (`vendor/loader/src/index.ts:201-208`), while the registry reads the
|
|
476
|
+
* schema off the plugin value itself. The Config therefore has to ride the
|
|
477
|
+
* default export, exactly as `./read-guard.ts` attaches its own.
|
|
478
|
+
*/
|
|
479
|
+
export interface SessionStoreEntry {
|
|
480
|
+
/** Does nothing; every read and write goes through {@link SessionAxesStore}. */
|
|
481
|
+
(): Promise<void>;
|
|
482
|
+
/** This entry's composition config, projected by the settings form. */
|
|
483
|
+
Config: z<{
|
|
484
|
+
axes: Dict<any>;
|
|
485
|
+
}>;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* The Loader entry's value: the no-op apply carrying the Config the registry
|
|
489
|
+
* reads off the plugin value itself. Spelled as an interface for the same reason
|
|
490
|
+
* {@link Config} is annotated — the inferred type of `Object.assign(apply, {
|
|
491
|
+
* Config })` names `Dict` from `@deepseek-ai/cosmokit`, which declaration emit
|
|
492
|
+
* cannot reach by name.
|
|
493
|
+
*/
|
|
494
|
+
declare const entry: SessionStoreEntry;
|
|
495
|
+
export default entry;
|
|
496
|
+
//# sourceMappingURL=session-store.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@t4r71/dsh-dual-axis",
|
|
3
|
+
"description": "The dual-axis access-mode bundle: the file sandbox's read and write axes, custom allow/deny paths included, as an installable profile layer",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "lib/index.js",
|
|
10
|
+
"types": "lib/types/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/types/index.d.ts",
|
|
14
|
+
"default": "./lib/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./fs": {
|
|
17
|
+
"types": "./lib/types/fs.d.ts",
|
|
18
|
+
"default": "./lib/fs.js"
|
|
19
|
+
},
|
|
20
|
+
"./read-guard": {
|
|
21
|
+
"types": "./lib/types/read-guard.d.ts",
|
|
22
|
+
"default": "./lib/read-guard.js"
|
|
23
|
+
},
|
|
24
|
+
"./session-store": {
|
|
25
|
+
"types": "./lib/types/session-store.d.ts",
|
|
26
|
+
"default": "./lib/session-store.js"
|
|
27
|
+
},
|
|
28
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"dsh": {
|
|
32
|
+
"bundle": {
|
|
33
|
+
"patch": "./cordis.patch.yml"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -b && tsdown",
|
|
38
|
+
"test": "node --import tsx/esm --test tests/axis.spec.ts tests/groups.spec.ts tests/settings.spec.ts tests/session-store.spec.ts tests/seed-parity.spec.ts tests/narrowing.spec.ts tests/content.spec.ts",
|
|
39
|
+
"typecheck": "tsc -b && tsc -p tsconfig.test.json --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"lib/index.js",
|
|
43
|
+
"lib/fs.js",
|
|
44
|
+
"lib/read-guard.js",
|
|
45
|
+
"lib/session-store.js",
|
|
46
|
+
"cordis.patch.yml",
|
|
47
|
+
"lib/types/**/*.d.ts"
|
|
48
|
+
],
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"author": "T4R71 <T4R71@outlook.com>",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/T4R71/dsh-dual-axis.git",
|
|
54
|
+
"directory": "packages/dual-axis"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/T4R71/dsh-dual-axis#readme",
|
|
57
|
+
"bugs": "https://github.com/T4R71/dsh-dual-axis/issues",
|
|
58
|
+
"keywords": [
|
|
59
|
+
"dsh",
|
|
60
|
+
"deepseek-harness",
|
|
61
|
+
"cordis",
|
|
62
|
+
"plugin",
|
|
63
|
+
"sandbox",
|
|
64
|
+
"access-control"
|
|
65
|
+
],
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
68
|
+
"@deepseek-ai/dsh-agent": "0.1.7-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-commands": "0.1.7-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-fs": "0.1.7-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-fs-sandbox": "0.1.7-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-sandbox": "0.1.7-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-session-projection": "0.1.7-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-settings": "0.1.7-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.7-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.2",
|
|
79
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
80
|
+
"zod": "^4.4.3"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
84
|
+
"@deepseek-ai/cosmokit": "^1.8.5",
|
|
85
|
+
"@deepseek-ai/dsh-agent": "0.1.7-rc.2",
|
|
86
|
+
"@deepseek-ai/dsh-commands": "0.1.7-rc.2",
|
|
87
|
+
"@deepseek-ai/dsh-fs": "0.1.7-rc.2",
|
|
88
|
+
"@deepseek-ai/dsh-fs-local": "0.1.7-rc.2",
|
|
89
|
+
"@deepseek-ai/dsh-fs-sandbox": "0.1.7-rc.2",
|
|
90
|
+
"@deepseek-ai/dsh-sandbox": "0.1.7-rc.2",
|
|
91
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.2",
|
|
92
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.2",
|
|
93
|
+
"@deepseek-ai/dsh-session-projection": "0.1.7-rc.2",
|
|
94
|
+
"@deepseek-ai/dsh-settings": "0.1.7-rc.2",
|
|
95
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.7-rc.2",
|
|
96
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.2",
|
|
97
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
98
|
+
"@types/node": "^24.0.0",
|
|
99
|
+
"tsdown": "^0.22.2",
|
|
100
|
+
"tsx": "^4.22.4",
|
|
101
|
+
"typescript": "^6.0.3"
|
|
102
|
+
}
|
|
103
|
+
}
|