@aiwayds/dsh-tui-pi 2.23.1 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/model-list.js CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Pure model-list assembly for the /model picker — favorites pinned on top,
3
- * hidden models in a dim section at the bottom, everything between. No TUI
4
- * imports: every function here is plain data-in/data-out, so the ordering,
5
- * filtering and toggle semantics stay unit-testable without a terminal
3
+ * hidden models in a dim section at the bottom, everything between. Every
4
+ * section lists its models aggregated under their provider (providers
5
+ * alphabetical, models alphabetical within a provider). No TUI imports:
6
+ * every function here is plain data-in/data-out, so the ordering, filtering
7
+ * and toggle semantics stay unit-testable without a terminal
6
8
  * (test/model-list.test.mjs).
7
9
  */
8
10
  /** Composite settings key of a model: `${provider}/${id}`. */
@@ -18,6 +20,22 @@ export function matchesModelFilter(model, query) {
18
20
  || model.id.toLowerCase().includes(needle)
19
21
  || model.provider.toLowerCase().includes(needle));
20
22
  }
23
+ /**
24
+ * Display order of every picker section: providers aggregate alphabetically,
25
+ * and within a provider models sort by the label the MODEL column shows
26
+ * (name, falling back to id) with the id as the tiebreak. The listing
27
+ * arrives from pickModel as a Promise.all race across providers, so the
28
+ * input order interleaves providers by resolve speed — this comparator is
29
+ * what makes the rows deterministic.
30
+ */
31
+ export function compareListedModels(a, b) {
32
+ const byProvider = a.provider.localeCompare(b.provider);
33
+ if (byProvider !== 0)
34
+ return byProvider;
35
+ const labelA = a.name === '' ? a.id : a.name;
36
+ const labelB = b.name === '' ? b.id : b.name;
37
+ return labelA.localeCompare(labelB) || a.id.localeCompare(b.id);
38
+ }
21
39
  /**
22
40
  * Toggle `key` in `list`: remove when present, append when absent (join
23
41
  * order = favorite order). Returns a new array; the input is never mutated.
@@ -44,33 +62,39 @@ export function narrowStringList(value) {
44
62
  /**
45
63
  * Assemble the picker rows, top to bottom:
46
64
  *
47
- * 1. Favorites (`section: 'favorite'`, ★ prefix applied by renderCell), in
48
- * join order — a favorited model is removed from every other section;
65
+ * 1. Favorites (`section: 'favorite'`, ★ prefix applied by renderCell) —
66
+ * display order is the shared provider-grouped alphabetical order, not
67
+ * the join order of the persisted `favoriteModels` list (persistence
68
+ * keeps join order; only the rendering sorts). A favorited model is
69
+ * removed from every other section;
49
70
  * 2. one full-width divider row — only when at least one favorite is visible
50
71
  * AND something follows it;
51
- * 3. the remaining models (`section: 'normal'`) in listing order;
72
+ * 3. the remaining models (`section: 'normal'`) in the same order;
52
73
  * 4. the Hidden section — only without an active filter and when non-empty:
53
74
  * a `hiddenHeader` row carrying the count, then the hidden models
54
- * (`section: 'hidden'`, rendered dim) that are not also favorited.
75
+ * (`section: 'hidden'`, rendered dim) that are not also favorited,
76
+ * likewise in the shared order.
55
77
  *
56
78
  * An active `filter` keeps the pinned structure but leaves only matching
57
79
  * models per section; stale keys in `favorites`/`hidden` (models no longer
58
80
  * listed by any provider) are skipped silently.
59
81
  */
