@treeseed/dsh-chapters 0.2.3 → 0.2.5

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/docs/contract.md CHANGED
@@ -604,7 +604,17 @@ and does not let a plugin do, with anchors:
604
604
  `{value.result.kind/text}`). New **wire events are NOT available to plugins**: the forwarded
605
605
  allowlist is app-owned ("the one home of this application's forwarded-Host-event allowlist",
606
606
  `packages/api/remotes/src/remote-events.ts:24`).
607
- - **Push transport is 0.2.x-only.** `appendPluginRecord(session, 'plugin:<name>', data)`
607
+ **Every execution is recorded** (measured, `packages/interaction/commands/src/index.ts:365-385`):
608
+ `command/run` + `command/done` lifecycle events, log-only (never model surface) but rendered as
609
+ history nodes by the web UI; `recordInput: false` (definition flag, `index.ts:71-75`) suppresses
610
+ only the `args` payload, NOT the node pair. There is no quiet-invocation mode — a polling client
611
+ on this wire writes visible history every pull, which is why the bar's cadence is a tested
612
+ decision (#193) and the push swap (#194) is the structural fix.
613
+ - **Push transport is 0.2.1+ (measured 2026-10-30, #193 — this line said "0.2.x-only" and was
614
+ wrong):** `appendPluginRecord` has **zero occurrences** at tag `dsh-v0.2.0-rc.2`
615
+ (`packages/core/session/src/index.ts`) and is present at `dsh-v0.2.1-rc.2` (`:492`). The pinned
616
+ rig host (0.2.0-rc.2) therefore CANNOT serve the push swap; #194 gates on a host-pin bump plus
617
+ feature detection with pull fallback. `appendPluginRecord(session, 'plugin:<name>', data)`
608
618
  (`packages/core/session/src/index.ts:492`, grammar/types at `types.ts:525-543`, reader
609
619
  `pluginRecordOf` at `index.ts:508`): durable, append-only, "never enters the model-visible
610
620
  surface", retained by fork/resume, skipped by unknown readers. The `verify-plugin-record-callers`
package/lib/client.js CHANGED
@@ -94,7 +94,7 @@ window.__ModuleLoader__.load({
94
94
  case "goal": return {
95
95
  ...state,
96
96
  goalPhase: ev.phase,
97
- autorun: ev.phase === "complete" ? "disarmed" : state.autorun
97
+ autorun: ev.phase === "complete" || ev.phase === "blocked" ? "disarmed" : state.autorun
98
98
  };
99
99
  }
100
100
  return state;
@@ -105,6 +105,23 @@ window.__ModuleLoader__.load({
105
105
  if (s.status === "ready" || s.status === "failed") return "Recommended Next Step";
106
106
  return null;
107
107
  }
108
+ /**
109
+ * #193: the pull cadence, as a decision instead of an interval constant. Every pull is two recorded
110
+ * host lifecycle events (`command/run` + `command/done`) the UI renders as history nodes — cadence
111
+ * IS the litter, so it belongs in this module with a row for every arm. Active = the state can
112
+ * plausibly change on a short clock (generation in flight, an armed loop to fire, a turn running
113
+ * that will end into an offer). Quiet = stable; one pull a minute is plenty for a state that only
114
+ * changes at turn boundaries. Hidden-and-quiet = pull NOTHING (a background tab should write zero
115
+ * history); hidden-and-active still pulls, because walking away from a running loop is the point of
116
+ * autorun and pausing it silently would be the loop dying with nobody told.
117
+ */
118
+ const ACTIVE_POLL_MS = 1e4;
119
+ const QUIET_POLL_MS = 6e4;
120
+ function pollSpacingFor(s) {
121
+ const active = s.status === "analyzing" || s.autorun === "armed" || s.running;
122
+ if (!active && s.hidden) return null;
123
+ return active ? ACTIVE_POLL_MS : QUIET_POLL_MS;
124
+ }
108
125
  function barVisible(s) {
109
126
  if (s.status === "off") return false;
110
127
  if (s.status === "ready" && (s.text === null || s.text === "")) return false;
@@ -119,7 +136,7 @@ window.__ModuleLoader__.load({
119
136
  if (s.goal !== null && s.goal.phase === "active" && s.goal.activation === "armed") return false;
120
137
  if (s.autorun !== "armed") return false;
121
138
  if (s.running) return false;
122
- if (s.goalPhase === "complete") return false;
139
+ if (s.goalPhase === "complete" || s.goalPhase === "blocked") return false;
123
140
  if (s.hiddenTurnSeq !== null && s.hiddenTurnSeq === s.turnSeq) return false;
124
141
  return true;
125
142
  }
@@ -310,9 +327,6 @@ window.__ModuleLoader__.load({
310
327
  const PULL_TIMEOUT_MS = 15e3;
311
328
  /** Retry ceiling for the FIRST pull only, in attempts and spacing. A mount-time race must not be able to
312
329
  * starve the bar forever, and a real `off` answer stops the loop on the first reply either way. */
313
- /** Cadence of the keep-poll. A local RPC that answers in milliseconds; the cost is one command per
314
- * interval, and it ends the moment the state is `off`. */
315
- const POLL_SPACING_MS = 1e4;
316
330
  function execWithTimeout(exec, line, ms) {
317
331
  return new Promise((resolve, reject) => {
318
332
  const timer = setTimeout(() => {
@@ -341,6 +355,7 @@ window.__ModuleLoader__.load({
341
355
  alive.current = false;
342
356
  }, []);
343
357
  const [busy, setBusy] = (0, react.useState)(false);
358
+ const kick = (0, react.useRef)(void 0);
344
359
  const answered = (0, react.useRef)(false);
345
360
  const replyStatus = (0, react.useRef)("unknown");
346
361
  const stateRef = (0, react.useRef)(initialBarState());
@@ -380,22 +395,43 @@ window.__ModuleLoader__.load({
380
395
  pulled.current = true;
381
396
  let timer;
382
397
  const tick = async () => {
383
- await refresh();
398
+ const spacing = pollSpacingFor({
399
+ status: replyStatus.current,
400
+ autorun: stateRef.current.autorun,
401
+ running: stateRef.current.running,
402
+ hidden: typeof document !== "undefined" && document.hidden === true
403
+ });
404
+ if (spacing !== null) await refresh();
384
405
  if (replyStatus.current === "off") return;
385
406
  timer = setTimeout(() => {
386
407
  tick();
387
- }, POLL_SPACING_MS);
408
+ }, spacing ?? 6e4);
409
+ };
410
+ kick.current = () => {
411
+ if (timer !== void 0) clearTimeout(timer);
412
+ tick();
413
+ };
414
+ const onVisible = () => {
415
+ if (typeof document !== "undefined" && !document.hidden) kick.current?.();
388
416
  };
417
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
389
418
  tick();
390
419
  return () => {
391
420
  if (timer !== void 0) clearTimeout(timer);
421
+ kick.current = void 0;
422
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
392
423
  };
393
424
  }, [refresh]);
425
+ const prevRunning = (0, react.useRef)(props.running ?? true);
394
426
  (0, react.useEffect)(() => {
427
+ const next = props.running ?? true;
428
+ const prev = prevRunning.current;
429
+ prevRunning.current = next;
395
430
  dispatch({
396
431
  t: "running",
397
- running: props.running ?? true
432
+ running: next
398
433
  });
434
+ if (prev && !next) kick.current?.();
399
435
  }, [props.running]);
400
436
  const run = (0, react.useCallback)(() => {
401
437
  if (state.text === null || state.text === "" || exec === void 0) return;