@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.
@@ -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
+ }