dsh-advisor 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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +7 -0
  3. package/README.md +303 -0
  4. package/README.zh.md +166 -0
  5. package/cordis.patch.yml +6 -0
  6. package/lib/advisor-runtime.d.ts +242 -0
  7. package/lib/advisor-runtime.js +662 -0
  8. package/lib/advisor-runtime.js.map +1 -0
  9. package/lib/client/advisor-card.d.ts +90 -0
  10. package/lib/client/advisor-store.d.ts +310 -0
  11. package/lib/client/index.d.ts +39 -0
  12. package/lib/client/locales.d.ts +40 -0
  13. package/lib/client.d.ts +1 -0
  14. package/lib/client.js +840 -0
  15. package/lib/commands.d.ts +136 -0
  16. package/lib/commands.js +185 -0
  17. package/lib/commands.js.map +1 -0
  18. package/lib/config.d.ts +74 -0
  19. package/lib/config.js +93 -0
  20. package/lib/config.js.map +1 -0
  21. package/lib/delivery.d.ts +129 -0
  22. package/lib/delivery.js +169 -0
  23. package/lib/delivery.js.map +1 -0
  24. package/lib/emission-guard.d.ts +99 -0
  25. package/lib/emission-guard.js +155 -0
  26. package/lib/emission-guard.js.map +1 -0
  27. package/lib/gateway.d.ts +116 -0
  28. package/lib/gateway.js +214 -0
  29. package/lib/gateway.js.map +1 -0
  30. package/lib/index.d.ts +48 -0
  31. package/lib/index.js +485 -0
  32. package/lib/index.js.map +1 -0
  33. package/lib/kinds.d.ts +38 -0
  34. package/lib/kinds.js +24 -0
  35. package/lib/kinds.js.map +1 -0
  36. package/lib/prompts.d.ts +22 -0
  37. package/lib/prompts.js +38 -0
  38. package/lib/prompts.js.map +1 -0
  39. package/lib/settings.d.ts +96 -0
  40. package/lib/settings.js +141 -0
  41. package/lib/settings.js.map +1 -0
  42. package/lib/transcript.d.ts +257 -0
  43. package/lib/transcript.js +530 -0
  44. package/lib/transcript.js.map +1 -0
  45. package/package.json +90 -0
  46. package/scripts/build-client.mjs +268 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * T1 (plan dsh-advisor-settings-n2) — host-side `advisor` settings namespace
