mixdog 0.9.96 → 0.9.97

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 (170) hide show
  1. package/README.md +0 -17
  2. package/package.json +8 -7
  3. package/scripts/build-tui.mjs +6 -6
  4. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  5. package/src/defaults/skills/setup/SKILL.md +1 -1
  6. package/src/lib/keychain-cjs.cjs +1 -1
  7. package/src/lib/mixdog-debug.cjs +2 -3
  8. package/src/rules/lead/01-general.md +1 -2
  9. package/src/rules/lead/lead-tool.md +0 -3
  10. package/src/rules/shared/01-tool.md +11 -10
  11. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  12. package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
  13. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  14. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  15. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
  16. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
  18. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
  19. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
  20. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +40 -25
  21. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  22. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  23. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  24. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  25. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  26. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
  27. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
  28. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
  29. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  30. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
  33. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
  34. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
  35. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
  36. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
  37. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  38. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
  39. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  41. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +67 -21
  44. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
  45. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  46. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
  47. package/src/runtime/channels/lib/config.mjs +33 -33
  48. package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
  49. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  50. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  51. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  52. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  53. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  54. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  55. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  56. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  57. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  58. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  59. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  60. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  61. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  62. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  63. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  64. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  65. package/src/runtime/channels/tool-defs.mjs +1 -1
  66. package/src/runtime/memory/index.mjs +72 -211
  67. package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
  68. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
  69. package/src/runtime/memory/lib/http-router.mjs +1 -6
  70. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  71. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  72. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  73. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  74. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  75. package/src/runtime/memory/lib/memory.mjs +1 -1
  76. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  77. package/src/runtime/search/index.mjs +2 -2
  78. package/src/runtime/shared/abort-race.mjs +70 -0
  79. package/src/runtime/shared/child-spawn-gate.mjs +101 -63
  80. package/src/runtime/shared/config.mjs +11 -24
  81. package/src/runtime/shared/llm/index.mjs +1 -1
  82. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  83. package/src/runtime/shared/resource-admission.mjs +6 -5
  84. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  85. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  86. package/src/runtime/shared/tool-surface.mjs +91 -22
  87. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  88. package/src/runtime/shared/turn-snapshot.mjs +17 -2
  89. package/src/session-runtime/channel-config-api.mjs +5 -5
  90. package/src/session-runtime/config-lifecycle.mjs +27 -27
  91. package/src/session-runtime/lifecycle-api.mjs +22 -3
  92. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  93. package/src/session-runtime/prewarm.mjs +1 -1
  94. package/src/session-runtime/provider-models.mjs +1 -1
  95. package/src/session-runtime/remote-control.mjs +6 -7
  96. package/src/session-runtime/runtime-core.mjs +52 -28
  97. package/src/session-runtime/self-update.mjs +1 -1
  98. package/src/session-runtime/session-lifecycle.mjs +36 -15
  99. package/src/session-runtime/session-title.mjs +1 -1
  100. package/src/session-runtime/session-turn-api.mjs +55 -15
  101. package/src/session-runtime/settings-api.mjs +7 -7
  102. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  103. package/src/standalone/channel-admin.mjs +42 -53
  104. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
  105. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +45 -46
  106. package/src/standalone/channel-worker.mjs +124 -647
  107. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +187 -132
  108. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  109. package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
  110. package/src/standalone/session-protocol.mjs +8 -4
  111. package/src/standalone/session-runtime-pool.mjs +393 -0
  112. package/src/standalone/session-runtime-record.mjs +20 -0
  113. package/src/standalone/session-runtime-worker.mjs +203 -0
  114. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +170 -157
  115. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +139 -61
  116. package/src/standalone/session-wire.mjs +55 -0
  117. package/src/tui/App.jsx +4 -4
  118. package/src/tui/app/app-format.mjs +1 -1
  119. package/src/tui/app/channel-pickers.mjs +41 -41
  120. package/src/tui/app/input-parsers.mjs +2 -2
  121. package/src/tui/app/onboarding-steps.mjs +2 -2
  122. package/src/tui/app/project-picker.mjs +8 -8
  123. package/src/tui/app/prompt-submit.mjs +8 -8
  124. package/src/tui/app/settings-picker.mjs +25 -25
  125. package/src/tui/app/use-prompt-queue-history.mjs +1 -1
  126. package/src/tui/app/use-transcript-scroll.mjs +1 -1
  127. package/src/tui/components/ToolExecution.jsx +1 -1
  128. package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
  129. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  130. package/src/tui/dist/index.mjs +154 -127
  131. package/src/tui/hooks/useSession.mjs +16 -0
  132. package/src/tui/index.jsx +11 -11
  133. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  134. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  135. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  136. package/src/tui/{engine → session}/context-state.mjs +3 -3
  137. package/src/tui/{engine → session}/labels.mjs +2 -2
  138. package/src/tui/{engine → session}/live-share.mjs +7 -7
  139. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  140. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  141. package/src/tui/session/oauth-flows.mjs +140 -0
  142. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  143. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  144. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  145. package/src/tui/{engine → session}/session-api-ext.mjs +30 -17
  146. package/src/tui/{engine → session}/session-api.mjs +7 -60
  147. package/src/tui/{engine → session}/session-flow.mjs +1 -1
  148. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  149. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  150. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  151. package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
  152. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  153. package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
  154. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  155. package/src/tui/{engine → session}/turn.mjs +29 -107
  156. package/src/tui/{engine-local-session.mjs → session-local.mjs} +59 -75
  157. package/src/tui/session.mjs +22 -0
  158. package/src/ui/statusline-segments.mjs +1 -1
  159. package/src/ui/statusline.mjs +1 -1
  160. package/vendor/ink/build/components/App.js +10 -1
  161. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  162. package/src/standalone/engine-daemon-protocol.mjs +0 -32
  163. package/src/tui/engine/turn-watchdog.mjs +0 -92
  164. package/src/tui/engine.mjs +0 -22
  165. package/src/tui/hooks/useEngine.mjs +0 -16
  166. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  167. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  168. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  169. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  170. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -0,0 +1,16 @@
