@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,59 @@
1
+ /**
2
+ * The dual-axis bundle's command registration: the one write path the two
3
+ * composer dropdowns reach the host through.
4
+ *
5
+ * The name is NOT permission. @deepseek-ai/dsh-permission-presets registers
6
+ * /permission on the global command layer, and the registry's NamedEntries
7
+ * throws on a duplicate global name, so reusing it would fail this plugin's
8
+ * load. The preset picker keeps /permission; this command owns the axes.
9
+ *
10
+ * Registration rides ctx.inject(['commands'], …) rather than the plugin's
11
+ * static inject: a profile without a command registry composes this bundle
12
+ * for its fence and prompt, and an unconditional ctx.commands access would fail
13
+ * that load.
14
+ *
15
+ * The write is a WHOLE-DOCUMENT settings write and therefore asynchronous, so
16
+ * the handler is too. A lost revision race settles as an ordinary command
17
+ * error carrying the conflict's own sentence: the dispatching surface renders
18
+ * it beside the picker, and nothing is reported as applied that was not stored.
19
+ *
20
+ * @module @t4r71/dsh-dual-axis/axis-command
21
+ */
22
+ import type { Context } from '@deepseek-ai/cordis';
23
+ import type { Session } from '@deepseek-ai/dsh-session';
24
+ import type { EffectiveScopes } from './axis.ts';
25
+ /** The registered command name, without its leading slash. */
26
+ export declare const AXIS_COMMAND_NAME = "axis";
27
+ /** What the registration needs from its owner. */
28
+ export interface AxisCommandOptions {
29
+ /**
30
+ * The session's standing axis pair, read from the session axis store — the one
31
+ * source every other consumer reads. Supplies the axis a one-axis entry does
32
+ * not name.
33
+ * @param session - the session the invocation belongs to.
34
+ * @returns the pair in force, with both defaults filled in.
35
+ */
36
+ readonly current: (session: Session) => EffectiveScopes;
37
+ /**
38
+ * The rule-group ids the settings page currently defines. Read at the moment
39
+ * of the invocation so a group created since startup is selectable, and used
40
+ * only to REFUSE an unknown id: an existing id is recorded verbatim and
41
+ * expanded at the moment of use, never here.
42
+ * @returns the defined ids.
43
+ */
44
+ readonly knownGroups: () => ReadonlySet<string>;
45
+ /**
46
+ * Store one session's new pair. Supplied by the composing plugin, which owns
47
+ * the store; the command layer never reaches the settings document itself.
48
+ * @param session - the session to write.
49
+ * @param axes - the complete new pair.
50
+ */
51
+ readonly write: (session: Session, axes: EffectiveScopes) => Promise<void>;
52
+ }
53
+ /**
54
+ * Register the axis command for every composed command adapter.
55
+ * @param ctx - host context; the registration is owned by this context's fiber.
56
+ * @param options - the standing-pair reader, the group-id reader, and the write path.
57
+ */
58
+ export declare function registerAxisCommand(ctx: Context, options: AxisCommandOptions): void;
59
+ //# sourceMappingURL=axis-command.d.ts.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The dual-axis command's argument grammar: one client-composed entry string
3
+ * into one access axis.
4
+ *
5
+ * The grammar is owned by the browser half's pickers, which emit exactly two
6
+ * forms — <axis>:<deny|workspace|all> for the three closed values, and
7
+ * <axis>:custom:base=<deny|workspace|all>[,groups=<id>|<id>][,allow=<absolute path>][,deny=<absolute path>]
8
+ * from the path editor, where a repeated key carries several entries.
9
+ *
10
+ * A `groups` entry records the IDS ONLY, checked against the ids the settings
11
+ * page currently defines and then stored verbatim. The group's rules are never
12
+ * expanded into the entry: expansion happens at the moment of use, so editing a
13
+ * group reaches every session that references it without a restart.
14
+ *
15
+ * The caller supplies the session's standing pair so a one-axis entry returns
16
+ * the complete new pair; the axis the entry does not name keeps its value.
17
+ *
18
+ * @module @t4r71/dsh-dual-axis/axis-entry
19
+ */
20
+ import type { EffectiveScopes } from './axis.ts';
21
+ /** The two axis names an entry string may start with. */
22
+ export declare const AXIS_ENTRY_NAMES: readonly string[];
23
+ /** A parsed entry, or the sentence explaining why it is not one. */
24
+ export type AxisEntryParse = {
25
+ readonly ok: true;
26
+ readonly axes: EffectiveScopes;
27
+ } | {
28
+ readonly ok: false;
29
+ readonly problem: string;
30
+ };
31
+ /** The separator between rule-group ids in one `groups` entry. */
32
+ export declare const GROUP_ID_SEPARATOR = "|";
33
+ /**
34
+ * Read one client-composed axis entry into the complete axis pair it selects.
35
+ * @param rawInput - the command invocation's verbatim input; the commands
36
+ * registry passes the text after the command name including the separating
37
+ * space, so this trims first.
38
+ * @param current - the session's standing pair; the axis the entry does not
39
+ * name keeps its value from here.
40
+ * @param knownGroups - the rule-group ids the settings page currently defines;
41
+ * a `groups` entry naming anything else is refused.
42
+ * @returns the new pair, or the sentence explaining why the entry was refused.
43
+ */
44
+ export declare function parseAxisEntry(rawInput: string, current: EffectiveScopes, knownGroups?: ReadonlySet<string>): AxisEntryParse;
45
+ //# sourceMappingURL=axis-entry.d.ts.map
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The read/write ACCESS AXES of the file sandbox: the per-axis vocabulary a
3
+ * session selects and the lossless mapping onto the legacy single `mode`.
4
+ *
5
+ * This is the 0.1.7 port of the algebra upstream deleted. 0.1.6 shipped it as
6
+ * `@deepseek-ai/dsh-sandbox/access` (152 lines) and
7
+ * `@deepseek-ai/dsh-sandbox/scope` (152 lines); 0.1.7 has neither symbol nor
8
+ * file, so the package carries its own copy and keeps the 0.1.6 semantics
9
+ * verbatim: four kinds, `custom` = base + allow - deny with deny winning.
10
+ *
11
+ * One axis answers "which absolute paths may this execution touch". `deny`
12
+ * permits nothing, `workspace` permits the calling session's workspace root
13
+ * plus the platform temp areas, `all` is the whole host filesystem, and
14
+ * `custom` names a BASE kind plus absolute path ADDITIONS and REMOVALS —
15
+ * path lists, not patterns.
16
+ *
17
+ * `mode` REMAINS the write axis's persistence spelling. The session-format
18
+ * whitelist pins its literal values, so this module keeps `mode` derivable
19
+ * from the write scope in both directions instead of replacing it:
20
+ * {@link scopeOfMode} and {@link modeOfScope} are the two halves of that
21
+ * bijection, and `custom` maps onto the mode implied by its own base.
22
+ *
23
+ * @module @t4r71/dsh-dual-axis/axis
24
+ */
25
+ import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox';
26
+ /**
27
+ * One axis's kind. `deny` touches nothing; `workspace` is the session's
28
+ * workspace plus the platform temp areas; `all` is the host filesystem;
29
+ * `custom` carries additions and removals over a `base`.
30
+ */
31
+ export type AxisScopeKind = 'deny' | 'workspace' | 'all' | 'custom';
32
+ /** The comparison base a `custom` scope adds to and removes from. */
33
+ export type AxisBase = 'deny' | 'workspace' | 'all';
34
+ /**
35
+ * One axis's complete value. The three closed kinds carry no payload;
36
+ * `custom` carries the absolute paths its selection adds (`allow`) and the
37
+ * absolute paths it removes even from the base (`deny`). Removal wins over
38
+ * addition — see {@link scopeContains}.
39
+ */
40
+ export type AxisScope = {
41
+ kind: Exclude<AxisScopeKind, 'custom'>;
42
+ } | {
43
+ kind: 'custom';
44
+ /** The scope the additions and removals are applied over. */
45
+ base: AxisBase;
46
+ /**
47
+ * Ids of the rule groups this axis references. The DEFINITION of an id is
48
+ * read from the settings page at the moment of use, never stored here: a
49
+ * session that copied a group's contents would keep enforcing the copy after
50
+ * the group changed, and copies of one group drift apart.
51
+ */
52
+ groups?: readonly string[] | undefined;
53
+ /** Absolute paths added to the base. */
54
+ allow: readonly string[];
55
+ /** Absolute paths removed from (base ∪ allow). */
56
+ deny: readonly string[];
57
+ };
58
+ /** Every {@link AxisScopeKind}, for option advertisement and untrusted-value validation. */
59
+ export declare const AXIS_KINDS: readonly AxisScopeKind[];
60
+ /** Every {@link AxisBase}, for option advertisement and untrusted-value validation. */
61
+ export declare const AXIS_BASES: readonly AxisBase[];
62
+ /** The default READ axis: every mode permitted reading before the axes existed, so the whole host. */
63
+ export declare const DEFAULT_READ_SCOPE: AxisScope;
64
+ /** The default WRITE axis: the session workspace plus the platform temp areas. */
65
+ export declare const DEFAULT_WRITE_SCOPE: AxisScope;
66
+ /**
67
+ * Whether an untrusted runtime value names one of the three `SandboxMode` members.
68
+ *
69
+ * The TYPE says `SandboxMode`; the session LOG says whatever a past writer put
70
+ * there. A replayed or foreign log can carry a mode this build never wrote, and
71
+ * a fold that trusts it would take the whole projected axis cell down with it.
72
+ * This guard is the boundary that turns 'the log claims a mode' into 'the log
73
+ * claims a mode this build understands': callers read a false answer as 'names
74
+ * no mode' and keep the axis value they already had.
75
+ * @param value - the untrusted value from a session-log payload.
76
+ * @returns true when the value is a `SandboxMode`.
77
+ */
78
+ export declare function isSandboxMode(value: unknown): value is SandboxMode;
79
+ /**
80
+ * The write scope a legacy `mode` means.
81
+ * @param mode - the sandbox mode.
82
+ * @returns the equivalent write scope.
83
+ */
84
+ export declare function scopeOfMode(mode: SandboxMode): AxisScope;
85
+ /**
86
+ * The mode an axis BASE implies — the shared half of {@link modeOfScope}. The
87
+ * three-value mode set is closed, so every base spells exactly one mode.
88
+ * @param base - the axis base to spell.
89
+ * @returns the mode that base has always been persisted as.
90
+ */
91
+ export declare function modeOfAxisBase(base: AxisBase): SandboxMode;
92
+ /**
93
+ * The legacy `mode` a write scope is spelled as. `custom` resolves through its
94
+ * own base: the mode names the containment the fence must not exceed, while the
95
+ * scope's additions and removals ride beside it.
96
+ * @param scope - the write axis value.
97
+ * @returns the mode that spells this scope's base.
98
+ */
99
+ export declare function modeOfScope(scope: AxisScope): SandboxMode;
100
+ /** The axis pair actually in force, with both defaults filled in. */
101
+ export interface EffectiveScopes {
102
+ /** The read axis in force. */
103
+ read: AxisScope;
104
+ /** The write axis in force. */
105
+ write: AxisScope;
106
+ }
107
+ /**
108
+ * Read the axis pair a resolved policy carries, falling back to the defaults.
109
+ * The policy's `mode` is authoritative for the write axis's BASE: a policy
110
+ * that carries no write axes writes exactly what its mode always meant, so
111
+ * every pre-existing policy keeps its exact behavior.
112
+ *
113
+ * 0.1.7's `SandboxExecutionPolicy` has no `readScope` / `writeScope`
114
+ * members (only `mode`, `workspaceRoot`, `sessionId?`), so the axes arrive
115
+ * through the extra argument this package's own fence passes. The
116
+ * `mode`-derived fallback keeps the function total for the upstream type.
117
+ * @param policy - the resolved policy (supplies `mode`).
118
+ * @param axes - the session's axis pair, when the caller holds one.
119
+ * @returns both axes in force.
120
+ */
121
+ export declare function effectiveScopes(policy: SandboxExecutionPolicy, axes?: Partial<EffectiveScopes>): EffectiveScopes;
122
+ /**
123
+ * Whether a policy's `mode` still spells its write scope's base — the
124
+ * invariant every construction site must preserve. A custom write scope whose
125
+ * base disagrees with `mode` would enforce containment the session never chose.
126
+ * @param policy - the policy to check.
127
+ * @param axes - the session's axis pair, when the caller holds one.
128
+ * @returns true when the two agree.
129
+ */
130
+ export declare function isModeConsistent(policy: SandboxExecutionPolicy, axes?: Partial<EffectiveScopes>): boolean;
131
+ //# sourceMappingURL=axis.d.ts.map
@@ -0,0 +1,173 @@
1
+ /**
2
+ * The dual-axis settings surface: the 0.1.7 spelling of the section 0.1.6
3
+ * registered through `ctx.settings.installSection`.
4
+ *
5
+ * 0.1.7 removed both `installSection` and `settings.register(ns, schema)`:
6
+ * `SettingsForms.describe()` projects exactly the Config schemas of LOADED
7
+ * Loader entries (`packages/settings/settings/src/index.ts:302-340`) and
8
+ * `write()` refuses a namespace with no matching entry
9
+ * (`:382-384`, `No configurable plugin entry`). The namespace IS the entry
10
+ * id, so this package's own row id in `cordis.patch.yml` — `dual-axis` — is
11
+ * the settings namespace. 0.1.6's separate `sandbox-axis` namespace name is
12
+ * therefore retired.
13
+ *
14
+ * 0.1.7 also added the volatile gate: a field that is not beneath a
15
+ * `.volatile()` node is neither projected into the form
16
+ * (`packages/settings/settings/src/schema.ts:37-47`) nor writable
17
+ * (`:74-78`), and an entry with no volatile field at all is refused outright
18
+ * (`index.ts:385-386`). Both axes are marked volatile here.
19
+ *
20
+ * The schema is deliberately `z.any()` per axis, exactly as in 0.1.6:
21
+ * schemastery's union types strip the `custom` branch's `base/allow/deny`
22
+ * payload, which would silently degrade a configured custom axis into an axis
23
+ * carrying no paths. Shape validation is {@link normalizeScope}'s job.
24
+ *
25
+ * @module @t4r71/dsh-dual-axis/config
26
+ */
27
+ import type { Context, Volatile } from '@deepseek-ai/cordis';
28
+ import z from '@deepseek-ai/schemastery';
29
+ import type { AxisScope } from './axis.ts';
30
+ import type { RuleGroup } from './groups.ts';
31
+ /**
32
+ * This package's row id in `cordis.patch.yml`. It doubles as the 0.1.7
33
+ * settings namespace (the Loader entry id) and, prefixed with the package
34
+ * name, as the `plugins.row.config` slot entry key the client half uses.
35
+ */
36
+ export declare const DUAL_AXIS_ROW_ID = "dual-axis";
37
+ /**
38
+ * The 0.1.6 settings namespace name, kept only so a migration can recognize
39
+ * the retired spelling. Nothing registers under it in 0.1.7.
40
+ */
41
+ export declare const RETIRED_SETTINGS_NAMESPACE = "sandbox-axis";
42
+ /**
43
+ * This row's composition config: the defaults a NEWLY CREATED session takes,
44
+ * plus the group library its `defaultGroups` are drawn from.
45
+ *
46
+ * Every field here is a SEED. A session reads this row once, when it is created,
47
+ * and owns its copy from then on; the decision paths (the fence and the prompt)
48
+ * read the row only to resolve, by id, the definitions of the groups a session
49
+ * already references ({@link groupLibraryValue}). Nothing here is consulted when
50
+ * an existing session decides whether a path is allowed.
51
+ *
52
+ * Fields left empty fall back to the built-in defaults (read `all`, write
53
+ * `workspace`, no groups), which are 0.1.6's `DEFAULT_READ_SCOPE` /
54
+ * `DEFAULT_WRITE_SCOPE`.
55
+ */
56
+ export interface Config {
57
+ /** The read axis a new session starts from (default: `{ kind: 'all' }`). */
58
+ read?: Volatile<AxisScope>;
59
+ /** The write axis a new session starts from (default: `{ kind: 'workspace' }`). */
60
+ write?: Volatile<AxisScope>;
61
+ /** The group library: named rule fragments sessions reference by id (default: empty). */
62
+ groups?: Volatile<RuleGroup[]>;
63
+ /** Which of {@link groups} a new session starts out referencing (default: empty). */
64
+ defaultGroups?: Volatile<string[]>;
65
+ }
66
+ /**
67
+ * The composition config's schema. Both axes are `.volatile()` and
68
+ * `z.any()` — see the module docstring for why each is required.
69
+ *
70
+ * Not annotated `z<Config>`: `.volatile()` widens the field's inferred type
71
+ * to schemastery's `Volatile<>` accessor, which the plain `Config` interface
72
+ * does not describe. The repo's own volatile Configs
73
+ * (`packages/core/agent-default-model/src/index.ts:51-55`) are inferred the
74
+ * same way, and {@link axesOf} re-establishes the declared shape.
75
+ */
76
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
77
+ read: z<any, any, "volatile">;
78
+ write: z<any, any, "volatile">;
79
+ groups: z<NoInfer<any[]>, NoInfer<any[]>, "volatile-defined">;
80
+ defaultGroups: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
81
+ }>>, Schemastery.ObjectT<NoInfer<{
82
+ read: z<any, any, "volatile">;
83
+ write: z<any, any, "volatile">;
84
+ groups: z<NoInfer<any[]>, NoInfer<any[]>, "volatile-defined">;
85
+ defaultGroups: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
86
+ }>>, "plain">;
87
+ /**
88
+ * Collect one `custom` axis's path list: every entry must be a non-empty
89
+ * absolute path.
90
+ * @param label - the axis name used in the error message.
91
+ * @param entry - the list name used in the error message (`allow` or `deny`).
92
+ * @param value - the untrusted list value.
93
+ * @returns the validated path list.
94
+ * @throws When the value is not a string array, or holds a non-absolute path.
95
+ */
96
+ export declare function pathList(label: string, entry: string, value: unknown): readonly string[];
97
+ /**
98
+ * Coerce one axis value from a config document (which a human may have edited)
99
+ * into a legal {@link AxisScope}, throwing rather than guessing.
100
+ *
101
+ * An unreadable axis must not become some other, wider or narrower grant:
102
+ * throwing fails the row's mount or the settings write on the spot instead of
103
+ * proceeding under an invented boundary. This is 0.1.6's `normalizeScope`,
104
+ * unchanged.
105
+ * @param label - the axis name used in error messages (`read` or `write`).
106
+ * @param value - the untrusted axis value.
107
+ * @returns the validated axis value.
108
+ * @throws When the value is not one of the four kinds, or a custom entry is not absolute.
109
+ */
110
+ export declare function normalizeScope(label: string, value: unknown): AxisScope;
111
+ /**
112
+ * Whether two axes describe the same boundary. Compared member by member
113
+ * rather than by reference: every re-parse builds fresh axis objects, and a
114
+ * custom axis's two path lists carry order as part of their value.
115
+ * @param left - one axis.
116
+ * @param right - another axis.
117
+ * @returns whether both describe the same axis.
118
+ */
119
+ export declare function sameScope(left: AxisScope, right: AxisScope): boolean;
120
+ /**
121
+ * Read both axes out of one settings/config value.
122
+ *
123
+ * The parameter is UNTRUSTED on purpose: the same function serves the
124
+ * composition layer's parsed Config, the settings service's projected
125
+ * descriptor (an `unknown` on the wire between two packages), and a
126
+ * hand-edited document, and none of those three is guaranteed to carry the
127
+ * declared shape. Both axes therefore pass through {@link normalizeScope},
128
+ * which is where an illegal value becomes a throw instead of an invented
129
+ * boundary; a missing side falls back to the built-in default axis.
130
+ * @param section - the section's current resolved value, whatever carries it.
131
+ * @returns both axes.
132
+ * @throws When either axis's shape is illegal.
133
+ */
134
+ export declare function axesOf(section: unknown): {
135
+ read: AxisScope;
136
+ write: AxisScope;
137
+ };
138
+ /**
139
+ * This row's current value on the settings page, or `undefined` when no
140
+ * settings service is mounted.
141
+ *
142
+ * This is the ONE read path to that row. It goes through `describe()` rather
143
+ * than the config the plugin instance captured at mount because the loader does
144
+ * not commit a later save back to that instance
145
+ * (`vendor/loader/src/config/entry.ts:162-195`); `describe()` re-projects
146
+ * `entry.fiber.config` and unwraps the volatile accessors on every call
147
+ * (`packages/settings/settings/src/index.ts:319-323` +
148
+ * `packages/settings/settings/src/schema.ts:11`), which is the same path the
149
+ * settings page renders from. A save therefore applies without a restart.
150
+ * @param ctx - host context; the service is looked up, never injected.
151
+ * @returns the row's current value, or `undefined` when unavailable.
152
+ */
153
+ export declare function declaredSection(ctx: Context): unknown;
154
+ /**
155
+ * The group library a decision path may read, and the ONLY part of the settings
156
+ * row one may read for a decision.
157
+ *
158
+ * The row's `read`, `write` and `defaultGroups` fields are seeds for
159
+ * NEWLY CREATED sessions and carry no authority over an existing one; a
160
+ * decision path that read them would let the settings page silently re-scope a
161
+ * running session, which is the drift this package exists to prevent.
162
+ * @param section - the row's current value, as {@link declaredSection} returns it.
163
+ * @returns the untrusted `groups` value, for {@link resolveEffectiveAxes}.
164
+ */
165
+ export declare function groupLibraryValue(section: unknown): unknown;
166
+ /**
167
+ * The group ids a newly created session starts out referencing.
168
+ * @param section - the row's current value, as {@link declaredSection} returns it.
169
+ * @returns the ids, deduplicated; empty when the row declares none.
170
+ * @throws When a declared id is not a non-empty string.
171
+ */
172
+ export declare function defaultGroupIds(section: unknown): readonly string[];
173
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 目标路径包含判定:把上游 `@deepseek-ai/dsh-fs-sandbox/containment` 的
3
+ * `isPathUnder` 原样内联到本包。
4
+ *
5
+ * 为什么必须内联:fs-sandbox@0.1.7-rc.2 的**发布包** exports 只开出
6
+ * `.`、`./src/*` 与 `./package.json` 三条,**没有 `./containment`**;
7
+ * 而源码工作区里该文件存在,靠路径解析能直接命中。于是同一个 import 在
8
+ * 工作区里编译通过、装成 tgz 后运行期必然抛
9
+ * `Package subpath './containment' is not defined by "exports"`,
10
+ * 使 dual-axis 这个 entry 永远无法激活(fiberPhase 恒为 null)。
11
+ * 自有副本消除这个 install-shape 依赖。
12
+ * @module @t4r71/dsh-dual-axis/containment
13
+ */
14
+ /**
15
+ * Determine whether a canonical target is a writable root or lies beneath it.
16
+ * The lexical fast path handles normal canonical spellings. When spellings
17
+ * differ, walk the target's existing ancestors and compare filesystem identity
18
+ * with the root; this recognizes Windows long-name/8.3 aliases and casing
19
+ * without weakening containment to a textual approximation.
20
+ * @param path - canonical target key, which may end in a missing suffix.
21
+ * @param root - canonical writable root.
22
+ * @param caseSensitive - whether lexical comparison preserves case; defaults
23
+ * to the host filesystem convention used by supported platforms.
24
+ * @returns whether the target is the root or a descendant of it.
25
+ */
26
+ export declare function isPathUnder(path: string, root: string, caseSensitive?: boolean): Promise<boolean>;
27
+ //# sourceMappingURL=containment.d.ts.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Whether a session has been USED yet — the one predicate that decides when its
3
+ * axis pair freezes.
4
+ *
5
+ * A session the workspace picker reopens in the same workspace is the same
6
+ * session id with the same stored record, so the axes it was seeded with at
7
+ * creation are what its two dropdowns show for the rest of its life. That is
8
+ * correct for a session someone has actually worked in; for one nobody has ever
9
+ * exchanged a turn with it is wrong, because the settings row is a live default
10
+ * and the person changing it expects the next conversation to follow.
11
+ *
12
+ * So the record is written when the session is used, not when it is created:
13
+ *
14
+ * - nothing appended beyond the loop's runtime-context snapshot → no record; the
15
+ * seed recomputed from the CURRENT settings row answers every read, so the
16
+ * dropdown follows the settings page in real time;
17
+ * - one real user turn, or an assistant reply → the record is written and the
18
+ * pair freezes;
19
+ * - `/axis` → {@link SessionAxesStore.set} writes unconditionally, so a manual
20
+ * pick always freezes, on an empty session too.
21
+ *
22
+ * ## What counts as "content"
23
+ *
24
+ * Everything except a message whose source is the loop's own runtime-context
25
+ * snapshot. That snapshot is appended by the agent loop on EVERY turn
26
+ * (`packages/core/agent-loop/src/runtime-context.ts`), including the very first
27
+ * one, and it is present before any human input exists — the four events a
28
+ * freshly created session carries are the header, the preset, the mode, and the
29
+ * approval policy. Counting it would make every session look used the moment
30
+ * anything rendered its prompt, which is exactly the state this predicate has to
31
+ * tell apart.
32
+ *
33
+ * The snapshot is identified by its `source.kind`, the discriminant the loop
34
+ * writes and reads back (`isOwned` in that module requires `kind === 'plugin'`;
35
+ * the system-prompt renderer stamps `kind: 'runtime-context'`). Matching on the
36
+ * kind is cheaper and more robust than matching the rendered text, which is
37
+ * localized prose that changes with every contribution.
38
+ *
39
+ * Every other message counts, the skill catalogue and the compaction markers
40
+ * included: each of them means the session has entered a turn, and a session
41
+ * that has entered a turn is one somebody is using.
42
+ *
43
+ * @module @t4r71/dsh-dual-axis/content
44
+ */
45
+ import type { Session } from '@deepseek-ai/dsh-session';
46
+ /**
47
+ * The event types that can carry content, so a listener can skip the rest without
48
+ * building the session's whole event list.
49
+ *
50
+ * A `system/message` is in here because it only exists once a turn has started,
51
+ * and a started turn is a used session. The prompt is assembled and that event
52
+ * appended BEFORE the human message reaches the log (measured: `system/message`
53
+ * at seq 7, the user turn at seq 8), so a rule that waited for the user message
54
+ * alone would let a tool-less turn finish without freezing anything.
55
+ */
56
+ export declare const CONTENT_EVENT_TYPES: ReadonlySet<string>;
57
+ /**
58
+ * Whether a session has content beyond the loop's own runtime-context snapshots.
59
+ *
60
+ * Cheap by construction: it walks the session's existing event array, which is
61
+ * already materialized and cached, without copying, parsing, or allocating per
62
+ * event. It is called on every `ensure` for a session that has no record, and
63
+ * that set is exactly the sessions nobody has used — so the scan is short.
64
+ * @param session - the session to judge.
65
+ * @returns whether this session has been used.
66
+ */
67
+ export declare function hasContent(session: Session): boolean;
68
+ //# sourceMappingURL=content.d.ts.map