3
+ * + live source wiring.
4
+ *
5
+ * The plugin-row config (the `entry` passed to the plugin's `apply`) is the
6
+ * composition BASE of the `advisor` settings namespace: when a dsh settings
7
+ * service is mounted, its user layer is layered on top (schema defaults →
8
+ * base → user layer) and the runtime reads the live resolved value through
9
+ * the bridge's `source` thunk — the same source-thunk pattern as dsh's
10
+ * `agent-default-model`. Without a settings service the conditional
11
+ * `ctx.inject(['settings'], ...)` child never activates and the source is
12
+ * exactly the entry: behavior identical to today.
13
+ *
14
+ * The hard gate is untouched: the source returns the RAW composed config and
15
+ * every consumer passes it through `resolveAdvisorConfig` — the SSOT for the
16
+ * enabled-without-pair disabled-with-reason resolution (no model call).
17
+ *
18
+ * The namespace does NOT join the apiproxy configuration-client boundary on
19
+ * current upstream dsh builds: the host's `exposedNamespaces()` unions only
20
+ * model-provider namespaces plus its own product namespaces (locale /
21
+ * permission / ui-conversation / ui-theme / ui-onboarding / agent-presets) —
22
+ * there is no registration-level opt-in in upstream dsh (verified against the
23
+ * pristine 20da39e snapshot; `SettingsRegisterOptions` has no
24
+ * `exposeToWebClients` key). The advisor namespace is therefore always absent
25
+ * from `settings.describe` on the web configuration boundary — but the web
26
+ * card reaches the config through the TypertRemoteService channel instead (plan
27
+ * dsh-advisor-settings-gateway-n5: `AdvisorConfigGateway` claims
28
+ * `/api/advisor/get` + `/api/advisor/set`, and the client calls
29
+ * `connection.rpc.call('/api', …)`; the in-process `ctx.settings.update`
30
+ * behind `set` carries no exposed-namespace check — the allowlist gate exists
31
+ * only in the apiproxy wire layer). The unexposed-namespace notice is now
32
+ * only the KD-G5 fallback (gateway unreachable). No host patch is applied or
33
+ * required for the plugin to function — the runtime reads the entry config
34
+ * exactly as before.
35
+ *
36
+ * @module dsh-advisor/settings
37
+ */
38
+ import type { Context } from '@deepseek-ai/cordis';
39
+ import type { AdvisorConfig } from './config.js';
40
+ /** The `advisor` settings namespace (registered when a settings service exists). */
41
+ export declare const ADVISOR_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
42
+ /**
43
+ * The live configuration source for the runtime.
44
+ *
45
+ * `source()` returns the RAW composed config (schema defaults → plugin-row
46
+ * base → settings user layer); consumers pass it through
47
+ * `resolveAdvisorConfig` — the hard gate stays the SSOT. `onChange` registers
48
+ * a callback that re-applies derived state whenever the composed value changes
49
+ * (attach, committed change, or detach back to the entry). The contract is
50
+ * `(cb) => void` per the plan: the listener set is owned by the consumer's
51
+ * plugin closure for its lifetime, and the detach path is handled by the
52
+ * inject child's disposer (the `installSettingsSection` contract), so no
53
+ * per-listener disposer is returned (qc1 S-3 — a discarded disposer would
54
+ * invite misuse).
55
+ */
56
+ export interface AdvisorSettingsBridge {
57
+ source(): AdvisorConfig;
58
+ onChange(callback: () => void): void;
59
+ }
60
+ /**
61
+ * Install the `advisor` settings namespace and wire the live source.
62
+ *
63
+ * Mirrors the dsh `agent-default-model` pattern exactly (the dsh-settings
64
+ * `installSettingsSection` contract): the registration rides a conditional
65
+ * `ctx.inject(['settings'], ...)` child, so with no settings service the
66
+ * source stays the entry config. `setSource` swaps the authoritative thunk
67
+ * (the settings scope's resolved value while attached); `onChange` fires at
68
+ * attach, on committed changes, and at detach. The registration carries NO
69
+ * `exposeToWebClients` option — upstream dsh (pristine 20da39e) has no such
70
+ * registration-level opt-in (its `exposedNamespaces()` unions model-provider
71
+ * plus product namespaces only), so the advisor namespace stays off the
72
+ * apiproxy web configuration boundary on every current dsh build. Web clients
73
+ * reach the config through the TypertRemoteService channel instead (plan
74
+ * dsh-advisor-settings-gateway-n5 — `/api/advisor/get` + `/api/advisor/set`);
75
+ * the in-process settings service is the write target behind the gateway's
76
+ * `set`. (A previous iteration believed the opt-in existed upstream and
77
+ * declared it here; that conclusion was a circular verification against a
78
+ * locally-modified staging tree — the option does not exist in upstream types
79
+ * and has been removed.)
80
+ *
81
+ * qc1 W-5 (multi-fiber dedupe): the host composes several dsh-advisor fibers,
82
+ * and this runs on EVERY instance — but `Settings.register` fails loud on a
83
+ * duplicate namespace (`settings namespace "advisor" is already registered`).
84
+ * The register call runs inside the conditional inject child, so the duplicate
85
+ * error surfaces ASYNCHRONOUSLY there (an outer try/catch around a library
86
+ * `installSettingsSection` call cannot see it) — this child body wraps the
87
+ * register instead. A deduped instance logs (debug) and keeps the
88
+ * entry-source fallback: its `source` thunk is only ever swapped by a
89
+ * SUCCESSFUL registration's setSource hook, so the ALREADY-REGISTERED
90
+ * instance owns the live namespace. The reviewer's settings wiring (the
91
+ * `bridge.onChange` live re-apply in `index.ts`) is the concern — it only
92
+ * re-applies when the reviewer's own bridge attached to the live scope; in
93
+ * practice the reviewer is the first apply, whose inject child registers
94
+ * first.
95
+ */
96
+ export declare function installAdvisorSettings(ctx: Context, entry: AdvisorConfig): AdvisorSettingsBridge;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * T1 (plan dsh-advisor-settings-n2) — host-side `advisor` settings namespace
3
+ * + live source wiring.
4
+ *
5
+ * The plugin-row config (the `entry` passed to the plugin's `apply`) is the
6
+ * composition BASE of the `advisor` settings namespace: when a dsh settings
7
+ * service is mounted, its user layer is layered on top (schema defaults →
8
+ * base → user layer) and the runtime reads the live resolved value through
9
+ * the bridge's `source` thunk — the same source-thunk pattern as dsh's
10
+ * `agent-default-model`. Without a settings service the conditional
11
+ * `ctx.inject(['settings'], ...)` child never activates and the source is
12
+ * exactly the entry: behavior identical to today.
13
+ *
14
+ * The hard gate is untouched: the source returns the RAW composed config and
15
+ * every consumer passes it through `resolveAdvisorConfig` — the SSOT for the
16
+ * enabled-without-pair disabled-with-reason resolution (no model call).
17
+ *
18
+ * The namespace does NOT join the apiproxy configuration-client boundary on
19
+ * current upstream dsh builds: the host's `exposedNamespaces()` unions only
20
+ * model-provider namespaces plus its own product namespaces (locale /
21
+ * permission / ui-conversation / ui-theme / ui-onboarding / agent-presets) —
22
+ * there is no registration-level opt-in in upstream dsh (verified against the
23
+ * pristine 20da39e snapshot; `SettingsRegisterOptions` has no
24
+ * `exposeToWebClients` key). The advisor namespace is therefore always absent
25
+ * from `settings.describe` on the web configuration boundary — but the web
26
+ * card reaches the config through the TypertRemoteService channel instead (plan
27
+ * dsh-advisor-settings-gateway-n5: `AdvisorConfigGateway` claims
28
+ * `/api/advisor/get` + `/api/advisor/set`, and the client calls
29
+ * `connection.rpc.call('/api', …)`; the in-process `ctx.settings.update`
30
+ * behind `set` carries no exposed-namespace check — the allowlist gate exists
31
+ * only in the apiproxy wire layer). The unexposed-namespace notice is now
32
+ * only the KD-G5 fallback (gateway unreachable). No host patch is applied or
33
+ * required for the plugin to function — the runtime reads the entry config
34
+ * exactly as before.
35
+ *
36
+ * @module dsh-advisor/settings
37
+ */
38
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings';
39
+ import { Config } from './config.js';
40
+ /** The `advisor` settings namespace (registered when a settings service exists). */
41
+ export const ADVISOR_SETTINGS_NAMESPACE = settingsNamespace('advisor');
42
+ /**
43
+ * Mirror of @deepseek-ai/dsh-settings' `isUnloading` guard (its
44
+ * `installSettingsSection` skips source/listener work while the plugin fiber
45
+ * is unloading or disposed). The library compares `ctx.fiber.state` against
46
+ * `FiberState.DISPOSED` / `FiberState.UNLOADING`; the const enum is erased at
47
+ * runtime, so the vendored numeric values (4 / 5) are mirrored here.
48
+ */
49
+ function isUnloading(ctx) {
50
+ const state = ctx.fiber?.state;
51
+ return state === 4 || state === 5;
52
+ }
53
+ /**
54
+ * Install the `advisor` settings namespace and wire the live source.
55
+ *
56
+ * Mirrors the dsh `agent-default-model` pattern exactly (the dsh-settings
57
+ * `installSettingsSection` contract): the registration rides a conditional
58
+ * `ctx.inject(['settings'], ...)` child, so with no settings service the
59
+ * source stays the entry config. `setSource` swaps the authoritative thunk
60
+ * (the settings scope's resolved value while attached); `onChange` fires at
61
+ * attach, on committed changes, and at detach. The registration carries NO
62
+ * `exposeToWebClients` option — upstream dsh (pristine 20da39e) has no such
63
+ * registration-level opt-in (its `exposedNamespaces()` unions model-provider
64
+ * plus product namespaces only), so the advisor namespace stays off the
65
+ * apiproxy web configuration boundary on every current dsh build. Web clients
66
+ * reach the config through the TypertRemoteService channel instead (plan
67
+ * dsh-advisor-settings-gateway-n5 — `/api/advisor/get` + `/api/advisor/set`);
68
+ * the in-process settings service is the write target behind the gateway's
69
+ * `set`. (A previous iteration believed the opt-in existed upstream and
70
+ * declared it here; that conclusion was a circular verification against a
71
+ * locally-modified staging tree — the option does not exist in upstream types
72
+ * and has been removed.)
73
+ *
74
+ * qc1 W-5 (multi-fiber dedupe): the host composes several dsh-advisor fibers,
75
+ * and this runs on EVERY instance — but `Settings.register` fails loud on a
76
+ * duplicate namespace (`settings namespace "advisor" is already registered`).
77
+ * The register call runs inside the conditional inject child, so the duplicate
78
+ * error surfaces ASYNCHRONOUSLY there (an outer try/catch around a library
79
+ * `installSettingsSection` call cannot see it) — this child body wraps the
80
+ * register instead. A deduped instance logs (debug) and keeps the
81
+ * entry-source fallback: its `source` thunk is only ever swapped by a
82
+ * SUCCESSFUL registration's setSource hook, so the ALREADY-REGISTERED
83
+ * instance owns the live namespace. The reviewer's settings wiring (the
84
+ * `bridge.onChange` live re-apply in `index.ts`) is the concern — it only
85
+ * re-applies when the reviewer's own bridge attached to the live scope; in
86
+ * practice the reviewer is the first apply, whose inject child registers
87
+ * first.
88
+ */
89
+ export function installAdvisorSettings(ctx, entry) {
90
+ const listeners = new Set();
91
+ let source = () => entry;
92
+ const notify = () => {
93
+ for (const listener of [...listeners])
94
+ listener();
95
+ };
96
+ ctx.inject(['settings'], (sctx) => {
97
+ let scope;
98
+ try {
99
+ scope = sctx.settings.register(ADVISOR_SETTINGS_NAMESPACE, Config, {
100
+ base: entry,
101
+ // No `exposeToWebClients` here: the option does not exist in upstream
102
+ // dsh (verified against pristine 20da39e — `SettingsRegisterOptions`
103
+ // has no such key, and the host's `exposedNamespaces()` unions only
104
+ // model-provider plus product namespaces). The namespace stays off
105
+ // the apiproxy web configuration boundary; web clients reach the
106
+ // config through the TypertRemoteService channel (advisor/get +
107
+ // advisor/set), and the runtime reads the entry exactly as it would
108
+ // without a settings service.
109
+ });
110
+ }
111
+ catch (error) {
112
+ if (!(error instanceof Error) || !error.message.includes('already registered'))
113
+ throw error;
114
+ ctx.logger('advisor').debug('settings namespace already registered — entry-source fallback (multi-fiber dedupe)');
115
+ return;
116
+ }
117
+ // Mirrors installSettingsSection: the source thunk reads the scope's live
118
+ // resolved value while attached, and the detach disposer falls back to the
119
+ // entry when the settings service goes away (skipped during unload).
120
+ source = () => scope.get();
121
+ sctx.effect(() => () => {
122
+ if (isUnloading(ctx))
123
+ return;
124
+ source = () => entry;
125
+ notify();
126
+ });
127
+ notify();
128
+ scope.watch(() => {
129
+ if (isUnloading(ctx))
130
+ return;
131
+ notify();
132
+ });
133
+ });
134
+ return {
135
+ source: () => source(),
136
+ onChange: (callback) => {
137
+ listeners.add(callback);
138
+ },
139
+ };
140
+ }
141
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAE7D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGpC,oFAAoF;AACpF,MAAM,CAAC,MAAM,0BAA0B,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAA;AAEtE;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,GAAY;IAC/B,MAAM,KAAK,GAAI,GAAiD,CAAC,KAAK,EAAE,KAAK,CAAA;IAC7E,OAAO,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAA;AACnC,CAAC;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAY,EAAE,KAAoB;IACvE,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;IACvC,IAAI,MAAM,GAAG,GAAkB,EAAE,CAAC,KAAK,CAAA;IACvC,MAAM,MAAM,GAAG,GAAS,EAAE;QACxB,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC;YAAE,QAAQ,EAAE,CAAA;IACnD,CAAC,CAAA;IACD,GAAG,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE;QAChC,IAAI,KAA+C,CAAA;QACnD,IAAI,CAAC;YACH,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,0BAA0B,EAAE,MAAM,EAAE;gBACjE,IAAI,EAAE,KAAK;gBACX,sEAAsE;gBACtE,qEAAqE;gBACrE,oEAAoE;gBACpE,mEAAmE;gBACnE,iEAAiE;gBACjE,gEAAgE;gBAChE,oEAAoE;gBACpE,8BAA8B;aAC/B,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC;gBAAE,MAAM,KAAK,CAAA;YAC3F,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,oFAAoF,CAAC,CAAA;YACjH,OAAM;QACR,CAAC;QACD,0EAA0E;QAC1E,2EAA2E;QAC3E,qEAAqE;QACrE,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAA;QAC1B,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE;YACrB,IAAI,WAAW,CAAC,GAAG,CAAC;gBAAE,OAAM;YAC5B,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK,CAAA;YACpB,MAAM,EAAE,CAAA;QACV,CAAC,CAAC,CAAA;QACF,MAAM,EAAE,CAAA;QACR,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE;YACf,IAAI,WAAW,CAAC,GAAG,CAAC;gBAAE,OAAM;YAC5B,MAAM,EAAE,CAAA;QACV,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IACF,OAAO;QACL,MAAM,EAAE,GAAkB,EAAE,CAAC,MAAM,EAAE;QACrC,QAAQ,EAAE,CAAC,QAAoB,EAAQ,EAAE;YACvC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QACzB,CAAC;KACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Session observer + bounded delta renderer (spec §2 S2, §4 mapping table,
3
+ * §6 self-review exclusion, §8.3 KD-3, §8.5 KD-5).
4
+ *
5
+ * `DeltaRenderer` is the pure transcript core: a cursor over the session event
6
+ * log plus a fingerprint of the delivered prefix. `update(events)` extracts
7
+ * the newly appended messages, excludes advisor-source messages (self-review
8
+ * guard), and renders a role-annotated markdown delta bounded by the
9
+ * `maxDeltaMessages` window. A prefix rewrite — a `compact/*` event, a
10
+ * `user/message` with `surfaceOp.op === 'replace'`, or a fingerprint mismatch
11
+ * (defensive fallback) — resets the cursor and replays the full post-rewrite
12
+ * surface on the next update.
13
+ *
14
+ * `SessionTranscriptObserver` is the per-session wiring unit: it owns one
15
+ * `DeltaRenderer` per session id and dispatches `session/event` tuples to
16
+ * them. It has TWO trigger modes (KD-N4-5):
17
+ *
18
+ * 1. **turn/end** (standard turn-driven sessions): renders only on a stepped
19
+ * `turn/end` whose `reason.kind` is reviewable (`completed` | `max-tokens`
20
+ * | `error`; spec §4 — skip `aborted`/`blocked`/`interrupted`, i.e. do not
21
+ * critique user-cut-short turns). Byte-identical behavior — the original
22
+ * single-trigger path.
23
+ * 2. **agent reply complete** (harness/agentic sessions, which never emit
24
+ * `turn/end`): fires on human-input arrival (`user/message` with
25
+ * `source.kind === 'user'`, or `agent/inbox/spliced` whose `inserted`
26
+ * carries a message with `source.kind === 'user'` — inbox-spliced input
27
+ * may commit as that event first and never re-emit as `user/message`).
28
+ * Non-user inbox splices (the advisor's own inject/steer deliveries,
29
+ * workspace/tool/empty splices) never trigger (C-1 self-trigger fix).
30
+ * Before rendering,
31
+ * a read-only predicate checks for an unreviewed non-advisor
32
+ * `assistant/message` since the renderer cursor; on a miss the cursor is
33
+ * untouched (the first user input of a session neither triggers nor
34
+ * advances). Append-type triggers pass `events` minus the trigger itself
35
+ * (the arriving input opens the next round); rewrite-type triggers
36
+ * (compact summary replace) pass the full log to trigger the KD-5 replay.
37
+ * Both modes share the same renderer/cursor, so the cursor advance dedupes
38
+ * (one delta per completed round), and both call `onSteppedTurnEnd` (the
39
+ * immuneTurns countdown — T6) before rendering.
40
+ *
41
+ * **Mode latch**: once a session has produced ANY `turn/end` event
42
+ * (reviewable or not), the new gate sleeps for it — the session is a
43
+ * standard turn-driven one and keeps verbatim behavior, including spec §4
44
+ * skip-aborted (a cut-short first turn's unreviewed increment must not be
45
+ * supplementarily reviewed by the new gate). Harness/agentic sessions never
46
+ * emit `turn/end`, so their latch never arms.
47
+ *
48
+ * It also exposes the T6 delivery hooks: `onSteppedTurnEnd` (one completed
49
+ * stepped primary turn — the immuneTurns countdown) and `onRewrite` (a
50
+ * compact/replace event — the KD-5 latch reset). `index.ts` binds the cordis
51
+ * `session/event` / `session/disposed` / `agent/disposed` listeners into an
52
+ * instance of this class.
53
+ *
54
+ * @module dsh-advisor/transcript
55
+ */
56
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
57
+ /** One incremental transcript delta handed to the advisor runtime (T4). */
58
+ export interface Delta {
59
+ /** Role-annotated markdown of the new (or replayed) transcript messages. */
60
+ readonly markdown: string;
61
+ /**
62
+ * Whether more transcript content is expected to follow the same logical
63
+ * update. dsh closes a turn with `turn/end` (all its steps are done), so a
64
+ * delta rendered at a stepped `turn/end` is always complete — `false` for
65
+ * the MVP (see the task-3 report for the rationale).
66
+ */
67
+ readonly willContinue: boolean;
68
+ }
69
+ /** Options for one {@link DeltaRenderer}. */
70
+ export interface DeltaRendererOptions {
71
+ /**
72
+ * Bounded message window (spec §8.3 KD-3): keep the most recent N messages
73
+ * and prepend the truncation marker when a rendered delta would exceed N;
74
+ * `0` = unbounded. Default 60.
75
+ */
76
+ readonly maxDeltaMessages: number;
77
+ }
78
+ /** Default delta window (KD-3). */
79
+ export declare const DEFAULT_MAX_DELTA_MESSAGES = 60;
80
+ /** Marker line prepended when a rendered delta omits earlier messages (KD-3). */
81
+ export declare const TRUNCATION_MARKER = "\u2026 <earlier messages omitted>";
82
+ /**
83
+ * Cursor + fingerprint bookkeeping over one session's event log.
84
+ *
85
+ * The renderer consumes the session event log (the full `session.events`
86
+ * snapshot on each call — the cordis wiring passes the live log). It keeps:
87
+ * - `cursor` — how many log events are consumed (seqs are contiguous from 0,
88
+ * so this equals the next unprocessed seq);
89
+ * - `surface` / `messages` — the current ordered surface of derived
90
+ * non-advisor messages (seqs + messages), maintained incrementally for
91
+ * appends and rebuilt from a full fold after a reset;
92
+ * - `deliveredFingerprint` — fingerprint of the delivered prefix, detecting a
93
+ * hidden rewrite;
94
+ * - `droppedPrefix` — whether messages before the bounded window were dropped
95
+ * (drives the truncation marker on replay renders).
96
+ */
97
+ export declare class DeltaRenderer {
98
+ private maxDeltaMessages;
99
+ private cursor;
100
+ private readonly surface;
101
+ private readonly messages;
102
+ private deliveredFingerprint;
103
+ private droppedPrefix;
104
+ constructor(options?: Partial<DeltaRendererOptions>);
105
+ /**
106
+ * Update the bounded message window (live config — settings onChange, plan
107
+ * dsh-advisor-settings-n2 T1). Existing fold state is kept; the new bound
108
+ * applies from the next render on.
109
+ */
110
+ setMaxDeltaMessages(value: number): void;
111
+ /**
112
+ * Process a session event log snapshot and return the next delta, or
113
+ * `undefined` when no new (renderable) content was appended.
114
+ *
115
+ * `skipLast` treats the final log event as excluded — the arriving trigger
116
+ * of an append-type review (qc3 F1): equivalent to the caller slicing
117
+ * `events.slice(0, events.length - 1)` WITHOUT the O(full-log) shallow copy
118
+ * the reply-complete gate used to pay on every review. The excluded event
119
+ * must be the last log entry (the wiring guarantees it).
120
+ *
121
+ * - Detects a reset: a rewrite event (`compact/*`, replace surface op) in
122
+ * the unconsumed portion, a shorter log than the cursor, or a delivered
123
+ * prefix whose fingerprint changed. On reset the cursor rewinds to 0 and
124
+ * the full post-rewrite surface is replayed (bounded by KD-3).
125
+ * - Otherwise appends the new events, extracts their messages (advisor
126
+ * excluded), and renders only those (incremental delta).
127
+ */
128
+ update(events: readonly SessionEvent[], skipLast?: boolean): Delta | undefined;
129
+ /**
130
+ * Full reset: rewind the cursor to 0 and drop all fold state. The next
131
+ * `update` replays the whole current surface (bounded). Also the KD-5
132
+ * reset surface for emission-guard / immuneTurns latches (T5/T6 hook into
133
+ * the same session lifecycle).
134
+ */
135
+ reset(): void;
136
+ /**
137
+ * Seed the cursor to `length` (KD-5 seed-on-enable): skip existing history
138
+ * — the next update renders only events at/after `length` (no full-history
139
+ * replay, matching omp).
140
+ */
141
+ seedTo(length: number): void;
142
+ /**
143
+ * Read-only predicate for the agentic reply-complete gate (KD-N4-5): does an
144
+ * unreviewed non-advisor `assistant/message` increment exist since the
145
+ * cursor? Scans the unconsumed log tail with the same derivation the
146
+ * renderer uses (an empty-content `assistant/message` derives no message, so
147
+ * it does not count) and excludes advisor-source messages (self-review
148
+ * guard, spec §6). NEVER mutates state — a blind `update()` here would
149
+ * pre-advance the cursor and lose the user prompt from the first
150
+ * standard-session turn/end delta, so the caller only calls `update()` when
151
+ * this returns true.
152
+ */
153
+ hasUnreviewedAssistant(events: readonly SessionEvent[]): boolean;
154
+ /** Rebuild the fold from a full log (post-reset replay, bounded to `length`). */
155
+ private rebuild;
156
+ /** Incrementally fold new events in `[start, end)` (guaranteed append-only) and return added messages. */
157
+ private append;
158
+ /** Render the newly appended messages (incremental delta). */
159
+ private renderTail;
160
+ /** Render the current surface in full (post-reset replay, bounded). */
161
+ private renderSurface;
162
+ private render;
163
+ }
164
+ /** Callback contract for {@link SessionTranscriptObserver}. */
165
+ export interface SessionObserverOptions {
166
+ /** Bounded delta window (KD-3); forwarded to every per-session renderer. */
167
+ readonly maxDeltaMessages: number;
168
+ /** Invoked once per stepped reviewable turn/end with the rendered delta. */
169
+ readonly onDelta: (sessionId: string, delta: Delta) => void;
170
+ /**
171
+ * Invoked once per stepped reviewable turn/end (the same gate as `onDelta`),
172
+ * before the delta is rendered — the delivery module (T6) counts completed
173
+ * primary turns here to decrement its immuneTurns cooldown (spec §6).
174
+ */
175
+ readonly onSteppedTurnEnd?: (sessionId: string) => void;
176
+ /**
177
+ * Invoked when a rewrite event is observed (`compact/*` or a non-append
178
+ * surface op) — the delivery module (T6) clears its immuneTurns latch here
179
+ * (KD-5 reset triggers). Fires before the turn gate: a rewrite can arrive
180
+ * outside a turn/end.
181
+ */
182
+ readonly onRewrite?: (sessionId: string) => void;
183
+ }
184
+ /** True when a `turn/end` event carries a reviewable end reason. */
185
+ export declare function isReviewableTurnEnd(event: SessionEvent): boolean;
186
+ /**
187
+ * True when an event is a human-input arrival — the trigger of the agentic
188
+ * reply-complete gate (KD-N4-5).
189
+ *
190
+ * - `user/message` with `source.kind === 'user'` — the primary signal: a
191
+ * direct human prompt (the queued message claimed for a step).
192
+ * - `agent/inbox/spliced` — the fallback: inbox-spliced input commits as
193
+ * this log-only event first and may never re-emit as `user/message`
194
+ * (the merged `SessionEventMap` entry comes from the dsh-agent peer, per
195
+ * the `compact/*` precedent). Payload-discriminated (C-1 fix): the event
196
+ * only triggers when `inserted` is non-empty and carries at least one
197
+ * message whose `source.kind === 'user'`. Every other inbox mutation is
198
+ * excluded — the advisor's OWN inject/steer deliveries (source.kind
199
+ * `advisor`), workspace-context sync (`workspace-instructions`),
200
+ * tool-result splicing (`tool`), and claim/clear splices (empty
201
+ * `inserted`) must not self-trigger the review gate.
202
+ *
203
+ * Synthetic/injected user-role messages (tool results, advisor notes,
204
+ * workspace context) carry other `source.kind` values and never trigger.
205
+ */
206
+ export declare function isHumanInputEvent(event: SessionEvent): boolean;
207
+ /**
208
+ * One renderer per session id, driven by `session/event` tuples. Cordis-free,
209
+ * so the wiring logic is unit-testable; `index.ts` binds the cordis listeners
210
+ * into an instance and forwards `session.events` (the live log).
211
+ */
212
+ export declare class SessionTranscriptObserver {
213
+ private readonly options;
214
+ private readonly renderers;
215
+ /** seedTo lengths issued before a session's renderer existed (KD-5 enable). */
216
+ private readonly pendingSeeds;
217
+ /**
218
+ * Mode latch (KD-N4-5): sessions that have produced ANY `turn/end` event.
219
+ * Once a session emits `turn/end` it is a standard turn-driven session —
220
+ * the new agentic reply-complete gate sleeps for it (verbatim behavior,
221
+ * including spec §4 skip-aborted: a cut-short first turn's unreviewed
222
+ * increment must not be supplementarily reviewed). Agentic/harness
223
+ * sessions never emit `turn/end`, so they never latch and the new gate
224
+ * stays active. Per-session; cleared on dispose.
225
+ */
226
+ private readonly turnEndSessions;
227
+ /** Bounded delta window (KD-3); forwarded to every per-session renderer. */
228
+ private maxDeltaMessages;
229
+ constructor(options: SessionObserverOptions);
230
+ /**
231
+ * Update the bounded delta window (live config — settings onChange, plan
232
+ * dsh-advisor-settings-n2 T1): the observer default AND every live
233
+ * per-session renderer, so existing sessions pick up the new bound without
234
+ * losing their fold state.
235
+ */
236
+ setMaxDeltaMessages(value: number): void;
237
+ /**
238
+ * Feed one session event (mirroring the cordis `session/event` listener:
239
+ * `(session, event)` — `events` is the session's live log, `event` the
240
+ * appended event). Renders (and emits via `onDelta`) on either trigger:
241
+ * a stepped `turn/end` with a reviewable reason (standard sessions), or a
242
+ * human-input arrival with an unreviewed assistant increment (agentic
243
+ * sessions — the reply-complete gate, KD-N4-5). The mode latch keeps the
244
+ * second gate dormant for any session that emits `turn/end`.
245
+ */
246
+ handleEvent(sessionId: string, events: readonly SessionEvent[], event: SessionEvent): void;
247
+ /** Lazy per-session renderer creation, shared by both trigger modes. */
248
+ private rendererFor;
249
+ /** Drop a session's renderer (wiring: `session/disposed` / `agent/disposed`). */
250
+ disposeSession(sessionId: string): void;
251
+ /**
252
+ * KD-5 seed-on-enable: skip existing history for a session's renderer.
253
+ * Issued before the renderer exists (e.g. `/advisor on` before the session
254
+ * produced a stepped turn), the seed is remembered and applied on creation.
255
+ */
256
+ seedTo(sessionId: string, length: number): void;
257
+ }