dsh-loop-engine 0.1.5-rc5 → 0.1.7-rc1

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/lib/index.js CHANGED
@@ -395,21 +395,30 @@ function renderAssistantBlocks(blocks) {
395
395
  }
396
396
  return sections.join("\n\n");
397
397
  }
398
- function renderToolResult(message) {
399
- const block = message.content[0];
400
- const body = block.content.map((child) => {
401
- switch (child.type) {
398
+ function renderTextBlocks(blocks) {
399
+ return blocks.map((block) => {
400
+ switch (block.type) {
402
401
  case "text":
403
- return child.text;
402
+ return block.text;
404
403
  case "image":
405
404
  return OMITTED_IMAGE_TEXT;
406
405
  default:
407
406
  return "";
408
407
  }
409
408
  }).filter((section) => section !== "").join("\n\n");
410
- const tag = block.isError === true ? "tool-result-error" : "tool-result";
409
+ }
410
+ function renderToolResult(blocks, isError) {
411
+ const body = renderTextBlocks(blocks);
412
+ const tag = isError === true ? "tool-result-error" : "tool-result";
411
413
  return frame(tag, body || "(no content)");
412
414
  }
415
+ function renderModernToolResult(message) {
416
+ return renderToolResult(message.content, message.isError);
417
+ }
418
+ function renderLegacyToolResult(message) {
419
+ const block = message.content[0];
420
+ return renderToolResult(block?.content ?? [], block?.isError);
421
+ }
413
422
  var ENGINE_SLASH_LINE = /^\/[^\s/]+(?:[ \t]+.*)?$/;
414
423
  function engineSlashPrompt(messages) {
415
424
  const last = messages.at(-1);
@@ -433,24 +442,18 @@ function serializeHistory(messages) {
433
442
  break;
434
443
  }
435
444
  case "user": {
436
- const user = message;
437
- if (user.source.kind === "tool") {
438
- sections.push(renderToolResult(user));
445
+ if (message.source.kind === "tool") {
446
+ sections.push(renderLegacyToolResult(message));
439
447
  } else {
440
- const body = user.content.map((block) => {
441
- switch (block.type) {
442
- case "text":
443
- return block.text;
444
- case "image":
445
- return OMITTED_IMAGE_TEXT;
446
- default:
447
- return "";
448
- }
449
- }).filter((section) => section !== "").join("\n\n");
448
+ const body = renderTextBlocks(message.content);
450
449
  sections.push(frame("user", body || "(no content)"));
451
450
  }
452
451
  break;
453
452
  }
453
+ case "tool": {
454
+ sections.push(renderModernToolResult(message));
455
+ break;
456
+ }
454
457
  default:
455
458
  break;
456
459
  }
@@ -624,6 +627,29 @@ var DriverInbox = class {
624
627
  }
625
628
  };
626
629
 
630
+ // src/driver-core/system-head.ts
631
+ import { createSystemMessage } from "@deepseek-ai/dsh-llm";
632
+
633
+ // src/compat.ts
634
+ import * as dshSettings from "@deepseek-ai/dsh-settings";
635
+ var LEGACY_HARNESS = "SettingsProvider" in dshSettings;
636
+
637
+ // src/driver-core/system-head.ts
638
+ var SURFACE_TYPES = /* @__PURE__ */ new Set([
639
+ "system/message",
640
+ "user/message",
641
+ "developer/message",
642
+ "assistant/message",
643
+ "tool/result"
644
+ ]);
645
+ function appendSystemHeadIfMissing(session, turn, step) {
646
+ if (LEGACY_HARNESS) return;
647
+ for (const event of session.snapshotEvents()) {
648
+ if (SURFACE_TYPES.has(event.type) && event.surfaceOp !== void 0) return;
649
+ }
650
+ session.append("system/message", { turn, step, message: createSystemMessage("") }, { surfaceOp: "append" });
651
+ }
652
+
627
653
  // src/provider-route.ts
628
654
  import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
629
655
  function hostedRouteLabelOf(_engine) {
@@ -1508,6 +1534,7 @@ var ClaudeCodeAgent = class {
1508
1534
  this.session.append("step/start", { turn, step });
1509
1535
  phase.step = step;
1510
1536
  try {
1537
+ appendSystemHeadIfMissing(this.session, turn, step);
1511
1538
  for (const message of decision.messages) {
1512
1539
  this.session.append("user/message", message, { surfaceOp: "append" });
1513
1540
  }
@@ -2037,16 +2064,21 @@ var HostedEngineRuntime = class {
2037
2064
  return {
2038
2065
  agent,
2039
2066
  signal: abort.signal,
2040
- publish: (source) => {
2067
+ publish: async (source) => {
2041
2068
  assertLive();
2042
2069
  const joining = lifetime.entered;
2043
2070
  if (!joining) lifetime.bind(agent.ctx.sessions.enter(session));
2044
2071
  detachAgent = loopCtx.agents.enter(agent, parentAgent);
2045
2072
  if (!joining) agent.ctx.sessions.announce(session);
2046
2073
  assertLive();
2047
- loopCtx.agents.announce(agent);
2048
- assertLive();
2049
- emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
2074
+ if (LEGACY_HARNESS) {
2075
+ ;
2076
+ loopCtx.agents.announce(agent);
2077
+ assertLive();
2078
+ emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
2079
+ } else {
2080
+ await loopCtx.agents.announce(agent, source, abort.signal);
2081
+ }
2050
2082
  assertLive();
2051
2083
  return { agent, dispose, retire, lifetime };
2052
2084
  },
@@ -2913,6 +2945,7 @@ var CodexAgent = class {
2913
2945
  this.session.append("step/end", { turn: phase.turn, step: phase.step });
2914
2946
  phase.step += 1;
2915
2947
  this.session.append("step/start", { turn: phase.turn, step: phase.step });
2948
+ appendSystemHeadIfMissing(this.session, phase.turn, phase.step);
2916
2949
  this.stepSettledTools = 0;
2917
2950
  }
2918
2951
  /** Lazily created app-server client, reused across steps and released on scope teardown. */
@@ -3964,6 +3997,7 @@ var PiAgent = class {
3964
3997
  this.session.append("step/end", { turn: phase.turn, step: phase.step });
3965
3998
  phase.step += 1;
3966
3999
  this.session.append("step/start", { turn: phase.turn, step: phase.step });
4000
+ appendSystemHeadIfMissing(this.session, phase.turn, phase.step);
3967
4001
  this.stepSettledTools = 0;
3968
4002
  }
3969
4003
  /**
@@ -5299,6 +5333,7 @@ var KimiAgent = class {
5299
5333
  this.session.append("step/start", { turn, step });
5300
5334
  phase.step = step;
5301
5335
  try {
5336
+ appendSystemHeadIfMissing(this.session, turn, step);
5302
5337
  for (const message of decision.messages) {
5303
5338
  this.session.append("user/message", message, { surfaceOp: "append" });
5304
5339
  }
@@ -5693,7 +5728,7 @@ var KimiLoop = class extends HostedEngineRuntime {
5693
5728
  import z5 from "@deepseek-ai/schemastery";
5694
5729
 
5695
5730
  // src/namespace.ts
5696
- var LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
5731
+ var LEGACY_LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL = "agent-loop-engine";
5697
5732
 
5698
5733
  // src/settings.ts
5699
5734
  var LOOP_ENGINE_SETTINGS_SCHEMA = z5.object({
@@ -5701,8 +5736,16 @@ var LOOP_ENGINE_SETTINGS_SCHEMA = z5.object({
5701
5736
  showInComposer: z5.boolean().default(true)
5702
5737
  });
5703
5738
  function loopEngineSettingsNamespace() {
5704
- return LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL;
5739
+ return LEGACY_LOOP_ENGINE_SETTINGS_NAMESPACE_LITERAL;
5740
+ }
5741
+ function withVolatile(schema) {
5742
+ const volatile = schema.volatile;
5743
+ return volatile === void 0 ? schema : volatile.call(schema);
5705
5744
  }
5745
+ var LOOP_ENGINE_ENGINE_SCHEMA = withVolatile(
5746
+ z5.union(LOOP_ENGINE_IDS.map((id) => z5.const(id))).default("in-process")
5747
+ );
5748
+ var LOOP_ENGINE_SHOW_IN_COMPOSER_SCHEMA = withVolatile(z5.boolean().default(true));
5706
5749
 
5707
5750
  // src/patch-manager.ts
5708
5751
  var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block --";
@@ -5841,8 +5884,13 @@ async function writeIfDifferent(path, text) {
5841
5884
  await rename(tmp, path);
5842
5885
  return true;
5843
5886
  }
5887
+ async function readComposition(source, id) {
5888
+ if (source.readDocument !== void 0) return (await source.readDocument(id)).content;
5889
+ if (source.read !== void 0) return source.read(id);
5890
+ throw new Error("loop-engine: the preset roster exposes neither readDocument() nor read()");
5891
+ }
5844
5892
  async function ensureEnginePresets(dshHome, source) {
5845
- const composition = await source.read(SOURCE_PRESET_ID);
5893
+ const composition = await readComposition(source, SOURCE_PRESET_ID);
5846
5894
  const stripped = `${MANAGED_HEADER}
5847
5895
  ${stripPresetRows(composition)}`;
5848
5896
  let changed = false;
@@ -5856,7 +5904,7 @@ ${stripPresetRows(composition)}`;
5856
5904
  }
5857
5905
 
5858
5906
  // src/router-loop.ts
5859
- import AgentLoop from "@deepseek-ai/dsh-agent-loop";
5907
+ import AgentLoop, { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from "@deepseek-ai/dsh-agent-loop";
5860
5908
 
5861
5909
  // src/model-selection-reset.ts
5862
5910
  var AGENT_DEFAULT_MODEL_NS = "agent-default-model";
@@ -6757,7 +6805,10 @@ var RouterLoop = class extends AgentLoop {
6757
6805
  * @param warn - diagnostic sink for a skipped engine command.
6758
6806
  */
6759
6807
  constructor(ctx, build, records, warn) {
6760
- super(ctx, { agents: [] });
6808
+ super(ctx, LEGACY_HARNESS ? { agents: [] } : {
6809
+ agents: [],
6810
+ maxParallelToolCalls: { get: () => DEFAULT_MAX_PARALLEL_TOOL_CALLS }
6811
+ });
6761
6812
  this.build = build;
6762
6813
  this.records = records;
6763
6814
  this.warn = warn;
@@ -7341,7 +7392,13 @@ var Config5 = z6.object({
7341
7392
  approvalPolicy: z6.union(CODEX_APPROVAL_POLICIES.map((policy) => z6.const(policy))),
7342
7393
  piProvider: z6.string(),
7343
7394
  piThinking: z6.string(),
7344
- kimiBin: z6.string()
7395
+ kimiBin: z6.string(),
7396
+ // The live fields exist only on the 0.1.7 line; the 0.1.5 line carries the
7397
+ // selection in a settings section instead. The empty arm never runs when the
7398
+ // coverage job is on 0.1.7; the 0.1.5 dep set is exercised by
7399
+ // vitest.config.compat015.ts, which takes it.
7400
+ /* v8 ignore next -- legacy arm, exercised by vitest.config.compat015.ts */
7401
+ ...LEGACY_HARNESS ? {} : { engine: LOOP_ENGINE_ENGINE_SCHEMA, showInComposer: LOOP_ENGINE_SHOW_IN_COMPOSER_SCHEMA }
7345
7402
  });
7346
7403
  function resolvePatchPath(config) {
7347
7404
  if (config.patchPath !== void 0 && config.patchPath !== "") return config.patchPath;
@@ -7541,9 +7598,7 @@ function apply(ctx, config) {
7541
7598
  });
7542
7599
  };
7543
7600
  let savedPresetDefault;
7544
- let steeredEngine;
7545
7601
  const steerPresetDefault = (engine) => {
7546
- steeredEngine = engine;
7547
7602
  if (engine === "in-process") {
7548
7603
  const saved = savedPresetDefault;
7549
7604
  savedPresetDefault = void 0;
@@ -7562,7 +7617,6 @@ function apply(ctx, config) {
7562
7617
  mutatePresetDefault({ op: "set", path: ["default"], value: target });
7563
7618
  });
7564
7619
  };
7565
- const seedEngine = legacyEngine ?? "in-process";
7566
7620
  const pluginWarn = (message) => {
7567
7621
  ctx.logger.warn(message);
7568
7622
  };
@@ -7623,25 +7677,47 @@ function apply(ctx, config) {
7623
7677
  CLEAR_ROUTER_RETRY();
7624
7678
  releaseRoutes();
7625
7679
  }, "loop-engine: cleanup");
7626
- let source;
7627
- ctx.inject(["settings"], (settingsCtx) => {
7628
- settingsCtx.settings.installSection(
7629
- ctx,
7630
- loopEngineSettingsNamespace(),
7631
- LOOP_ENGINE_SETTINGS_SCHEMA,
7632
- { engine: seedEngine, showInComposer: true },
7633
- {
7634
- setSource: (current) => {
7635
- source = current;
7636
- },
7637
- onChange: () => {
7638
- const next = source().engine;
7639
- if (next === steeredEngine) return;
7640
- steerPresetDefault(next);
7680
+ if (LEGACY_HARNESS) {
7681
+ let source;
7682
+ let steered;
7683
+ ctx.inject(["settings"], (settingsCtx) => {
7684
+ const settings = settingsCtx.settings;
7685
+ settings.installSection(
7686
+ ctx,
7687
+ loopEngineSettingsNamespace(),
7688
+ LOOP_ENGINE_SETTINGS_SCHEMA,
7689
+ // The managed block's engine survives the upgrade as the section's seed;
7690
+ // a deployment with no block keeps the in-process default.
7691
+ { engine: legacyEngine ?? "in-process", showInComposer: true },
7692
+ {
7693
+ setSource: (current) => {
7694
+ source = current;
7695
+ },
7696
+ onChange: () => {
7697
+ const next = source().engine;
7698
+ if (next === steered) return;
7699
+ steered = next;
7700
+ steerPresetDefault(next);
7701
+ }
7641
7702
  }
7642
- }
7703
+ );
7704
+ });
7705
+ return;
7706
+ }
7707
+ ctx.inject(["settings"], (settingsCtx) => {
7708
+ settingsCtx.effect(
7709
+ () => settingsCtx.settings.configure({ auto: false }, ctx.fiber),
7710
+ "loop-engine: settings page policy"
7643
7711
  );
7644
7712
  });
7713
+ let observedEngine = config.engine.get();
7714
+ ctx.on("settings/document-updated", () => {
7715
+ const next = config.engine.get();
7716
+ if (next === observedEngine) return;
7717
+ observedEngine = next;
7718
+ steerPresetDefault(next);
7719
+ });
7720
+ steerPresetDefault(legacyEngine ?? observedEngine);
7645
7721
  }
7646
7722
  export {
7647
7723
  Config5 as Config,
package/lib/invariant.js CHANGED
@@ -12,6 +12,14 @@ var LOOP_ENGINE_SETTINGS_SCHEMA = z.object({
12
12
  engine: z.union(LOOP_ENGINE_IDS.map((id) => z.const(id))).default("in-process"),
13
13
  showInComposer: z.boolean().default(true)
14
14
  });
15
+ function withVolatile(schema) {
16
+ const volatile = schema.volatile;
17
+ return volatile === void 0 ? schema : volatile.call(schema);
18
+ }
19
+ var LOOP_ENGINE_ENGINE_SCHEMA = withVolatile(
20
+ z.union(LOOP_ENGINE_IDS.map((id) => z.const(id))).default("in-process")
21
+ );
22
+ var LOOP_ENGINE_SHOW_IN_COMPOSER_SCHEMA = withVolatile(z.boolean().default(true));
15
23
 
16
24
  // src/patch-manager.ts
17
25
  var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block --";
@@ -1,7 +1,12 @@
1
1
  /**
2
- * Loop engine settings plugin, browser half. Registers the "Loop engine"
3
- * page under the settings section slot once the settings shell declares it,
4
- * binding one store to the duplicated `agent-loop-engine` settings scope.
2
+ * Loop engine settings plugin, browser half. Registers the "Loop engine" page
3
+ * under the settings section slot and binds one store to whichever settings
4
+ * client the running harness serves: the 0.1.7 `configForms` form for the
5
+ * plugin's own `loop-engine` profile entry, or the 0.1.5 `settingsScope` for its
6
+ * `agent-loop-engine` settings section. Two inject callbacks register the page,
7
+ * one per generation — whichever service exists fires, and the client bundle
8
+ * never imports a host package, so it cannot probe the generation like the node
9
+ * half does.
5
10
  * Export discipline: packages/client/AGENTS.md.
6
11
  * @module dsh-loop-engine/client
7
12
  */
@@ -17,12 +22,17 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
17
22
  'settings.loop-engine': LoopEngineKey;
18
23
  }
19
24
  }
20
- /** Required services (cordis fiber inject). The target slot is declared by
21
- * ui-settings' apply; registration depends on it through `slots.inject()`. */
25
+ /**
26
+ * Required services (cordis fiber inject). Only the services BOTH generations
27
+ * provide are listed statically: `configForms` (0.1.7) and `settingsScope`
28
+ * (0.1.5) are mutually exclusive, so each is injected inside its own callback
29
+ * rather than named here. The target slot is declared by ui-settings' apply;
30
+ * registration depends on it through `slots.inject()`.
31
+ */
22
32
  export declare const inject: string[];
23
33
  /**
24
34
  * Register the Loop engine section once the `settings.section` declaration is
25
- * on the ledger and bind its store to the duplicated settings scope.
35
+ * on the ledger and bind its store to whichever settings client is present.
26
36
  * @param ctx - client root context.
27
37
  */
28
38
  export declare function apply(ctx: ClientContext): void;
@@ -82,8 +82,9 @@ export type { SessionEngineReport } from '../agent-preset-ids.ts';
82
82
  /**
83
83
  * The standard seat members this half reads off a session-scope slot.
84
84
  *
85
- * Only the identity: the engine itself is no longer read from a framework hook,
86
- * it is asked of the host through this plugin's own Remote.
85
+ * The identity, plus the one live fact the turn-status row needs: whether the
86
+ * session is mid-turn. The engine itself is no longer read from a framework
87
+ * hook, it is asked of the host through this plugin's own Remote.
87
88
  */
88
89
  export interface SessionSeat {
89
90
  /**
@@ -92,6 +93,23 @@ export interface SessionSeat {
92
93
  * (which only ever renders with a session) shows nothing.
93
94
  */
94
95
  sessionId?: string;
96
+ /**
97
+ * Selector hook over the current Session's snapshot (`SessionStandardProps` of
98
+ * every session-scoped seat). Optional for the same reason as the identity: a
99
+ * caller with no such seat — or a test rendering one directly — has none, and
100
+ * the turn-status gate is then left alone rather than guessed.
101
+ */
102
+ useSession?: SnapshotSelector;
103
+ }
104
+ /**
105
+ * The selector-hook shape this half needs from `useSession`: a read of one field
106
+ * of the Session snapshot, with no equality function of its own.
107
+ */
108
+ export type SnapshotSelector = <S>(select: (snapshot: SessionRunningState) => S) => S;
109
+ /** The one field of the Session snapshot the turn-status row reads. */
110
+ export interface SessionRunningState {
111
+ /** Whether a turn is in flight for this session. */
112
+ readonly running: boolean;
95
113
  }
96
114
  /** Cordis service key AND wire namespace of the plugin's own Remote. */
97
115
  export declare const LOOP_ENGINE_REMOTE_NAMESPACE = "loopEngine";
@@ -239,14 +257,30 @@ export declare function engineSwitchReady(report: SessionEngineReport | undefine
239
257
  * this page.
240
258
  */
241
259
  export declare function switchNeedsReload(current: SessionEngine, target: LoopEngineId): boolean;
242
- /** A codec of one wire field (harness: `TypertCodec`, strict branch). */
260
+ /** One boundary schema of a wire field (a zod schema, as the Gateway holds it). */
261
+ interface RemoteSchema {
262
+ parse(value: unknown): unknown;
263
+ }
264
+ /**
265
+ * A codec of one wire field (harness: `TypertCodec`, strict branch).
266
+ *
267
+ * The two harness generations read DIFFERENT members of this object and each
268
+ * validates only its own: the 0.1.5 line's registry and client parse through
269
+ * `schema`, while the 0.1.7 line's registry requires a `create()` factory and
270
+ * parses through `codec.create()` (`typert: … strict codec has no create()
271
+ * factory`). One build serves both, so the codec carries both fields.
272
+ */
243
273
  interface RemoteCodec {
244
274
  readonly mode: 'strict';
245
275
  /** Stable identity of the field's declared type, for diagnostics. */
246
276
  readonly typeSymbol: string;
247
- readonly schema: {
248
- parse(value: unknown): unknown;
249
- };
277
+ /** The 0.1.5 line's read: the boundary schema itself. */
278
+ readonly schema: RemoteSchema;
279
+ /**
280
+ * The 0.1.7 line's read: a factory returning the boundary schema.
281
+ * @returns the same boundary schema.
282
+ */
283
+ readonly create: () => RemoteSchema;
250
284
  }
251
285
  /** One ordered business parameter (harness: `InvocationParameterDescriptor`). */
252
286
  interface RemoteParameter {
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Loop engine selection store: the durable settings scope is the transport,
3
- * and the store publishes a render-safe snapshot plus the write path.
2
+ * Loop engine selection store: a settings transport is the channel, and the
3
+ * store publishes a render-safe snapshot plus the write path. The transport is
4
+ * abstracted so one store follows either generation's client service — the
5
+ * 0.1.7 `ConfigForm` for the plugin's own profile entry, or the 0.1.5
6
+ * `SettingsScope` for its `agent-loop-engine` settings section.
4
7
  * @module dsh-loop-engine/client/store
5
8
  */
6
- import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
7
9
  import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
8
10
  import type { LoopEngineId } from '../agent-preset-ids.ts';
11
+ import type { LoopEngineSettings } from '../settings.ts';
9
12
  /** State rendered by the loop engine section. */
10
13
  export interface LoopEngineState {
11
14
  status: 'loading' | 'ready' | 'unavailable' | 'saving';
@@ -14,44 +17,76 @@ export interface LoopEngineState {
14
17
  writable: boolean;
15
18
  error: string | null;
16
19
  }
17
- /** Narrow a wire section to the stored engine id and display toggle; an invalid one reads default. */
20
+ /** The render-relevant slice of a settings snapshot, shared by both generations' transports. */
21
+ export interface LoopEngineSettingsSnapshot {
22
+ status: 'loading' | 'ready' | 'unavailable';
23
+ value: LoopEngineSettings | undefined;
24
+ writable: boolean;
25
+ }
26
+ /**
27
+ * The settings channel the store follows. Satisfied by the 0.1.7 `ConfigForm`
28
+ * and by the 0.1.5 `SettingsScope` (through a thin adapter in `./index.ts`); the
29
+ * write's boolean is optional because the 0.1.5 scope reports acceptance only
30
+ * through the snapshot it leaves behind.
31
+ */
32
+ export interface LoopEngineSettingsTransport {
33
+ /** @returns the current sync snapshot. */
34
+ getSnapshot(): LoopEngineSettingsSnapshot;
35
+ /**
36
+ * Observe snapshot replacements.
37
+ * @param listener - invoked after each snapshot change.
38
+ * @returns the disposer removing the listener.
39
+ */
40
+ subscribe(listener: () => void): () => void;
41
+ /**
42
+ * Persist one field.
43
+ * @param field - the field to write.
44
+ * @param value - the value to write.
45
+ * @returns whether the write was accepted, when the transport reports it.
46
+ */
47
+ set(field: 'engine' | 'showInComposer', value: unknown): Promise<boolean | void>;
48
+ }
49
+ /**
50
+ * Narrow a wire section to the stored engine id and display toggle; an invalid
51
+ * one reads default. Used by the 0.1.5 `settingsScope.bind` decoder, whose scope
52
+ * receives raw wire sections.
53
+ * @param section - the wire section value.
54
+ * @returns the decoded selection, or `undefined` when the section is unusable.
55
+ */
18
56
  export declare function decodeLoopEngine(section: unknown): {
19
57
  engine: LoopEngineId;
20
58
  showInComposer: boolean;
21
59
  } | undefined;
22
60
  /** Coordinates the settings-backed loop engine selection. */
23
61
  export declare class LoopEngineStore {
24
- private readonly scope;
62
+ private readonly transport;
25
63
  /** uSES-safe state source shared by the registered settings section. */
26
64
  readonly store: SnapshotStore<LoopEngineState>;
27
65
  private following;
28
66
  private saving;
29
67
  /**
30
- * @param scope - the loop engine settings namespace scope.
68
+ * @param transport - the settings channel the store follows.
31
69
  */
32
- constructor(scope: SettingsScope<{
33
- engine: LoopEngineId;
34
- showInComposer: boolean;
35
- }>);
36
- /** Begin following the bound scope and publish its current answer. */
70
+ constructor(transport: LoopEngineSettingsTransport);
71
+ /** Begin following the transport and publish its current answer. */
37
72
  load(): void;
38
73
  /**
39
- * Persist the selected engine. Success is judged against the snapshot the
40
- * write left behind, so a refused write reports error after its recovery.
74
+ * Persist the selected engine. Success is the transport's accepted answer
75
+ * (when it reports one) checked against the snapshot the write left behind,
76
+ * so a refused write reports error after its recovery.
41
77
  * @param engine - the engine to select for future Agent turns.
42
78
  * @returns whether the write landed.
43
79
  */
44
80
  setEngine(engine: LoopEngineId): Promise<boolean>;
45
81
  /**
46
- * Persist whether the composer shows the engine picker. Success is judged
47
- * against the snapshot the write left behind, so a refused write reports
48
- * error after its recovery. Unlike {@link setEngine}, landing does not reload
49
- * the page — the toggle only changes composer visibility.
82
+ * Persist whether the composer shows the engine picker. Unlike
83
+ * {@link setEngine}, landing does not reload the page — the toggle only
84
+ * changes composer visibility.
50
85
  * @param show - whether the chat page composer reveals the engine picker.
51
86
  * @returns whether the write landed.
52
87
  */
53
88
  setShowInComposer(show: boolean): Promise<boolean>;
54
- /** Stop following the scope. */
89
+ /** Stop following the transport. */
55
90
  dispose(): void;
56
91
  private derive;
57
92
  }
@@ -20,8 +20,8 @@
20
20
  * default, a session running pi included.
21
21
  *
22
22
  * The reflection's SUBJECT is still a document-level attribute rather than the
23
- * row's own element: the sheet has to reach a class the harness hashes
24
- * (`[class$="_turnStatus"]`), which takes an attribute selector on an ancestor,
23
+ * row's own element: the sheet has to reach a class the harness hashes (see
24
+ * {@link ROW_SELECTORS}), which takes an attribute selector on an ancestor,
25
25
  * and the row offers no session-scoped hook for a plugin to hang it on. A
26
26
  * document-level attribute has exactly ONE owner at a time, though — and on this
27
27
  * page the WRITERS outnumber the reader: the chip and the composer of every
@@ -47,10 +47,24 @@
47
47
  * paints by it, and a leftover attribute is inert on a page whose turn-status row
48
48
  * is not rendered.
49
49
  *
50
+ * The paint is also gated on the session being MID-TURN, through a second
51
+ * document-level attribute ({@link RUNNING_ATTR}, written from the same
52
+ * reflection). On the 0.1.5 line the row only exists while a turn is in flight,
53
+ * so the engine attribute alone was enough; on the 0.1.7 line that same button
54
+ * stays on screen after the turn ends, as the collapsed summary that reads
55
+ * "用时 4秒" — an engine-only gate leaves the glyph and the sweep animating on a
56
+ * finished turn, which is not what the live line means. The gate is a surface's
57
+ * own answer (`session.running`, which every session-scoped seat receives), so a
58
+ * surface that cannot answer passes nothing and leaves it alone rather than
59
+ * guessing.
60
+ *
50
61
  * Three facts about the harness markup make that safe and specific:
51
- * - the row carries exactly one class whose `[hash]_turnStatus` suffix is
52
- * stable across rebuilds (the hash in front is not), so
53
- * `[class$="_turnStatus"]` matches it and not the sibling clock;
62
+ * - the row has one stable handle per harness generation — an
63
+ * `[hash]_turnStatus` class suffix on the 0.1.5 line, and, on the 0.1.7
64
+ * line, the `data-turn-process` button whose text is a `<span>` whose hashed
65
+ * class ends `_label` — so each generation gets its own copy of the sheet
66
+ * ({@link ROW_SELECTORS}) and the one the running app renders matches while
67
+ * the other stays inert;
54
68
  * - its gradient paints through the `--dsw-static-deepseek-*` custom
55
69
  * properties, so recoloring is a variable override rather than a fight
56
70
  * over `background` and `background-clip`;
@@ -93,6 +107,11 @@ import type { LoopEngineId } from '../agent-preset-ids.ts';
93
107
  * clears the attribute, because the sibling surface of the same session is still
94
108
  * on screen painting through it.
95
109
  *
110
+ * The MID-TURN gate is written from here as well, but only by a surface that can
111
+ * answer it (`running` given): the sheet is gated on both, because the 0.1.7 row
112
+ * outlives its turn, and a surface with no answer must leave the gate as the
113
+ * surface that has one left it rather than guess.
114
+ *
96
115
  * The NO-OP covers the late reflection as well — the one from the session the
97
116
  * user has just left, whose own answer landing after the switch used to paint
98
117
  * over the session that replaced it.
@@ -101,8 +120,10 @@ import type { LoopEngineId } from '../agent-preset-ids.ts';
101
120
  * row): such a page has nothing to say about the row, so nothing is written and
102
121
  * nothing is withdrawn — it leaves the row it found alone.
103
122
  * @param engine - the engine that session runs, when it is known.
123
+ * @param running - whether that session is mid-turn, when the surface knows;
124
+ * omitted by a surface with no answer, which leaves the gate alone.
104
125
  */
105
- export declare function reflectTurnStatusEngine(sessionId: string | undefined, engine: LoopEngineId | undefined): void;
126
+ export declare function reflectTurnStatusEngine(sessionId: string | undefined, engine: LoopEngineId | undefined, running?: boolean): void;
106
127
  /**
107
128
  * Declare which session's surfaces are on screen — the row's one subject.
108
129
  *
@@ -55,12 +55,20 @@ import type { SessionEngineCache } from './session-engine.ts';
55
55
  *
56
56
  * One effect does the whole thing, so the three steps cannot come apart: declare
57
57
  * this session as the row's subject (`focusTurnStatusSession`), reflect its
58
- * engine by that declaration (`reflectTurnStatusEngine`), and release the
59
- * declaration when this surface goes away (`blurTurnStatusSession`, which
60
- * withdraws it only while it is still this session's).
58
+ * engine and its mid-turn gate by that declaration (`reflectTurnStatusEngine`),
59
+ * and release the declaration when this surface goes away
60
+ * (`blurTurnStatusSession`, which withdraws it only while it is still this
61
+ * session's).
62
+ *
63
+ * `running` is the session's own live state (`session.running`, from the
64
+ * session-scoped standard kit) and it is part of the effect's key: the 0.1.7 row
65
+ * stays on screen after the turn ends, so the paint has to come off when the turn
66
+ * does — a surface that does not know the state passes nothing and leaves the
67
+ * gate alone.
61
68
  * @param cache - the cache the plugin mounted.
62
69
  * @param sessionId - the session to follow, or undefined off a session scope.
70
+ * @param running - whether that session is mid-turn, when the surface knows.
63
71
  * @returns the engine report, or undefined while it is unknown.
64
72
  */
65
- export declare function useEngineOfSession(cache: SessionEngineCache, sessionId: string | undefined): SessionEngineReport | undefined;
73
+ export declare function useEngineOfSession(cache: SessionEngineCache, sessionId: string | undefined, running?: boolean): SessionEngineReport | undefined;
66
74
  //# sourceMappingURL=use-session-engine.d.ts.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Runtime harness-generation probe. One released build must serve the 0.1.5
3
+ * line (`0.1.5-rc.1` … `0.1.5-rc.3`, all sharing the OLD settings API) and the
4
+ * 0.1.7 line (`0.1.7-rc.1`), which rewrote the settings host and client and
5
+ * turned the agent-creation announcement async. The differences are not
6
+ * version strings but API SHAPES, so the switch is a structural probe of the
7
+ * imported module rather than a semver parse: `@deepseek-ai/dsh-settings`
8
+ * exported a named `SettingsProvider` class on the 0.1.5 line, and the 0.1.7
9
+ * rewrite deleted it in favour of `SettingsForms` plus per-entry volatile
10
+ * Config fields.
11
+ *
12
+ * The probe resolves to whichever `@deepseek-ai/dsh-settings` the running
13
+ * profile actually provides. It is host-side only: the browser bundle must not
14
+ * import host packages, so `src/client/*` never imports this module.
15
+ *
16
+ * @module dsh-loop-engine/compat
17
+ */
18
+ /**
19
+ * Whether the running harness predates the profile-backed settings rewrite
20
+ * (the 0.1.5 line). `true` selects the legacy settings host/client, the
21
+ * synchronous agent-creation announcement, and the legacy tool-result message
22
+ * introspection.
23
+ */
24
+ export declare const LEGACY_HARNESS: boolean;
25
+ //# sourceMappingURL=compat.d.ts.map