1
+ /**
2
+ * src/tui/hooks/useSession.mjs — subscribe React to the session store.
3
+ *
4
+ * The store (session-local.mjs) lives outside React and emits immutable snapshots;
5
+ * useSyncExternalStore re-renders the tree whenever a snapshot changes. This
6
+ * keeps the agentLoop fully decoupled from React's lifecycle.
7
+ */
8
+ import { useSyncExternalStore } from 'react';
9
+
10
+ /**
11
+ * @param {object} store returned by createSessionRuntime()
12
+ * @returns the current session runtime state snapshot
13
+ */
14
+ export function useSession(store) {
15
+ return useSyncExternalStore(store.subscribe, store.getState, store.getState);
16
+ }
package/src/tui/index.jsx CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * src/tui/index.jsx — entry that mounts the React/ink TUI.
3
3
  *
4
- * Creates the engine session (runs OUR agentLoop outside React) and ink-renders
4
+ * Creates the session runtime session (runs OUR agentLoop outside React) and ink-renders
5
5
  * <App store={...}/>. Resolves when the app exits (/exit or /quit).
6
6
  */
7
7
  import React from 'react';
@@ -13,8 +13,8 @@ import { performance } from 'node:perf_hooks';
13
13
  import { format } from 'node:util';
14
14
  import { App } from './App.jsx';
15
15
  import { cancelPendingMouseTrackingRestores } from './app/use-mouse-input.mjs';
16
- import { createEngineSession } from './engine.mjs';
17
- import { scheduleRenderFrameAck, TUI_RENDER_FPS } from './engine/render-timing.mjs';
16
+ import { createSessionRuntime } from './session.mjs';
17
+ import { scheduleRenderFrameAck, TUI_RENDER_FPS } from './session/render-timing.mjs';
18
18
  import { installProcessSignalCleanup } from '../runtime/shared/process-shutdown.mjs';
19
19
  import { finishProcessLifecycle } from '../runtime/shared/process-lifecycle.mjs';
20
20
  import { rgbSgr } from '../ui/ansi.mjs';
@@ -472,7 +472,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
472
472
 
473
473
  // Enter the alternate screen buffer before session/runtime boot so no stale
474
474
  // shell rows are visible while the statusline/TUI data warms up. The first
475
- // real Ink frame may still arrive after createEngineSession(), but the user
475
+ // real Ink frame may still arrive after createSessionRuntime(), but the user
476
476
  // sees a clean fullscreen surface immediately instead of the previous terminal
477
477
  // contents bleeding through the bottom status area.
478
478
  process.stdout.write(`${TERMINAL_MODE_RESET_HIDDEN_CURSOR}\x1b[?1049h${TERMINAL_MODE_RESET_HIDDEN_CURSOR}\x1b[2J\x1b[H`);
@@ -488,10 +488,10 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
488
488
 
489
489
  process.on('exit', restoreTerminal);
490
490
 
491
- // Engine startup is independent from palette selection. Start it now and
491
+ // Session runtime startup is independent from palette selection. Start it now and
492
492
  // overlap its runtime import/config work with the theme read, while still
493
493
  // awaiting the theme before the first React frame.
494
- const storeOutcomePromise = createEngineSession({ provider, model, toolMode, remote })
494
+ const storeOutcomePromise = createSessionRuntime({ provider, model, toolMode, remote })
495
495
  .then((store) => ({ store, error: null }), (error) => ({ store: null, error }));
496
496
 
497
497
  // Apply the persisted UI theme (ui.theme in mixdog-config.json) before the