60
82
  export function buildModelRows(models, favorites, hidden, filter = '') {
61
- const byKey = new Map(models.map(model => [modelKey(model), model]));
83
+ const sorted = [...models].sort(compareListedModels);
84
+ const byKey = new Map(sorted.map(model => [modelKey(model), model]));
62
85
  const favKeys = new Set(favorites);
63
86
  const hiddenSet = new Set(hidden);
64
87
  const favRows = favorites
65
88
  .map(key => byKey.get(key))
66
89
  .filter((model) => model !== undefined && matchesModelFilter(model, filter))
90
+ .sort(compareListedModels)
67
91
  .map(model => ({ kind: 'model', section: 'favorite', model }));
68
- const normalRows = models
92
+ const normalRows = sorted
69
93
  .filter(model => !favKeys.has(modelKey(model))
70
94
  && !hiddenSet.has(modelKey(model))
71
95
  && matchesModelFilter(model, filter))
72
96
  .map(model => ({ kind: 'model', section: 'normal', model }));
73
- const hiddenModels = models.filter(model => hiddenSet.has(modelKey(model)) && !favKeys.has(modelKey(model)));
97
+ const hiddenModels = sorted.filter(model => hiddenSet.has(modelKey(model)) && !favKeys.has(modelKey(model)));
74
98
  const showHidden = filter.trim() === '' && hiddenModels.length > 0;
75
99
  const rows = [...favRows];
76
100
  if (favRows.length > 0 && (normalRows.length > 0 || showHidden)) {
@@ -1 +1 @@
1
- {"version":3,"file":"model-list.js","sourceRoot":"","sources":["../src/model-list.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model-list.js","sourceRoot":"","sources":["../src/model-list.ts"],"names":[],"mappings":"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"}
package/lib/session.d.ts CHANGED
@@ -13,6 +13,8 @@ import type { Context } from '@deepseek-ai/cordis';
13
13
  import type { Agent, AgentHandle, ModelSelection } from '@deepseek-ai/dsh-agent';
14
14
  import { type StreamChunk } from '@deepseek-ai/dsh-llm';
15
15
  import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session';
16
+ import { type AttentionInfo, type AttentionRow } from './attention.ts';
17
+ import type { AttentionJevSource } from './attention-jev.ts';
16
18
  import type { AgentView } from './dsh-events.ts';
17
19
  import type { PendingPromptView } from './steer-flow.ts';
18
20
  export interface BridgeCallbacks {
@@ -215,6 +217,48 @@ export declare class DshSessionBridge {
215
217
  });
216
218
  /** Whether a `reconcileLiveWithProjection` pass is already in flight. */
217
219
  private projectionReconcileInFlight;
220
+ /**
221
+ * Shared child-adoption gate for the two discovery paths: the firehose
222
+ * `session/event` fold (constructor) and the reconcile tick's live-store
223
+ * sweep (`reconcileLiveFromStore`). A session whose parent is a tracked
224
+ * session AND is marked as delegated is a child; the descriptor event
225
+ * later refines the label/provider.
226
+ *
227
+ * Guard: a child is `origin: 'subagent'` OR a delegation budget
228
+ * `delegationDepth > 0`. Both markers are written together by dsh's
229
+ * childSessionMeta (spawn AND in-process fork children alike), so in
230
+ * practice the origin alone carries the decision; the budget clause
231
+ * only future-proofs the gate against a child that carries the budget
232
+ * without the origin. The budget test MUST be a VALUE test, not a
233
+ * field-presence test: the jsonl persistence backend materialises
234
+ * `delegationDepth: 0` on every restored header (write `?? 0`, read
235
+ * unconditionally), so a presence test would pull user-facing
236
+ * `Session.fork` conversations and other non-child restored sessions
237
+ * onto the live board / Ctrl+G.
238
+ *
239
+ * @returns whether a new view was adopted (caller emits).
240
+ */
241
+ private adoptChildSession;
242
+ /**
243
+ * Live-store sweep — the corpus-level companion to
244
+ * `reconcileLiveWithProjection` (dsh 0.1.7-rc.2). `listDescendants` walked
245
+ * the complete Session corpus on rc.1 but recurses parent CATALOGS on
246
+ * rc.2, so a child whose catalog branch is corrupt/unreadable — or whose
247
+ * catalog entry is missing entirely — is omitted from the authoritative
248
+ * listing with no diagnostic row of its own. Every still-working child
249
+ * necessarily resides in the process, and the store's live list is the one
250
+ * enumeration that depends on no catalog: this sweep re-runs the shared
251
+ * adoption gate over `sessions.list()` each tick and picks up what both
252
+ * the firehose (burst-lost creation events) and the catalog walk missed.
253
+ * Merge discipline is the bridge-wide one — only ADD (the
254
+ * `agentViews.has` guard makes it idempotent and never resurrects a
255
+ * settled view); rounds/settlement continue through the ordinary fold
256
+ * paths once the child is in `childSessions`/`trackedSessions`. Cost is
257
+ * one header check per process-resident session per tick — no persistence
258
+ * read, and children of previous runs are not resident after a restart,
259
+ * so they cannot be resurrected onto the board.
260
+ */
261
+ private reconcileLiveFromStore;
218
262
  /**
219
263
  * Correct the live view against the host's authoritative descendant
220
264
  * listing (`SubagentRuntime.listDescendants`, projection-backed — no
@@ -235,6 +279,13 @@ export declare class DshSessionBridge {
235
279
  * resurrect a settled continuable (its listing row is `inactive` while it
236
280
  * idles between turns). Runs best-effort: an absent/unshaped subagents
237
281
  * service or a throwing listing is silently skipped.
282
+ *
283
+ * dsh 0.1.7-rc.2: the listing recurses parent CATALOGS instead of walking
284
+ * the complete Session corpus — a child behind a corrupt/unreadable
285
+ * catalog branch is reported only as a branch diagnostic, and a truly
286
+ * catalog-less orphan is invisible (no row at all). Diagnostic rows are
287
+ * skipped here; the {@link reconcileLiveFromStore} sweep is the
288
+ * corpus-level companion that keeps such children off the blind spot.
238
289
  */
239
290
  private reconcileLiveWithProjection;
240
291
  /**
@@ -269,6 +320,28 @@ export declare class DshSessionBridge {
269
320
  * policy's hard-stop records composed in (they live off-view — see
270
321
  * markChildHardStopped). */
271
322
  getAgentViews(): readonly AgentView[];
323
+ /**
324
+ * Attach the attention ranking's jev upgrade source (heuristic-only when
325
+ * undefined) and the live maxRounds reader for the ranking's state table.
326
+ * Wired once by the host; the source self-debounces and self-guards.
327
+ */
328
+ attachAttentionSource(source: AttentionJevSource | undefined, capReader: () => number): void;
329
+ /** Re-observe the attention ranking; driven by the reconcile tick. */
330
+ private updateAttention;
331
+ /** One child's current attention info (heuristic base; jev once it has judged). */
332
+ getAttention(childId: string): AttentionInfo | undefined;
333
+ /** The spin-strike state for one child (the early-stop ladder's input). */
334
+ getSpinState(childId: string): {
335
+ streak: number;
336
+ lastStrikeAt: number;
337
+ };
338
+ /** All attention rows, highest score first (picker sort, status board). */
339
+ getAttentionRows(): readonly AttentionRow[];
340
+ /** The guard deny-copy hint: the top attention-worthy live child, when one ranks above ok. */
341
+ getAttentionHint(): {
342
+ label: string;
343
+ detail: string;
344
+ } | undefined;
272
345
  /** Children still running (`outcome` unset) — the live count the guard caps. */
273
346
  getLiveChildren(): readonly AgentView[];
274
347
  /** The buffered transcript of one child, in log order (capped, maybe truncated). */
@@ -369,9 +442,13 @@ export declare class DshSessionBridge {
369
442
  * after a successful `cancelChild`; a stop that found nothing live is
370
443
  * deliberately not recorded (the child settled on its own in time).
371
444
  */
372
- markChildHardStopped(childId: string, round: number, cap: number): void;
445
+ markChildHardStopped(childId: string, round: number, cap: number, early?: true): void;
373
446
  /** Hard stops the maxRounds policy executed, keyed by child session id. */
374
447
  private readonly hardStopRecords;
448
+ /** Attention ranking (heuristic base + optional jev layer) — see src/attention.ts. */
449
+ private readonly attentionBoard;
450
+ private attentionSource;
451
+ private attentionCapReader;
375
452
  /**
376
453
  * Whether ANY LLM work is in flight: the bridge's own mid-turn status OR
377
454
  * at least one live child. The keymap's running gate reads this — with
package/lib/session.js CHANGED
@@ -14,6 +14,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm';
14
14
  import { SettingsConflictError } from '@deepseek-ai/dsh-settings';
15
15
  import { SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
16
16
  import { readAppendSystem } from "./append-system.js";
17
+ import { AttentionBoard } from "./attention.js";
17
18
  import { statSync } from 'node:fs';
18
19
  import { join } from 'node:path';
19
20
  import { descriptorModelRoute, isAgentEnd, isAgentStart, isDcpCompactionNotice, isLlmRetry, isSubagentDescriptor, isTuiPluginInjection } from "./dsh-events.js";
@@ -355,45 +356,10 @@ export class DshSessionBridge {
355
356
  const sessionKey = String(session.id);
356
357
  // Discover subagent children by session header — the deployment may
357
358
  // never emit tool-workflow events (the firehose is not scope-filtered,
358
- // so child sessions arrive here too). A session whose parent is a
359
- // tracked session AND is marked as delegated is a child; the descriptor
360
- // event later refines the label/provider.
361
- //
362
- // Guard: a child is `origin: 'subagent'` OR a delegation budget
363
- // `delegationDepth > 0`. Both markers are written together by dsh's
364
- // childSessionMeta (spawn AND in-process fork children alike), so in
365
- // practice the origin alone carries the decision; the budget clause
366
- // only future-proofs the gate against a child that carries the budget
367
- // without the origin. The budget test MUST be a VALUE test, not a
368
- // field-presence test: the jsonl persistence backend materialises
369
- // `delegationDepth: 0` on every restored header (write `?? 0`, read
370
- // unconditionally), so a presence test would pull user-facing
371
- // `Session.fork` conversations and other non-child restored sessions
372
- // onto the live board / Ctrl+G.
373
- const header = session.header;
374
- if (header?.parentSession !== undefined
375
- && (header.origin === 'subagent' || (header.delegationDepth ?? 0) > 0)
376
- && this.trackedSessions.has(String(header.parentSession))
377
- && !this.agentViews.has(sessionKey)) {
378
- this.agentViews.set(sessionKey, {
379
- childId: sessionKey,
380
- parentSession: String(header.parentSession),
381
- // Current dsh children always carry the origin, so in practice the
382
- // label is `subagent <id8>`; the `fork` label only fires for the
383
- // defensive budget-without-origin shape above.
384
- label: header.origin === undefined
385
- ? `fork ${sessionKey.slice(0, 8)}`
386
- : `subagent ${sessionKey.slice(0, 8)}`,
387
- startedAt: event.time,
388
- tokens: 0,
389
- rounds: 0,
390
- retries: 0,
391
- contextTokens: 0,
392
- });
393
- this.childSessions.add(sessionKey);
394
- this.trackedSessions.add(sessionKey);
359
+ // so child sessions arrive here too). The gate is shared with the
360
+ // reconcile tick's live-store sweep (`reconcileLiveFromStore`).
361
+ if (this.adoptChildSession(session, event.time))
395
362
  this.emitLive();
396
- }
397
363
  // Fold workflow + child events for every tracked session.
398
364
  if (this.trackedSessions.has(sessionKey)) {
399
365
  this.foldTracked(sessionKey, event);
@@ -436,10 +402,17 @@ export class DshSessionBridge {
436
402
  // cap must still fire while the child works in the background.
437
403
  const reconcile = setInterval(() => {
438
404
  try {
405
+ // Live-store sweep first (dsh 0.1.7-rc.2 companion): a child adopted
406
+ // here is in childSessions before the round reconcile scans it, so a
407
+ // recovered child gets its rounds the same tick.
408
+ this.reconcileLiveFromStore();
439
409
  this.reconcileChildRounds();
440
410
  // The projection-backed reconciliation rides the same cadence but is
441
411
  // async and self-guarded (never overlaps itself).
442
412
  void this.reconcileLiveWithProjection();
413
+ // Attention ranking re-observe (heuristic recompute is a map walk;
414
+ // the jev layer self-debounces inside its source).
415
+ this.updateAttention();
443
416
  }
444
417
  catch {
445
418
  // A throwing reconcile must never take the process down.
@@ -450,6 +423,85 @@ export class DshSessionBridge {
450
423
  }
451
424
  /** Whether a `reconcileLiveWithProjection` pass is already in flight. */
452
425
  projectionReconcileInFlight = false;
426
+ /**
427
+ * Shared child-adoption gate for the two discovery paths: the firehose
428
+ * `session/event` fold (constructor) and the reconcile tick's live-store
429
+ * sweep (`reconcileLiveFromStore`). A session whose parent is a tracked
430
+ * session AND is marked as delegated is a child; the descriptor event
431
+ * later refines the label/provider.
432
+ *
433
+ * Guard: a child is `origin: 'subagent'` OR a delegation budget
434
+ * `delegationDepth > 0`. Both markers are written together by dsh's
435
+ * childSessionMeta (spawn AND in-process fork children alike), so in
436
+ * practice the origin alone carries the decision; the budget clause
437
+ * only future-proofs the gate against a child that carries the budget
438
+ * without the origin. The budget test MUST be a VALUE test, not a
439
+ * field-presence test: the jsonl persistence backend materialises
440
+ * `delegationDepth: 0` on every restored header (write `?? 0`, read
441
+ * unconditionally), so a presence test would pull user-facing
442
+ * `Session.fork` conversations and other non-child restored sessions
443
+ * onto the live board / Ctrl+G.
444
+ *
445
+ * @returns whether a new view was adopted (caller emits).
446
+ */
447
+ adoptChildSession(session, startedAt) {
448
+ const sessionKey = String(session.id);
449
+ const header = session.header;
450
+ if (header?.parentSession === undefined
451
+ || (header.origin !== 'subagent' && (header.delegationDepth ?? 0) <= 0)
452
+ || !this.trackedSessions.has(String(header.parentSession))
453
+ || this.agentViews.has(sessionKey))
454
+ return false;
455
+ this.agentViews.set(sessionKey, {
456
+ childId: sessionKey,
457
+ parentSession: String(header.parentSession),
458
+ // Current dsh children always carry the origin, so in practice the
459
+ // label is `subagent <id8>`; the `fork` label only fires for the
460
+ // defensive budget-without-origin shape above.
461
+ label: header.origin === undefined
462
+ ? `fork ${sessionKey.slice(0, 8)}`
463
+ : `subagent ${sessionKey.slice(0, 8)}`,
464
+ startedAt,
465
+ tokens: 0,
466
+ rounds: 0,
467
+ retries: 0,
468
+ contextTokens: 0,
469
+ });
470
+ this.childSessions.add(sessionKey);
471
+ this.trackedSessions.add(sessionKey);
472
+ return true;
473
+ }
474
+ /**
475
+ * Live-store sweep — the corpus-level companion to
476
+ * `reconcileLiveWithProjection` (dsh 0.1.7-rc.2). `listDescendants` walked
477
+ * the complete Session corpus on rc.1 but recurses parent CATALOGS on
478
+ * rc.2, so a child whose catalog branch is corrupt/unreadable — or whose
479
+ * catalog entry is missing entirely — is omitted from the authoritative
480
+ * listing with no diagnostic row of its own. Every still-working child
481
+ * necessarily resides in the process, and the store's live list is the one
482
+ * enumeration that depends on no catalog: this sweep re-runs the shared
483
+ * adoption gate over `sessions.list()` each tick and picks up what both
484
+ * the firehose (burst-lost creation events) and the catalog walk missed.
485
+ * Merge discipline is the bridge-wide one — only ADD (the
486
+ * `agentViews.has` guard makes it idempotent and never resurrects a
487
+ * settled view); rounds/settlement continue through the ordinary fold
488
+ * paths once the child is in `childSessions`/`trackedSessions`. Cost is
489
+ * one header check per process-resident session per tick — no persistence
490
+ * read, and children of previous runs are not resident after a restart,
491
+ * so they cannot be resurrected onto the board.
492
+ */
493
+ reconcileLiveFromStore() {
494
+ const sessions = this.ctx.get('sessions');
495
+ if (sessions?.list === undefined)
496
+ return;
497
+ let changed = false;
498
+ for (const session of sessions.list()) {
499
+ if (this.adoptChildSession(session, Date.now()))
500
+ changed = true;
501
+ }
502
+ if (changed)
503
+ this.emitLive();
504
+ }
453
505
  /**
454
506
  * Correct the live view against the host's authoritative descendant
455
507
  * listing (`SubagentRuntime.listDescendants`, projection-backed — no
@@ -470,6 +522,13 @@ export class DshSessionBridge {
470
522
  * resurrect a settled continuable (its listing row is `inactive` while it
471
523
  * idles between turns). Runs best-effort: an absent/unshaped subagents
472
524
  * service or a throwing listing is silently skipped.
525
+ *
526
+ * dsh 0.1.7-rc.2: the listing recurses parent CATALOGS instead of walking
527
+ * the complete Session corpus — a child behind a corrupt/unreadable
528
+ * catalog branch is reported only as a branch diagnostic, and a truly
529
+ * catalog-less orphan is invisible (no row at all). Diagnostic rows are
530
+ * skipped here; the {@link reconcileLiveFromStore} sweep is the
531
+ * corpus-level companion that keeps such children off the blind spot.
473
532
  */
474
533
  async reconcileLiveWithProjection() {
475
534
  if (this.projectionReconcileInFlight)
@@ -478,6 +537,9 @@ export class DshSessionBridge {
478
537
  if (root === undefined)
479
538
  return;
480
539
  // ctx.get (NOT property access) — see the sessions accessor note above.
540
+ // Inline rc.2 `SubagentDescendantListEntry` union: child rows (catalog
541
+ // walk) and branch diagnostics share the list; `kind` has been on every
542
+ // row since rc.1, so the discriminator is floor-safe.
481
543
  const subagents = this.ctx.get('subagents');
482
544
  if (subagents?.listDescendants === undefined)
483
545
  return;
@@ -487,13 +549,24 @@ export class DshSessionBridge {
487
549
  const rootKey = String(root);
488
550
  let changed = false;
489
551
  for (const entry of entries) {
552
+ // A branch diagnostic (corrupt/unavailable/unsupported) names no
553
+ // child row — skipping it here is what the sweep above covers for.
554
+ if (entry.kind === 'diagnostic')
555
+ continue;
490
556
  const childKey = String(entry.id);
491
- if (childKey === rootKey || this.agentViews.has(childKey))
557
+ if (childKey === rootKey)
492
558
  continue;
493
559
  // A running child this bridge never discovered (burst, or its events
494
560
  // never bubbled). Register it header-discovery-shaped; descriptor
495
- // refinement (label/mode/route) follows from the listing itself.
561
+ // refinement (label/mode/route) follows from the listing itself. A
562
+ // child already on the board needs no listing re-add — its own events
563
+ // drive the fold. (The existence guard lives HERE, not on the loop
564
+ // head: the settle arm below must still reach existing views — a
565
+ // loop-head guard made it dead code and stale one-shots lingered on
566
+ // the board holding maxAgents slots.)
496
567
  if (entry.activity === 'running') {
568
+ if (this.agentViews.has(childKey))
569
+ continue;
497
570
  this.agentViews.set(childKey, {
498
571
  childId: childKey,
499
572
  parentSession: entry.parentId !== undefined ? String(entry.parentId) : undefined,
@@ -655,6 +728,38 @@ export class DshSessionBridge {
655
728
  return hs === undefined || view.hardStop !== undefined ? view : { ...view, hardStop: hs };
656
729
  });
657
730
  }
731
+ /**
732
+ * Attach the attention ranking's jev upgrade source (heuristic-only when
733
+ * undefined) and the live maxRounds reader for the ranking's state table.
734
+ * Wired once by the host; the source self-debounces and self-guards.
735
+ */
736
+ attachAttentionSource(source, capReader) {
737
+ this.attentionSource = source;
738
+ this.attentionCapReader = capReader;
739
+ }
740
+ /** Re-observe the attention ranking; driven by the reconcile tick. */
741
+ updateAttention() {
742
+ const views = this.getLiveChildren();
743
+ const cap = this.attentionCapReader?.() ?? 0;
744
+ this.attentionBoard.update(views, cap);
745
+ this.attentionSource?.tick(this.attentionBoard, views, cap);
746
+ }
747
+ /** One child's current attention info (heuristic base; jev once it has judged). */
748
+ getAttention(childId) {
749
+ return this.attentionBoard.get(childId);
750
+ }
751
+ /** The spin-strike state for one child (the early-stop ladder's input). */
752
+ getSpinState(childId) {
753
+ return this.attentionBoard.spinState(childId);
754
+ }
755
+ /** All attention rows, highest score first (picker sort, status board). */
756
+ getAttentionRows() {
757
+ return this.attentionBoard.rows();
758
+ }
759
+ /** The guard deny-copy hint: the top attention-worthy live child, when one ranks above ok. */
760
+ getAttentionHint() {
761
+ return this.attentionBoard.topHint();
762
+ }
658
763
  /** Children still running (`outcome` unset) — the live count the guard caps. */
659
764
  getLiveChildren() {
660
765
  return this.getAgentViews().filter(view => view.outcome === undefined);
@@ -967,17 +1072,21 @@ export class DshSessionBridge {
967
1072
  * after a successful `cancelChild`; a stop that found nothing live is
968
1073
  * deliberately not recorded (the child settled on its own in time).
969
1074
  */
970
- markChildHardStopped(childId, round, cap) {
1075
+ markChildHardStopped(childId, round, cap, early) {
971
1076
  // The record lives in its OWN map, not on the view: fold updates spread
972
1077
  // the view snapshot captured at their own event-dispatch time, and the
973
1078
  // cancel races those events — a hardStop folded onto the view object was
974
1079
  // observably overwritten by the in-flight `turn/end` spread. The map is
975
1080
  // authoritative; getAgentViews composes it onto the returned views.
976
- this.hardStopRecords.set(childId, { round, cap });
1081
+ this.hardStopRecords.set(childId, { round, cap, ...(early !== undefined ? { early } : {}) });
977
1082
  this.emitLive();
978
1083
  }
979
1084
  /** Hard stops the maxRounds policy executed, keyed by child session id. */
980
1085
  hardStopRecords = new Map();
1086
+ /** Attention ranking (heuristic base + optional jev layer) — see src/attention.ts. */
1087
+ attentionBoard = new AttentionBoard();
1088
+ attentionSource;
1089
+ attentionCapReader;
981
1090
  /**
982
1091
  * Whether ANY LLM work is in flight: the bridge's own mid-turn status OR
983
1092
  * at least one live child. The keymap's running gate reads this — with