@@ -501,7 +501,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
501
501
  try { await loadThemeSettingFromConfig(); } catch { /* default theme stays */ }
502
502
  emitTerminalBackground(theme.background);
503
503
 
504
- // Static splash + loading spinner while createEngineSession() warms up
504
+ // Static splash + loading spinner while createSessionRuntime() warms up
505
505
  // (config, providers, sessions). Without this the user stares at an empty
506
506
  // alt screen for the whole boot, then the UI "pops in" — the splash makes
507
507
  // the first ink frame an in-place repaint instead.
@@ -518,13 +518,13 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
518
518
  try {
519
519
  await store.touchProjectSelected(store.getState?.()?.cwd || process.cwd());
520
520
  } catch { /* best-effort recency */ }
521
- // The palette lives in THIS process: on a daemon-backed store the engine's
521
+ // The palette lives in THIS process: on a daemon-backed store the session runtime's
522
522
  // theme methods would run inside the daemon, so the picker saw a promise
523
523
  // instead of the list and a switch repainted nothing here. Bind the three
524
524
  // theme methods locally (same pattern as the ink selection helpers below)
525
- // and mirror the switch to the engine with persist:false, purely so its
525
+ // and mirror the switch to the session runtime with persist:false, purely so its
526
526
  // themeEpoch bump re-renders this tree.
527
- if (store.isRemoteEngine === true) {
527
+ if (store.isRemoteSession === true) {
528
528
  const remoteSetTheme = store.setTheme;
529
529
  store.listThemes = () => listThemes();
530
530
  store.getTheme = () => getThemeSetting();
@@ -547,7 +547,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
547
547
  process.stderr.write(`mixdog: ${error?.message || error}\n`);
548
548
  return 1;
549
549
  }
550
- // The engine store is a daemon-backed Proxy: every method it does not
550
+ // The session runtime store is a daemon-backed Proxy: every method it does not
551
551
  // implement locally comes back as an ASYNC remote call. A synchronous
552
552
  // getOnboardingStatus() probe inside App therefore sees a Promise, never
553
553
  // `.completed`, and re-opens the finished wizard on every launch. Resolve
@@ -1,8 +1,8 @@
1
1
  /**
2
- * src/tui/engine/agent-envelope.mjs - parse agent/background-task/shell
2
+ * src/tui/session/agent-envelope.mjs - parse agent/background-task/shell
3
3
  * notification envelopes into synthetic tool-item shapes, and derive
4
- * status/result text. Extracted from engine.mjs; parseBackgroundTaskEnvelope
5
- * remains part of engine.mjs's public surface.
4
+ * status/result text. Extracted from session-local.mjs; parseBackgroundTaskEnvelope
5
+ * remains part of session-local.mjs's public surface.
6
6
  */
7
7
  import { isBackgroundErrorOnlyBody } from '../../runtime/shared/err-text.mjs';
8
8
 
@@ -1,6 +1,6 @@
1
1
  /**
2
- * src/tui/engine/agent-job-feed.mjs — agent-job / runtime-notification plumbing
3
- * extracted from createEngineSession (engine.mjs) as a dependency-injection
2
+ * src/tui/session/agent-job-feed.mjs — agent-job / runtime-notification plumbing
3
+ * extracted from the session runtime as a dependency-injection
4
4
  * factory.
5
5
  *
6
6
  * Owns: the agent-job card result patch (updateAgentJobCard), the
@@ -11,7 +11,7 @@
11
11
  * These handlers mutate live session state and drive the queue, so
12
12
  * state/set/enqueue/drain/pushUserOrSyntheticItem/patchItem/etc are threaded via
13
13
  * the factory argument (getters/callbacks) — never stale snapshots. Every body
14
- * is the original engine.mjs logic verbatim.
14
+ * is the original session-local.mjs logic verbatim.
15
15
  */
16
16
  import {
17
17
  parseAgentJob,
@@ -314,7 +314,7 @@ export function createAgentJobFeed({
314
314
  if (firstDelivery && !successfulPreview && !bodyAlreadyDisplayed) {
315
315
  if (cardKey) rememberDisplayedExecutionNotificationKey(cardKey, terminal, executionId);
316
316
  if (executionId) rememberDisplayedExecutionResponseState(executionId, hasBody ? 'body' : 'preview', terminal);
317
- // Execution completions are inbound agent responses. The engine keeps
317
+ // Execution completions are inbound agent responses. The session runtime keeps
318
318
  // their aggregation tail-safe (only an immediately-adjacent inbound
319
319
  // response of the same preview/body phase can merge); the fallback
320
320
  // preserves the standalone path for minimal/test harnesses.
@@ -1,7 +1,7 @@
1
1
  /**
2
- * src/tui/engine/boot-profile.mjs - opt-in boot timing trace.
2
+ * src/tui/session/boot-profile.mjs - opt-in boot timing trace.
3
3
  *
4
- * Extracted from engine.mjs. Emits `[mixdog-boot]` stderr lines when
4
+ * Extracted from session-local.mjs. Emits `[mixdog-boot]` stderr lines when
5
5
  * MIXDOG_BOOT_PROFILE is truthy; a no-op otherwise.
6
6
  */
7
7
  import { performance } from 'node:perf_hooks';
@@ -1,7 +1,7 @@
1
1
  /**
2
- * src/tui/engine/context-state.mjs - route/context/agent-status derivations.
2
+ * src/tui/session/context-state.mjs - route/context/agent-status derivations.
3
3
  *
4
- * Extracted from engine.mjs unchanged. These read the live runtime + store
4
+ * Extracted from session-local.mjs unchanged. These read the live runtime + store
5
5
  * snapshot. The two sync helpers stage immutable draft patches through
6
6
  * updateState; callers still follow with set(...) to schedule publication.
7
7
  */
@@ -24,7 +24,7 @@ export function createContextState({ runtime, getState, updateState, getPendingS
24
24
  };
25
25
  const baseRouteState = () => ({
26
26
  sessionId: runtime.id,
27
- // Fork-on-resume marker: when the engine opened a live session as a fork
27
+ // Fork-on-resume marker: when the session runtime opened a live session as a fork
28
28
  // (fresh id, copied transcript), hosts validating "resume returned the
29
29
  // requested session" must accept the fork by its origin id.
30
30
  sessionForkedFrom: runtime.session?.forkedFrom || null,
@@ -1,6 +1,6 @@
1
1
  /**
2
- * src/tui/engine/labels.mjs - spinner verbs, elapsed formatting, and compact
3
- * event label/detail helpers. Extracted from engine.mjs.
2
+ * src/tui/session/labels.mjs - spinner verbs, elapsed formatting, and compact
3
+ * event label/detail helpers. Extracted from session-local.mjs.
4
4
  */
5
5
  import { SPINNER_VERBS } from '../spinner-verbs.mjs';
6
6
 
@@ -1,12 +1,12 @@
1
1
  /**
2
- * src/tui/engine/live-share.mjs — real-time cross-surface session mirroring.
2
+ * src/tui/session/live-share.mjs — real-time cross-surface session mirroring.
3
3
  *
4
4
  * OWNER leg: hosts a local named-pipe (win32) / unix-socket server for the
5
5
  * session it owns and pushes frame-batched transcript deltas (items /
6
6
  * streaming tail / spinner) to attached viewers. Push-only: zero polling,
7
7
  * idle cost is one open listener.
8
8
  * VIEWER leg: connects to the owner's pipe and mirrors those deltas into the
9
- * local engine store, so streaming output appears live on every co-open
9
+ * local session runtime store, so streaming output appears live on every co-open
10
10
  * surface (desktop <-> terminal). Viewer submits travel over the same pipe
11
11
  * into the owner's normal submit queue (concurrent input from both sides is
12
12
  * serialized there); the durable pending spool remains the fallback when the
@@ -25,7 +25,7 @@ const MAX_BUFFER_BYTES = 8 * 1024 * 1024;
25
25
  const SYNC_REQUEST_MIN_INTERVAL_MS = 500;
26
26
  // Session entry is latency-sensitive: the owner pipe can be a few event-loop
27
27
  // turns behind the viewer resume. Retry locally instead of waiting for the
28
- // coarse 3s engine safety tick.
28
+ // coarse 3s session runtime safety tick.
29
29
  const LIVE_CONNECT_RETRY_MIN_MS = 10;
30
30
  const LIVE_CONNECT_RETRY_MAX_MS = 160;
31
31
 
@@ -640,7 +640,7 @@ export function createLiveShare({
640
640
  };
641
641
 
642
642
  // Reconciles both legs against the current session role. Failed pipe opens
643
- // continue through the local retry timers; the engine's 3s call is only a
643
+ // continue through the local retry timers; the session runtime's 3s call is only a
644
644
  // safety net, never the normal connection cadence.
645
645
  const ensureShare = () => {
646
646
  if (disposed) return;
@@ -711,10 +711,10 @@ export function createLiveShare({
711
711
  }
712
712
 
713
713
  /**
714
- * ONE viewer-submit intake for EVERY engine entry point. The TUI store calls
715
- * submit(); the daemon awaits submitAsync() (engine-daemon-service prefers it).
714
+ * ONE viewer-submit intake for EVERY session runtime entry point. The TUI store calls
715
+ * submit(); the daemon awaits submitAsync() (session-service prefers it).
716
716
  * Wrapping only one of them let the other path enqueue the prompt on the VIEWER
717
- * engine, so that surface rendered its own local user row beside the owner's
717
+ * session runtime, so that surface rendered its own local user row beside the owner's
718
718
  * mirrored one — the same message twice on screen
719
719
  * (user: 데스크탑에서만 입력이 두 개로 들어간다).
720
720
  *
@@ -1,6 +1,6 @@
1
1
  /**
2
- * src/tui/engine/notice-text.mjs - polish user-facing failure/notice text.
3
- * Extracted from engine.mjs.
2
+ * src/tui/session/notice-text.mjs - polish user-facing failure/notice text.
3
+ * Extracted from session-local.mjs.
4
4
  */
5
5
  const FAILED_NOTICE_ACTIONS = new Map([
6
6
  ['api key save', 'save API key'],
@@ -1,9 +1,9 @@
1
1
  /**
2
- * src/tui/engine/notification-plan.mjs - pure runtime.onNotification delivery plan.
2
+ * src/tui/session/notification-plan.mjs - pure runtime.onNotification delivery plan.
3
3
  *
4
- * Extracted from engine.mjs (no behavior change). These are stateless helpers
4
+ * Extracted from session-local.mjs (no behavior change). These are stateless helpers
5
5
  * that decide how a runtime notification envelope should be delivered to the
6
- * TUI store (dedupe key + display/model routing). No closures over engine state.
6
+ * TUI store (dedupe key + display/model routing). No closures over session runtime state.
7
7
  */
8
8
  import {
9
9
  isStatusOnlyAgentCompletionNotification,
@@ -0,0 +1,140 @@
1
+ // Daemon-owned provider OAuth flows. The provider runtime returns live
2
+ // promises/functions, so the session surface must retain them locally and
3
+ // expose only serializable status records to desktop clients.
4
+ const DEFAULT_FLOW_TTL_MS = 10 * 60 * 1_000;
5
+
6
+ export function oauthFlowStatus(flow) {
7
+ return {
8
+ flowId: flow.id,
9
+ provider: flow.provider,
10
+ url: flow.url,
11
+ manualUrl: flow.manualUrl,
12
+ state: flow.state,
13
+ completed: flow.state === 'complete',
14
+ error: flow.error,
15
+ manualCodeSupported: typeof flow.completeCode === 'function',
16
+ };
17
+ }
18
+
19
+ function requiredFlowId(value) {
20
+ const id = String(value || '').trim();
21
+ if (!/^oauth_[a-z0-9_]+$/i.test(id)) throw new TypeError('OAuth flow id is invalid.');
22
+ return id;
23
+ }
24
+
25
+ function cancelFlow(flow, reason) {
26
+ try {
27
+ const result = flow.cancel?.();
28
+ if (result && typeof result.then === 'function') {
29
+ void Promise.resolve(result).catch((error) => {
30
+ console.warn(`OAuth flow could not be ${reason}:`, error);
31
+ });
32
+ }
33
+ } catch (error) {
34
+ console.warn(`OAuth flow could not be ${reason}:`, error);
35
+ }
36
+ }
37
+
38
+ export function createSessionOAuthFlowRegistry({ ttlMs = DEFAULT_FLOW_TTL_MS } = {}) {
39
+ const flows = new Map();
40
+ let sequence = 0;
41
+
42
+ function requireFlow(id) {
43
+ const key = requiredFlowId(id);
44
+ const flow = flows.get(key);
45
+ if (!flow) throw new Error('OAuth login flow is no longer available.');
46
+ return flow;
47
+ }
48
+
49
+ function register(value) {
50
+ const started = value && typeof value === 'object' ? value : null;
51
+ if (!started) throw new Error('OAuth provider did not return a login flow.');
52
+ const provider = String(started.provider || '').trim();
53
+ if (!provider) throw new Error('OAuth provider login is missing its provider id.');
54
+ const id = `oauth_${Date.now().toString(36)}_${(++sequence).toString(36)}`;
55
+ const flow = {
56
+ id,
57
+ provider,
58
+ url: typeof started.url === 'string' ? started.url : null,
59
+ manualUrl: typeof started.manualUrl === 'string' ? started.manualUrl : null,
60
+ state: 'pending',
61
+ result: null,
62
+ error: null,
63
+ completeCode: typeof started.completeCode === 'function' ? started.completeCode : undefined,
64
+ cancel: typeof started.cancel === 'function' ? started.cancel : undefined,
65
+ timeout: null,
66
+ };
67
+ flow.timeout = setTimeout(() => {
68
+ const current = flows.get(id);
69
+ if (!current) return;
70
+ cancelFlow(current, 'expired');
71
+ flows.delete(id);
72
+ }, Math.max(1, Number(ttlMs) || DEFAULT_FLOW_TTL_MS));
73
+ flow.timeout.unref?.();
74
+ flows.set(id, flow);
75
+ if (started.waitForCallback && typeof started.waitForCallback.then === 'function') {
76
+ void Promise.resolve(started.waitForCallback).then((result) => {
77
+ const current = flows.get(id);
78
+ if (!current || current.state !== 'pending' || !result) return;
79
+ current.state = 'complete';
80
+ current.result = true;
81
+ }).catch((error) => {
82
+ const current = flows.get(id);
83
+ if (!current || current.state !== 'pending') return;
84
+ current.state = 'failed';
85
+ current.error = error instanceof Error ? error.message : String(error);
86
+ });
87
+ }
88
+ return oauthFlowStatus(flow);
89
+ }
90
+
91
+ function status(id) {
92
+ return oauthFlowStatus(requireFlow(id));
93
+ }
94
+
95
+ async function complete(id, rawCode) {
96
+ const flow = requireFlow(id);
97
+ if (typeof flow.completeCode !== 'function') {
98
+ throw new Error('This OAuth provider does not accept a manual code.');
99
+ }
100
+ const code = String(rawCode || '').trim();
101
+ if (!code || code.length > 16_384) throw new TypeError('OAuth code is invalid.');
102
+ try {
103
+ const completed = await flow.completeCode(code);
104
+ flow.result = Boolean(completed);
105
+ flow.state = completed ? 'complete' : 'failed';
106
+ flow.error = completed ? null : 'OAuth code did not complete the login.';
107
+ } catch (error) {
108
+ flow.state = 'failed';
109
+ flow.error = error instanceof Error ? error.message : String(error);
110
+ throw error;
111
+ }
112
+ return oauthFlowStatus(flow);
113
+ }
114
+
115
+ async function cancel(id) {
116
+ const flow = requireFlow(id);
117
+ let failure = null;
118
+ try {
119
+ await flow.cancel?.();
120
+ } catch (error) {
121
+ failure = error;
122
+ } finally {
123
+ flow.state = 'cancelled';
124
+ clearTimeout(flow.timeout);
125
+ flows.delete(flow.id);
126
+ }
127
+ if (failure) throw failure;
128
+ return oauthFlowStatus(flow);
129
+ }
130
+
131
+ function cancelAll() {
132
+ for (const flow of flows.values()) {
133
+ clearTimeout(flow.timeout);
134
+ cancelFlow(flow, 'cancelled');
135
+ }
136
+ flows.clear();
137
+ }
138
+
139
+ return Object.freeze({ register, status, complete, cancel, cancelAll });
140
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
- * src/tui/engine/prompt-history.mjs - pure prompt-history derivation.
2
+ * src/tui/session/prompt-history.mjs - pure prompt-history derivation.
3
3
  *
4
- * Extracted from engine.mjs unchanged: the newest-first, deduped user-prompt
5
- * history the engine publishes on state.promptHistoryList. Pure (input items
4
+ * Extracted from session-local.mjs unchanged: the newest-first, deduped user-prompt
5
+ * history the session runtime publishes on state.promptHistoryList. Pure (input items
6
6
  * → array); callers decide when/whether to publish so the store's
7
7
  * immutable-emit contract is preserved.
8
8
  */
@@ -1,8 +1,8 @@
1
1
  /**
2
- * src/tui/engine/queue-helpers.mjs - pure helpers for the prompt/notification
2
+ * src/tui/session/queue-helpers.mjs - pure helpers for the prompt/notification
3
3
  * queue: priority ordering, entry visibility/editability, prompt content
4
4
  * flattening, session activity timestamps, and batch merging. Extracted from
5
- * engine.mjs.
5
+ * session-local.mjs.
6
6
  */
7
7
  import {
8
8
  agentJobResultText,
@@ -1,7 +1,7 @@
1
1
  /**
2
- * src/tui/engine/render-timing.mjs - render-throttle timing helper.
2
+ * src/tui/session/render-timing.mjs - render-throttle timing helper.
3
3
  *
4
- * Extracted from engine.mjs (no behavior change originally).
4
+ * Extracted from session-local.mjs (no behavior change originally).
5
5
  *
6
6
  * Ink renders through a maxFps throttle (120fps in index.jsx, ≈8.3ms). A plain
7
7
  * setImmediate only yields to the event loop; if Ink already painted within the
@@ -1,5 +1,5 @@
1
1
  /**
2
- * src/tui/engine/session-api-ext.mjs - part of the public engine session object.
2
+ * src/tui/session/session-api-ext.mjs - part of the public session runtime session object.
3
3
  */
4
4
  import { listThemes, getThemeSetting, setThemeSetting } from '../theme.mjs';
5
5
  import { resetAllStreamingMarkdownStablePrefixes } from '../markdown/streaming-markdown.mjs';
@@ -7,7 +7,8 @@ import { toolResultText } from './tool-result-text.mjs';
7
7
  import { parseSyntheticAgentMessage } from './agent-envelope.mjs';
8
8
  import { flushTuiSteeringPersist } from './tui-steering-persist.mjs';
9
9
  import { getVoiceStatus, toggleVoice } from '../lib/voice-setup.mjs';
10
- import { aggregateToolCategoryEntry, aggregateDoneCategories, classifyToolCategory, formatAggregateDetail, summarizeToolResult } from '../../runtime/shared/tool-surface.mjs';
10
+ import { createSessionOAuthFlowRegistry } from './oauth-flows.mjs';
11
+ import { aggregateToolCategoryEntries, aggregateDoneCategories, classifyToolCategory, formatAggregateDetail, summarizeToolResult } from '../../runtime/shared/tool-surface.mjs';
11
12
  import { aggregateBucketForCategory, aggregateRawResult, failureDetailText, shellCommandExitCode } from './tool-result-status.mjs';
12
13
  import { isInternalTranscriptDisplayText } from '../../runtime/shared/tool-execution-contract.mjs';
13
14
  import { toolResultTerminalStatus } from '../../runtime/shared/tool-status.mjs';
@@ -121,10 +122,11 @@ function buildRestoredAggregateItem(members) {
121
122
  const categoryOrder = [];
122
123
  const calls = [];
123
124
  for (const { item, category } of members) {
124
- const entry = aggregateToolCategoryEntry(item.name, item.args, category);
125
- if (!categories.has(entry.key)) categoryOrder.push(entry.key);
126
- const prev = categories.get(entry.key);
127
- categories.set(entry.key, { ...entry, count: Number(prev?.count || 0) + Number(entry.count || 1) });
125
+ for (const entry of aggregateToolCategoryEntries(item.name, item.args, category)) {
126
+ if (!categories.has(entry.key)) categoryOrder.push(entry.key);
127
+ const prev = categories.get(entry.key);
128
+ categories.set(entry.key, { ...entry, count: Number(prev?.count || 0) + Number(entry.count || 1) });
129
+ }
128
130
  const resultText = String(item.result ?? '');
129
131
  // Mirror live outcome semantics: a plain non-zero shell exit is the
130
132
  // neutral 'Exit N' state, not a red failure; only real call errors count.
@@ -186,7 +188,7 @@ function buildRestoredAggregateItem(members) {
186
188
  }
187
189
 
188
190
  // Restored transcripts must mirror the live turn's category merging: the live
189
- // engine (turn.mjs) collapses consecutive same-bucket tool calls into one
191
+ // session runtime (turn.mjs) collapses consecutive same-bucket tool calls into one
190
192
  // aggregate card, but resume rebuilt one card per call, so a reopened session
191
193
  // un-merged every run (user bug). Walk the restored items and merge adjacent
192
194
  // tool cards whose aggregateBucketForCategory matches; any non-tool item
@@ -381,10 +383,11 @@ export function sessionContextSnapshotProjection(session, contextStatus) {
381
383
  };
382
384
  }
383
385
 
384
- export function createEngineApiB(bag) {
386
+ export function createSessionApiB(bag) {
385
387
  const {
386
388
  runtime, nextId, flags, lifecycle, listeners, getState, set, flushEmitImmediate, disposeEmit, replaceItems, pushNotice, removeNotice, setProgressHint, clearToastTimers, disposeTranscriptSpill, routeState, syncContextStats, finishToolApproval, denyAllToolApprovals, restoreLeadSteeringFromDisk, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, commitTuiSessionReset, resetStatsAndSyncContext,
387
389
  } = bag;
390
+ const oauthFlows = createSessionOAuthFlowRegistry();
388
391
  return {
389
392
  resolveToolApproval: (id, decision = {}) => {
390
393
  const approved = decision === true || decision?.approved === true;
@@ -519,7 +522,7 @@ export function createEngineApiB(bag) {
519
522
  // `/remote` slash command — repeated /remote just re-claims (idempotent).
520
523
  claimRemote: async () => {
521
524
  // Desired state paints before daemon cold boot. The final read in
522
- // `finally` reconciles a skipped/failed claim to the backend truth.
525
+ // `finally` reconciles a skipped/failed claim to the session truth.
523
526
  set({
524
527
  remoteEnabled: true,
525
528
  remoteSessionId: runtime.getRemoteSessionId?.() || getState().sessionId || null,
@@ -633,7 +636,7 @@ export function createEngineApiB(bag) {
633
636
  return await runtime.getUsageDashboard?.(options);
634
637
  },
635
638
  consumeCodexRateLimitResetCredit: async (options = {}) => {
636
- // Desktop capability parity: without this delegation the engine surface
639
+ // Desktop capability parity: without this delegation the session runtime surface
637
640
  // rejects the sidebar's reset-credit invoke as unsupported even though
638
641
  // the session runtime implements it.
639
642
  if (typeof runtime.consumeCodexRateLimitResetCredit !== 'function') {
@@ -641,8 +644,14 @@ export function createEngineApiB(bag) {
641
644
  }
642
645
  return await runtime.consumeCodexRateLimitResetCredit(options);
643
646
  },
644
- getTurnReviewDiff: async () => {
645
- return (await runtime.getTurnReviewDiff?.()) ?? { supported: false, files: [], patch: '' };
647
+ getTurnReviewDiff: async (options = {}) => {
648
+ return (await runtime.getTurnReviewDiff?.(options)) ?? { supported: false, files: [], patch: '' };
649
+ },
650
+ revertTurnReviewFile: async (file) => {
651
+ if (typeof runtime.revertTurnReviewFile !== 'function') {
652
+ throw new Error('Turn review revert is unavailable');
653
+ }
654
+ return await runtime.revertTurnReviewFile(file);
646
655
  },
647
656
  getOnboardingStatus: () => {
648
657
  return runtime.getOnboardingStatus?.() || { completed: true, workflowRoutes: {} };
@@ -679,13 +688,16 @@ export function createEngineApiB(bag) {
679
688
  if (getState().commandBusy) throw new Error('command busy');
680
689
  set({ commandBusy: true });
681
690
  try {
682
- const result = await runtime.beginOAuthProviderLogin(provider);
691
+ const result = oauthFlows.register(await runtime.beginOAuthProviderLogin(provider));
683
692
  pushNotice(`provider oauth started: ${result.provider}`, 'info');
684
693
  return result;
685
694
  } finally {
686
695
  set({ commandBusy: false });
687
696
  }
688
697
  },
698
+ getOAuthProviderLoginStatus: (flowId) => oauthFlows.status(flowId),
699
+ completeOAuthProviderLogin: async (flowId, code) => oauthFlows.complete(flowId, code),
700
+ cancelOAuthProviderLogin: async (flowId) => oauthFlows.cancel(flowId),
689
701
  saveProviderApiKey: (provider, secret) => {
690
702
  const result = runtime.saveProviderApiKey(provider, secret);
691
703
  pushNotice(`provider api key saved: ${result.provider}`, 'info');
@@ -738,7 +750,7 @@ export function createEngineApiB(bag) {
738
750
  return runtime.getChannelSetup();
739
751
  },
740
752
  getChannelWorkerStatus: () => runtime.getChannelWorkerStatus?.(),
741
- setBackend: (name) => runtime.setBackend?.(name),
753
+ setChannelProvider: (name) => runtime.setChannelProvider?.(name),
742
754
  saveDiscordToken: (token) => {
743
755
  const result = runtime.saveDiscordToken(token);
744
756
  pushNotice('discord token saved', 'info');
@@ -880,7 +892,7 @@ export function createEngineApiB(bag) {
880
892
  renameSessionTitle: (id, title) => {
881
893
  return runtime.renameSessionTitle?.(id, title) ?? false;
882
894
  },
883
- // Desktop sidebar watcher hook: the backend watches this directory so
895
+ // Desktop sidebar watcher hook: the daemon watches this directory so
884
896
  // heartbeat sidecar create/delete pushes instant working/dot updates.
885
897
  // Without it the watcher silently no-ops and the sidebar falls back to
886
898
  // the 60s safety poll (user: spinner kept spinning after the turn ended).
@@ -1002,7 +1014,7 @@ export function createEngineApiB(bag) {
1002
1014
  }
1003
1015
  },
1004
1016
  // Desktop split panes: read-only transcript + route peek of an idle
1005
- // session — no resume, no ownership claim, no engine state mutation.
1017
+ // session — no resume, no ownership claim, no session runtime state mutation.
1006
1018
  // Panes render every visible session as foreground from this frame.
1007
1019
  peekSessionTranscript: (id, options = {}) => {
1008
1020
  const session = runtime.peekSession?.(id);
@@ -1028,7 +1040,7 @@ export function createEngineApiB(bag) {
1028
1040
  },
1029
1041
  resume: async (id, options = {}) => {
1030
1042
  if (getState().commandBusy) return false;
1031
- // quiet: viewer-follow refreshes (engine share tick) re-resume on every
1043
+ // quiet: viewer-follow refreshes (session runtime share tick) re-resume on every
1032
1044
  // owner turn — they must not flash the "Resuming conversation" status.
1033
1045
  set({
1034
1046
  commandBusy: true,
@@ -1095,6 +1107,7 @@ export function createEngineApiB(bag) {
1095
1107
  try { lifecycle.unsubscribeRemoteState?.(); } catch {}
1096
1108
  lifecycle.unsubscribeRemoteState = null;
1097
1109
  denyAllToolApprovals('runtime closing');
1110
+ oauthFlows.cancelAll();
1098
1111
  await flushTuiSteeringPersist();
1099
1112
  await runtime.close(reason, options);
1100
1113
  listeners.clear();