mixdog 1.0.4 → 1.0.6

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 (158) hide show
  1. package/README.md +117 -434
  2. package/package.json +18 -17
  3. package/scripts/gate-local.mjs +93 -0
  4. package/scripts/lib/gate-scheduler.mjs +24 -0
  5. package/scripts/lib/run-node-tests.mjs +5 -1
  6. package/scripts/release-paths.mjs +22 -1
  7. package/scripts/test-direct.mjs +2 -1
  8. package/scripts/test.mjs +3 -1
  9. package/src/defaults/agents.json +0 -12
  10. package/src/defaults/mixdog-config.template.json +1 -2
  11. package/src/defaults/skills/docx/SKILL.md +7 -4
  12. package/src/defaults/skills/docx/references/html.md +46 -0
  13. package/src/defaults/skills/docx/references/native-authoring.md +18 -4
  14. package/src/defaults/skills/pdf/SKILL.md +10 -2
  15. package/src/defaults/skills/pdf/references/html.md +64 -0
  16. package/src/defaults/skills/pptx/references/html.md +1 -1
  17. package/src/defaults/skills/xlsx/SKILL.md +7 -3
  18. package/src/defaults/skills/xlsx/references/html.md +40 -0
  19. package/src/defaults/skills/xlsx/references/report-design.md +21 -4
  20. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -3
  22. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  23. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +6 -5
  24. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +6 -3
  25. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +6 -2
  26. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +50 -9
  27. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +28 -1
  28. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +4 -1
  29. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +34 -8
  30. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +10 -2
  31. package/src/runtime/agent/orchestrator/session/transcript-restore/agent-envelope.mjs +1 -1
  32. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
  33. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +3 -1
  34. package/src/runtime/attachments/pdf-extract.mjs +7 -3
  35. package/src/runtime/computer-bridge/client.mjs +1 -1
  36. package/src/runtime/memory/index.mjs +4 -15
  37. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  38. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +0 -2
  39. package/src/runtime/memory/lib/cycle-scheduler/backlog-probe.mjs +3 -11
  40. package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +1 -3
  41. package/src/runtime/memory/lib/cycle-scheduler/scheduled-enqueue.mjs +1 -11
  42. package/src/runtime/memory/lib/cycle-scheduler/tick-loop.mjs +0 -2
  43. package/src/runtime/memory/lib/cycle-scheduler.mjs +4 -13
  44. package/src/runtime/memory/lib/cycle-signatures.mjs +0 -6
  45. package/src/runtime/memory/lib/cycle1/cycle1-rows.mjs +1 -1
  46. package/src/runtime/memory/lib/cycle1/cycle1-window.mjs +2 -2
  47. package/src/runtime/memory/lib/embedding-reindex.mjs +1 -1
  48. package/src/runtime/memory/lib/http-router/lifecycle-routes.mjs +0 -1
  49. package/src/runtime/memory/lib/memory-action-handlers/cycle-actions.mjs +7 -52
  50. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +6 -22
  51. package/src/runtime/memory/lib/memory-action-handlers/cycle-rebuild-action.mjs +10 -17
  52. package/src/runtime/memory/lib/memory-action-handlers/maintenance-actions.mjs +1 -3
  53. package/src/runtime/memory/lib/memory-action-handlers/manage-actions.mjs +2 -4
  54. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -4
  55. package/src/runtime/memory/lib/memory-cycle-requests.mjs +1 -1
  56. package/src/runtime/memory/lib/{memory-cycle2-shared.mjs → memory-cycle-shared.mjs} +14 -16
  57. package/src/runtime/memory/lib/memory-cycle.mjs +3 -2
  58. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  59. package/src/runtime/memory/lib/memory-embed.mjs +14 -1
  60. package/src/runtime/memory/lib/memory-ops-policy.mjs +8 -11
  61. package/src/runtime/memory/lib/memory-schema/entries.mjs +0 -1
  62. package/src/runtime/memory/lib/memory.mjs +2 -6
  63. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  64. package/src/runtime/memory/lib/query-handlers.mjs +1 -1
  65. package/src/runtime/memory/lib/query-hybrid-search.mjs +1 -1
  66. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +0 -6
  67. package/src/runtime/memory/lib/recall-format.mjs +1 -1
  68. package/src/runtime/office/authoring/docx-html-build.mjs +375 -0
  69. package/src/runtime/office/authoring/docx-html-measure.mjs +561 -0
  70. package/src/runtime/office/authoring/docx-html-runner.mjs +102 -0
  71. package/src/runtime/office/authoring/html-browser.mjs +61 -0
  72. package/src/runtime/office/authoring/html-document-author.mjs +181 -0
  73. package/src/runtime/office/authoring/html-source-drift.mjs +36 -0
  74. package/src/runtime/office/authoring/pdf-author-action.mjs +266 -0
  75. package/src/runtime/office/authoring/pdf-html-charts.mjs +379 -0
  76. package/src/runtime/office/authoring/pdf-html-runner.mjs +317 -0
  77. package/src/runtime/office/authoring/pptx-author-action.mjs +44 -22
  78. package/src/runtime/office/authoring/pptx-author-session.mjs +12 -6
  79. package/src/runtime/office/authoring/pptx-html-build.mjs +23 -4
  80. package/src/runtime/office/authoring/pptx-html-measure.mjs +38 -44
  81. package/src/runtime/office/authoring/pptx-html-runner.mjs +18 -3
  82. package/src/runtime/office/authoring/pptx-receipt.mjs +39 -31
  83. package/src/runtime/office/authoring/pptx-review-artifacts.mjs +9 -1
  84. package/src/runtime/office/authoring/pptx-script-runner.mjs +6 -2
  85. package/src/runtime/office/authoring/xlsx-html-build.mjs +365 -0
  86. package/src/runtime/office/authoring/xlsx-html-measure.mjs +257 -0
  87. package/src/runtime/office/authoring/xlsx-html-runner.mjs +139 -0
  88. package/src/runtime/office/capabilities-catalog.mjs +8 -0
  89. package/src/runtime/office/capabilities-signatures.mjs +18 -6
  90. package/src/runtime/office/capabilities.mjs +26 -1
  91. package/src/runtime/office/com/office-com-host.ps1 +133 -17
  92. package/src/runtime/office/core/office-actions-batch.mjs +16 -1
  93. package/src/runtime/office/core/office-actions-inspect.mjs +3 -2
  94. package/src/runtime/office/core/office-actions-lifecycle.mjs +6 -2
  95. package/src/runtime/office/core/office-actions-render.mjs +4 -1
  96. package/src/runtime/office/core/office-finalize/review-stage.mjs +15 -2
  97. package/src/runtime/office/core/office-qa/design-review-stage.mjs +9 -0
  98. package/src/runtime/office/core/office-session-dispatch.mjs +3 -0
  99. package/src/runtime/office/core/office-sessionless-actions.mjs +7 -2
  100. package/src/runtime/office/core/office-sessions.mjs +19 -2
  101. package/src/runtime/office/pdf/pdf-writer.mjs +1 -1
  102. package/src/runtime/office/portable/docx-formatting.mjs +21 -0
  103. package/src/runtime/office/portable/ooxml-validator.mjs +22 -1
  104. package/src/runtime/office/portable/portable-chart.mjs +10 -2
  105. package/src/runtime/office/portable/portable-docx-edits.mjs +12 -6
  106. package/src/runtime/office/portable/portable-docx-operations.mjs +75 -1
  107. package/src/runtime/office/portable/portable-docx-xml.mjs +49 -6
  108. package/src/runtime/office/portable/portable-docx.mjs +2 -0
  109. package/src/runtime/office/portable/portable-snapshot-docx.mjs +2 -0
  110. package/src/runtime/office/portable/portable-validation.mjs +6 -3
  111. package/src/runtime/office/portable/portable-xlsx-cell-values.mjs +87 -0
  112. package/src/runtime/office/portable/portable-xlsx-charts.mjs +55 -13
  113. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +1 -1
  114. package/src/runtime/office/portable/xlsx-contract.mjs +82 -3
  115. package/src/runtime/office/quality/assurance-checklist.mjs +2 -2
  116. package/src/runtime/office/quality/assurance-rendered.mjs +27 -1
  117. package/src/runtime/office/quality/assurance-structure-docx.mjs +44 -0
  118. package/src/runtime/office/quality/assurance-structure-pptx.mjs +24 -6
  119. package/src/runtime/office/quality/assurance-structure-xlsx.mjs +50 -1
  120. package/src/runtime/office/quality/design-review-critique.mjs +3 -2
  121. package/src/runtime/office/quality/document-brief.mjs +44 -0
  122. package/src/runtime/office/quality/inline-audit.mjs +7 -2
  123. package/src/runtime/office/quality/quality-pipeline.mjs +9 -1
  124. package/src/runtime/office/tool-defs.mjs +2 -2
  125. package/src/runtime/shared/llm/quota-value-estimate.mjs +317 -0
  126. package/src/runtime/shared/llm/usage-ledger-quota.mjs +266 -63
  127. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +35 -0
  128. package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
  129. package/src/runtime/shared/llm/usage-ledger.mjs +10 -1
  130. package/src/runtime/shared/provider-accounts.mjs +11 -1
  131. package/src/runtime/shared/statusline/statusline-segments.mjs +4 -4
  132. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  133. package/src/runtime/shared/wait-until.test-support.mjs +33 -0
  134. package/src/session-runtime/boot/apis.mjs +1 -1
  135. package/src/session-runtime/services/channel-admin.mjs +4 -3
  136. package/src/session-runtime/services/usage-stats-model.mjs +22 -0
  137. package/src/session-runtime/setup-tool/settings-contract.mjs +6 -1
  138. package/src/session-runtime/turn/session-ops.mjs +15 -9
  139. package/src/session-runtime/usage-stats-api.mjs +34 -2
  140. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  141. package/src/standalone/local-session-runtime.mjs +488 -0
  142. package/src/standalone/session-protocol.mjs +1 -0
  143. package/src/standalone/session-runtime-inline-host.mjs +1 -1
  144. package/src/tui/hooks/useSession.mjs +1 -1
  145. package/src/tui/session/session-api/commands.mjs +1 -1
  146. package/src/tui/session/session-api/integrations.mjs +3 -0
  147. package/src/tui/session/session-flow/auto-clear.mjs +6 -7
  148. package/src/tui/session/tool-card-results.mjs +1 -1
  149. package/src/tui/session/turn.mjs +5 -0
  150. package/src/tui/session-local.mjs +3 -488
  151. package/src/tui/session.mjs +1 -1
  152. package/src/ui/statusline-agents.mjs +0 -1
  153. package/src/rules/agent/41-cycle2-agent.md +0 -24
  154. package/src/runtime/memory/lib/cycle-scheduler/cycle2-runs.mjs +0 -120
  155. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +0 -110
  156. package/src/runtime/memory/lib/memory-cycle2-quarantine.mjs +0 -99
  157. package/src/runtime/memory/lib/memory-cycle2-review.mjs +0 -240
  158. package/src/runtime/memory/lib/memory-cycle2.mjs +0 -191
@@ -0,0 +1,488 @@
1
+ /**
2
+ * Daemon-hosted session store shared by terminal, desktop and remote clients.
3
+ *
4
+ * Runs mixdog's session manager inside the singleton daemon and exposes the
5
+ * subscribable store projected to clients. React/ink consumes the matching
6
+ * remote view through useSyncExternalStore (src/tui/hooks/useSession.mjs).
7
+ *
8
+ * Product TUI/CLI callers use src/tui/session.mjs, never this implementation.
9
+ * The singleton daemon and explicit parity tests are the only callers.
10
+ */
11
+ import { performance } from 'node:perf_hooks';
12
+ import { isDeepStrictEqual } from 'node:util';
13
+ import { bootProfile } from '../tui/session/boot-profile.mjs';
14
+ import { createGoalContinuation } from '../tui/session/goal-continuation.mjs';
15
+ import { createToolApproval } from '../tui/session/tool-approval.mjs';
16
+ import { createToolCardResults } from '../tui/session/tool-card-results.mjs';
17
+ import { createAgentJobFeed } from '../tui/session/agent-job-feed.mjs';
18
+ import { createContextState } from '../tui/session/context-state.mjs';
19
+ import { createSessionFlow } from '../tui/session/session-flow.mjs';
20
+ import { createRunTurn } from '../tui/session/turn.mjs';
21
+ import { createSessionApi } from '../tui/session/session-api.mjs';
22
+ import { createSessionDraftStore } from '../tui/session/draft-store.mjs';
23
+ import { createInitialSessionState } from '../tui/session/initial-state.mjs';
24
+ import { createActiveToolTracker } from '../tui/session/active-tool-summary.mjs';
25
+ import { createTranscriptStore } from '../tui/session/transcript-store.mjs';
26
+ import { createTranscriptIntake } from '../tui/session/transcript-intake.mjs';
27
+ import { createNoticeSurface } from '../tui/session/notice-surface.mjs';
28
+ import { attachCrossSurfaceShare } from '../tui/session/cross-surface-share.mjs';
29
+ import { createTranscriptRouteMetadata } from '../runtime/shared/transcript-metadata.mjs';
30
+ import { formatDoctorReport, runDoctorChecks } from '../tui/app/doctor.mjs';
31
+ // tuiDebug is an opt-in diagnostic trace for the hang chain (runTurn start/end,
32
+ // busy-queue enqueue/drain). Quiet by default so it can never tear through
33
+ // the alternate-screen render; enable with MIXDOG_TUI_DEBUG=1.
34
+ import { tuiDebug, nextId, createTranscriptSpillBuffer } from '../tui/session/transcript-spill.mjs';
35
+
36
+ const SESSION_RUNTIME_MODULE = '../mixdog-session-runtime.mjs';
37
+
38
+ // The runtime graph is imported lazily, but that import (measured ~250ms) used
39
+ // to land inside the FIRST session runtime creation, which desktop performs while it
40
+ // holds its transition lock — so the user's first navigation paid for it. Hosts
41
+ // can start it during their own idle startup instead; the promise is shared, so
42
+ // the create path either awaits an in-flight preload or does the import itself.
43
+ let sessionRuntimeModulePromise = null;
44
+ function importSessionRuntimeModule() {
45
+ sessionRuntimeModulePromise ??= import(SESSION_RUNTIME_MODULE);
46
+ return sessionRuntimeModulePromise;
47
+ }
48
+
49
+ export function preloadSessionRuntimeModule() {
50
+ return importSessionRuntimeModule().catch(() => {
51
+ // A real load failure surfaces on the authoritative create path.
52
+ sessionRuntimeModulePromise = null;
53
+ });
54
+ }
55
+
56
+ let agentLoopPrewarmPromise = null;
57
+ export function preloadAgentLoopRuntime() {
58
+ agentLoopPrewarmPromise ??= import('../runtime/agent/orchestrator/session/manager/runtime-loaders.mjs').then(
59
+ (module) => module.prewarmAgentLoop()
60
+ );
61
+ void agentLoopPrewarmPromise.catch(() => {
62
+ // Reservation-time prewarm retries through the same shared loader.
63
+ agentLoopPrewarmPromise = null;
64
+ });
65
+ return agentLoopPrewarmPromise;
66
+ }
67
+
68
+ // Memory-runtime attach (PG proxy + embed warmup) measured ~6.5s cold and is
69
+ // the largest first-turn blocker: loadCoreMemoryContext races it with a 2s
70
+ // cap, but a cold attach stalls the event loop hard enough that even that
71
+ // timer fires seconds late (probe: core-memory settle at ~6.0s). Warm the
72
+ // EXACT shared proxy instance the session create path resolves —
73
+ // getStandaloneMemoryRuntime caches by entry+dataDir, so the first turn finds
74
+ // it initialized. Honors the same MIXDOG_BOOT_CORE_MEMORY opt-out.
75
+ let memoryRuntimePrewarmPromise = null;
76
+ export function preloadMemoryRuntime() {
77
+ const bootFlag = String(process.env.MIXDOG_BOOT_CORE_MEMORY ?? '')
78
+ .trim()
79
+ .toLowerCase();
80
+ if (bootFlag === '0' || bootFlag === 'false' || bootFlag === 'no' || bootFlag === 'off') {
81
+ return Promise.resolve();
82
+ }
83
+ memoryRuntimePrewarmPromise ??= (async () => {
84
+ const [{ getStandaloneMemoryRuntime }, { getPluginData }, { fileURLToPath }] = await Promise.all([
85
+ import('../session-runtime/services/memory-runtime-proxy.mjs'),
86
+ import('../runtime/agent/orchestrator/config.mjs'),
87
+ import('node:url'),
88
+ ]);
89
+ const runtime = getStandaloneMemoryRuntime({
90
+ entry: fileURLToPath(new URL('../runtime/memory/index.mjs', import.meta.url)),
91
+ dataDir: process.env.MIXDOG_DATA_DIR || getPluginData?.() || undefined,
92
+ });
93
+ await runtime.init();
94
+ })();
95
+ void memoryRuntimePrewarmPromise.catch(() => {
96
+ // Opportunistic: loadCoreMemoryContext retries through the same proxy.
97
+ memoryRuntimePrewarmPromise = null;
98
+ });
99
+ return memoryRuntimePrewarmPromise;
100
+ }
101
+
102
+ // Windows keychain reads go through a DPAPI PowerShell host whose cold start
103
+ // dominates a packaged boot (measured ~1.5s). The runtime batches every secret
104
+ // into one call, but it only starts that batch when the runtime is created —
105
+ // late enough that the first context switch waits on it. Hosts can start the
106
+ // same batch while their window is still coming up.
107
+ let keychainPrewarmPromise = null;
108
+ export function preloadKeychainSecrets() {
109
+ keychainPrewarmPromise ??= (async () => {
110
+ try {
111
+ const { createRequire } = await import('node:module');
112
+ const require = createRequire(import.meta.url);
113
+ const keychain = require('../lib/keychain-cjs.cjs');
114
+ await keychain.prewarmSecrets?.();
115
+ } catch {
116
+ // Prewarm is opportunistic; the runtime still warms on its own path.
117
+ }
118
+ })();
119
+ return keychainPrewarmPromise;
120
+ }
121
+
122
+ const TOOL_APPROVAL_TIMEOUT_MS = (() => {
123
+ const value = Number(process.env.MIXDOG_TOOL_APPROVAL_TIMEOUT_MS);
124
+ return Number.isFinite(value) && value > 0 ? Math.max(1000, Math.round(value)) : 120_000;
125
+ })();
126
+
127
+ export {
128
+ cleanupStaleTranscriptSpillDirs,
129
+ createTranscriptSpillBuffer,
130
+ refillTranscriptViewOverlap,
131
+ replaceSessionItemsState,
132
+ createSessionItemMutators,
133
+ TRANSCRIPT_LIVE_ITEM_CAP,
134
+ TRANSCRIPT_SPILL_CHUNK_ITEMS,
135
+ } from '../tui/session/transcript-spill.mjs';
136
+ export { parseBackgroundTaskEnvelope } from '../tui/session/agent-envelope.mjs';
137
+
138
+ /**
139
+ * The 2s runtime pulse picks up runtime-derived state that changes without a
140
+ * notification: the route (model/effort/context window/cwd/Goal), the context
141
+ * gauge stats, and agent status. It publishes ONLY when one of those values
142
+ * actually differs from the draft or the published snapshot. Re-publishing an
143
+ * unchanged session minted a fresh snapshot every 2s, which the session
144
+ * service projected as a new revision and which kept its idle-projection
145
+ * release from ever firing for a watched session.
146
+ */
147
+ export function createRuntimePulse({
148
+ flags,
149
+ liveShareMirroring,
150
+ getState,
151
+ getPublishedState,
152
+ set,
153
+ emit,
154
+ routeState,
155
+ agentStatusState,
156
+ syncContextStats,
157
+ }) {
158
+ const publishChanged = (patch) => {
159
+ const draftState = getState();
160
+ const published = getPublishedState();
161
+ const changed = {};
162
+ let dirty = false;
163
+ for (const [key, value] of Object.entries(patch)) {
164
+ // syncContextStats stages its fields straight into the draft without
165
+ // scheduling a frame, so a value equal to the draft may still be
166
+ // unpublished.
167
+ if (isDeepStrictEqual(draftState[key], value) && isDeepStrictEqual(published[key], value)) continue;
168
+ changed[key] = value;
169
+ dirty = true;
170
+ }
171
+ if (!dirty) return false;
172
+ if (!set(changed)) emit();
173
+ return true;
174
+ };
175
+ return () => {
176
+ if (flags.disposed) return;
177
+ if (flags.pendingSessionReset) return;
178
+ // Attached viewer with a live pipe: the owner's frames are authoritative
179
+ // for stats/agent/tool state. Recomputing them locally here would stomp
180
+ // the mirror with this process's empty registries every 2s.
181
+ if (liveShareMirroring()) {
182
+ publishChanged({ ...routeState() });
183
+ return;
184
+ }
185
+ syncContextStats({ allowEstimated: true });
186
+ publishChanged({
187
+ ...routeState(),
188
+ stats: { ...getState().stats },
189
+ ...agentStatusState(),
190
+ });
191
+ };
192
+ }
193
+
194
+ export async function createLocalSessionRuntime({
195
+ provider: providerName,
196
+ model,
197
+ effort,
198
+ fast,
199
+ modelParameters,
200
+ toolMode = 'full',
201
+ remote = false,
202
+ cwd,
203
+ desktopSession,
204
+ sessionProfile,
205
+ executeAgentControl,
206
+ } = {}) {
207
+ const startedAt = performance.now();
208
+ bootProfile('session:create:start', { provider: providerName, model, toolMode, remote });
209
+ // Silence provider/session diagnostics so they cannot tear through the
210
+ // alternate-screen React/ink render.
211
+ process.env.MIXDOG_QUIET_PROVIDER_LOG = '1';
212
+ process.env.MIXDOG_QUIET_SESSION_LOG = '1';
213
+ process.env.MIXDOG_QUIET_MCP_LOG = '1';
214
+ process.env.MIXDOG_QUIET_MEMORY_LOG = '1';
215
+ process.env.MIXDOG_PATCH_NATIVE_PREWARM ??= '0';
216
+
217
+ const importStartedAt = performance.now();
218
+ const { createMixdogSessionRuntime } = await importSessionRuntimeModule();
219
+ bootProfile('session-runtime:imported', { ms: (performance.now() - importStartedAt).toFixed(1) });
220
+ const runtime = await createMixdogSessionRuntime({
221
+ provider: providerName,
222
+ model,
223
+ effort,
224
+ fast,
225
+ modelParameters,
226
+ toolMode,
227
+ remote,
228
+ ...(cwd ? { cwd } : {}),
229
+ ...(desktopSession ? { desktopSession } : {}),
230
+ ...(sessionProfile && typeof sessionProfile === 'object' ? { sessionProfile } : {}),
231
+ ...(typeof executeAgentControl === 'function' ? { executeAgentControl } : {}),
232
+ });
233
+ bootProfile('session:create:runtime-ready', { ms: (performance.now() - startedAt).toFixed(1) });
234
+ const runtimeCwd = runtime.cwd || process.cwd();
235
+ const stateStartedAt = performance.now();
236
+ const flags = {
237
+ disposed: false,
238
+ draining: false,
239
+ autoClearRunning: false,
240
+ pendingSessionReset: false,
241
+ lastUserActivityAt: Date.now(),
242
+ leadTurnEpoch: 0,
243
+ activePromptRestore: null,
244
+ pushingFromDeferredEntry: false,
245
+ flushDeferredBeforeImmediatePush: null,
246
+ pendingTranscriptMeta: null,
247
+ };
248
+ const lifecycle = {
249
+ runtimePulseTimer: null,
250
+ unsubscribeRuntimeNotifications: null,
251
+ unsubscribeAgentStatus: null,
252
+ unsubscribeRemoteState: null,
253
+ };
254
+ const pending = [];
255
+ const pendingNotificationKeys = new Set();
256
+ const displayedExecutionNotificationKeys = new Set();
257
+ const bag = {};
258
+ // The one synchronous draft every collaborator reads through; the draft
259
+ // store below publishes immutable snapshots from it once per frame.
260
+ const draft = { state: null };
261
+ // Bound once the goal-continuation controller exists: route publications
262
+ // read the Goal through its archive mask, never the raw record.
263
+ let visibleGoalStatus = null;
264
+ // Route/context/agent-status derivations live in src/tui/session/context-state.mjs.
265
+ // getState()/updateState() are late-bound to the draft so they serve both
266
+ // the pre-publication seed below and every later pulse.
267
+ const { autoClearState, agentStatusState, baseRouteState, routeState, syncContextStats } = createContextState({
268
+ runtime,
269
+ getState: () => draft.state,
270
+ updateState: (patch) => {
271
+ draft.state = { ...draft.state, ...patch };
272
+ },
273
+ getPendingSessionReset: () => flags.pendingSessionReset,
274
+ getVisibleGoal: () => (visibleGoalStatus ? visibleGoalStatus() : runtime.goalStatus?.() || null),
275
+ });
276
+ draft.state = createInitialSessionState({ runtime, runtimeCwd, baseRouteState });
277
+ bootProfile('session:route-state-ready', { ms: (performance.now() - stateStartedAt).toFixed(1) });
278
+ bootProfile('session:state-ready', { ms: (performance.now() - stateStartedAt).toFixed(1) });
279
+ const contextStartedAt = performance.now();
280
+ syncContextStats({ allowEstimated: true });
281
+ bootProfile('session:context-ready', { ms: (performance.now() - contextStartedAt).toFixed(1) });
282
+ const listeners = new Set();
283
+ const store = createSessionDraftStore({
284
+ draft,
285
+ listeners,
286
+ isDisposed: () => flags.disposed,
287
+ onBusyReleased: () => {
288
+ void bag.drain?.();
289
+ bag.scheduleGoalContinuation?.();
290
+ },
291
+ });
292
+ const { getState, getPublishedState, set, flushEmit, flushEmitImmediate } = store;
293
+
294
+ const itemIndexById = new Map();
295
+ const transcriptSpill = createTranscriptSpillBuffer();
296
+ const { markToolCallActive, markToolCallDone, clearActiveToolSummary, resetActiveToolCalls } =
297
+ createActiveToolTracker({ getState, set });
298
+ const {
299
+ restoreOlderTranscript,
300
+ restoreNewerTranscript,
301
+ replaceItems,
302
+ pushItem,
303
+ appendItems,
304
+ patchItem,
305
+ updateStreamingTail,
306
+ settleStreamingTail,
307
+ clearStreamingTail,
308
+ } = createTranscriptStore({
309
+ draft,
310
+ store,
311
+ flags,
312
+ transcriptSpill,
313
+ itemIndexById,
314
+ onBulkReplace: resetActiveToolCalls,
315
+ });
316
+ const transcriptRouteMetadata = (at = Date.now()) => createTranscriptRouteMetadata(runtime.session, routeState(), at);
317
+ const { upsertSyntheticToolItem, pushUserOrSyntheticItem, pushAsyncAgentResponse } = createTranscriptIntake({
318
+ getState,
319
+ flags,
320
+ pushItem,
321
+ patchItem,
322
+ transcriptRouteMetadata,
323
+ });
324
+ const { pushToast, pushNotice, removeNotice, setProgressHint, clearToastTimers } = createNoticeSurface({
325
+ getState,
326
+ set,
327
+ isDisposed: () => flags.disposed,
328
+ pushItem,
329
+ replaceItems,
330
+ });
331
+ const { presentNextToolApproval, finishToolApproval, denyAllToolApprovals, requestToolApproval } = createToolApproval(
332
+ {
333
+ getState,
334
+ set,
335
+ nextId,
336
+ getDisposed: () => flags.disposed,
337
+ timeoutMs: TOOL_APPROVAL_TIMEOUT_MS,
338
+ }
339
+ );
340
+ lifecycle.runtimePulseTimer = setInterval(
341
+ createRuntimePulse({
342
+ flags,
343
+ liveShareMirroring: () => bag.liveShareMirroring?.() === true,
344
+ getState,
345
+ getPublishedState,
346
+ set,
347
+ emit: store.emit,
348
+ routeState,
349
+ agentStatusState,
350
+ syncContextStats,
351
+ }),
352
+ 2000
353
+ );
354
+ lifecycle.runtimePulseTimer.unref?.();
355
+
356
+ const {
357
+ kickExecutionPendingResume,
358
+ flushDeferredExecutionPendingResumeKick,
359
+ scheduleExecutionPendingResumeKick,
360
+ discardExecutionPendingResume,
361
+ updateAgentJobCard,
362
+ buildAgentJobCardPatch,
363
+ subscribeRuntimeNotifications,
364
+ clearExecutionDedupState,
365
+ } = createAgentJobFeed({
366
+ runtime,
367
+ getState,
368
+ set,
369
+ nextId,
370
+ getDisposed: () => flags.disposed,
371
+ patchItem,
372
+ enqueue: (...args) => bag.enqueue(...args),
373
+ drain: (...args) => bag.drain(...args),
374
+ pushUserOrSyntheticItem,
375
+ pushAsyncAgentResponse,
376
+ makeQueueEntry: (...args) => bag.makeQueueEntry(...args),
377
+ getPending: () => pending,
378
+ agentStatusState,
379
+ displayedExecutionNotificationKeys,
380
+ pushNotice,
381
+ itemIndexById,
382
+ });
383
+ lifecycle.unsubscribeRuntimeNotifications = subscribeRuntimeNotifications();
384
+ if (typeof runtime.onAgentStatusChange === 'function') {
385
+ lifecycle.unsubscribeAgentStatus = runtime.onAgentStatusChange(() => {
386
+ if (flags.disposed || flags.pendingSessionReset || bag.liveShareMirroring?.()) return;
387
+ set({ ...agentStatusState({ force: true }) });
388
+ bag.scheduleGoalContinuation?.();
389
+ });
390
+ }
391
+
392
+ const { patchToolCardResult, flushToolResults } = createToolCardResults({
393
+ getState,
394
+ set,
395
+ patchItem,
396
+ markToolCallDone,
397
+ updateAgentJobCard,
398
+ buildAgentJobCardPatch,
399
+ agentStatusState,
400
+ itemIndexById,
401
+ });
402
+
403
+ Object.assign(bag, {
404
+ runtime,
405
+ nextId,
406
+ tuiDebug,
407
+ flags,
408
+ lifecycle,
409
+ pending,
410
+ pendingNotificationKeys,
411
+ displayedExecutionNotificationKeys,
412
+ clearExecutionDedupState,
413
+ listeners,
414
+ itemIndexById,
415
+ getState,
416
+ getPublishedState,
417
+ set,
418
+ flushEmit,
419
+ flushEmitImmediate,
420
+ disposeEmit: store.disposeEmit,
421
+ pushItem,
422
+ appendItems,
423
+ patchItem,
424
+ replaceItems,
425
+ restoreOlderTranscript,
426
+ restoreNewerTranscript,
427
+ updateStreamingTail,
428
+ settleStreamingTail,
429
+ clearStreamingTail,
430
+ pushToast,
431
+ pushNotice,
432
+ removeNotice,
433
+ setProgressHint,
434
+ pushUserOrSyntheticItem,
435
+ pushAsyncAgentResponse,
436
+ upsertSyntheticToolItem,
437
+ markToolCallActive,
438
+ markToolCallDone,
439
+ clearActiveToolSummary,
440
+ clearToastTimers,
441
+ autoClearState,
442
+ agentStatusState,
443
+ baseRouteState,
444
+ routeState,
445
+ transcriptRouteMetadata,
446
+ syncContextStats,
447
+ disposeTranscriptSpill: () => transcriptSpill.dispose(),
448
+ snapshotTranscriptSpill: () => transcriptSpill.snapshot(),
449
+ restoreTranscriptSpill: (snapshot) => transcriptSpill.restoreSnapshot(snapshot),
450
+ releaseTranscriptSpill: (snapshot) => transcriptSpill.releaseSnapshot(snapshot),
451
+ presentNextToolApproval,
452
+ finishToolApproval,
453
+ denyAllToolApprovals,
454
+ requestToolApproval,
455
+ patchToolCardResult,
456
+ flushToolResults,
457
+ kickExecutionPendingResume,
458
+ flushDeferredExecutionPendingResumeKick,
459
+ scheduleExecutionPendingResumeKick,
460
+ discardExecutionPendingResume,
461
+ updateAgentJobCard,
462
+ subscribeRuntimeNotifications,
463
+ });
464
+ Object.assign(bag, createSessionFlow(bag));
465
+ Object.assign(
466
+ bag,
467
+ createGoalContinuation({
468
+ runtime,
469
+ flags,
470
+ getState,
471
+ set,
472
+ getPending: () => pending,
473
+ enqueue: (...args) => bag.enqueue(...args),
474
+ })
475
+ );
476
+ visibleGoalStatus = bag.visibleGoalStatus;
477
+ bag.runTurn = createRunTurn(bag);
478
+ bag.doctor = { runDoctorChecks, formatDoctorReport };
479
+ const api = createSessionApi(bag);
480
+ attachCrossSurfaceShare({ runtime, api, bag, flags, getState, getPublishedState, listeners, set });
481
+ void Promise.resolve(bag.restoreLeadSteeringFromDisk())
482
+ .catch(() => {})
483
+ .finally(() => {
484
+ bag.refreshGoalState?.();
485
+ bag.scheduleGoalContinuation?.();
486
+ });
487
+ return api;
488
+ }
@@ -38,6 +38,7 @@ export const SESSION_READ_ACTIONS = Object.freeze([
38
38
  'inspectHuggingFaceModel',
39
39
  'getLocalProviderModelDetails',
40
40
  'getSessionReviewDiff',
41
+ 'getSessionUsage',
41
42
  'getTurnReviewDiff',
42
43
  'getTurnLiveness',
43
44
  'getUpdateSettings',
@@ -15,7 +15,7 @@ export function createInlineSessionRuntimeHost({
15
15
  cwd = process.cwd(),
16
16
  log = () => {},
17
17
  measureBootPhase = async (_phase, task) => await task(),
18
- loadLocalModule = () => import('../tui/session-local.mjs'),
18
+ loadLocalModule = () => import('./local-session-runtime.mjs'),
19
19
  loadAgentGraph = () =>
20
20
  Promise.all([
21
21
  import('../runtime/agent/orchestrator/config.mjs'),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * src/tui/hooks/useSession.mjs — subscribe React to the session store.
3
3
  *
4
- * The store (session-local.mjs) lives outside React and emits immutable snapshots;
4
+ * The store (local-session-runtime.mjs) lives outside React and emits immutable snapshots;
5
5
  * useSyncExternalStore re-renders the tree whenever a snapshot changes. This
6
6
  * keeps the agentLoop fully decoupled from React's lifecycle.
7
7
  */
@@ -17,7 +17,7 @@ export function createSessionCommandsApi(bag) {
17
17
  routeState,
18
18
  syncContextStats,
19
19
  replaceItems,
20
- // Injected by the store composer (session-local.mjs): the doctor checks
20
+ // Injected by the store composer (local-session-runtime.mjs): the doctor checks
21
21
  // live in the app layer, which the session layer must not import.
22
22
  doctor,
23
23
  } = bag;
@@ -68,6 +68,9 @@ export function createSessionIntegrationsApi(bag, { oauthFlows }) {
68
68
  getQuotaHistory: async (options = {}) => {
69
69
  return await runtime.getQuotaHistory?.(options);
70
70
  },
71
+ getSessionUsage: async () => {
72
+ return await runtime.getSessionUsage?.();
73
+ },
71
74
  consumeCodexRateLimitResetCredit: async (options = {}) => {
72
75
  // Desktop capability parity: without this delegation the session runtime
73
76
  // surface rejects the sidebar's reset-credit invoke as unsupported even
@@ -57,6 +57,7 @@ export function createAutoClearOps(bag, { reset, kickDrain }) {
57
57
  // Only an actual compaction resets the visible transcript; an unchanged
58
58
  // success keeps it. The model transcript always belongs to runtime.compact.
59
59
  function applyAutoClearUi(result) {
60
+ if (result.changed === false) return false;
60
61
  const compactChanged = result.changed !== false;
61
62
  if (compactChanged) {
62
63
  reset.resetStats();
@@ -68,6 +69,7 @@ export function createAutoClearOps(bag, { reset, kickDrain }) {
68
69
  stats: { ...getState().stats },
69
70
  });
70
71
  pushItem({ kind: 'statusdone', id: nextId(), label: 'Auto-clear complete' });
72
+ return true;
71
73
  }
72
74
 
73
75
  // Flush a deferred cleared-session UI sync once the active turn has settled.
@@ -77,8 +79,7 @@ export function createAutoClearOps(bag, { reset, kickDrain }) {
77
79
  const { result } = flags.pendingClearedSessionUi;
78
80
  flags.pendingClearedSessionUi = null;
79
81
  flags.autoClearInFlight = false;
80
- applyAutoClearUi(result);
81
- pushNotice(LATE_COMPLETION_NOTICE, 'info');
82
+ if (applyAutoClearUi(result)) pushNotice(LATE_COMPLETION_NOTICE, 'info');
82
83
  }
83
84
 
84
85
  function onLateCompact(lateResult) {
@@ -86,8 +87,7 @@ export function createAutoClearOps(bag, { reset, kickDrain }) {
86
87
  // Do not wipe items/queued or force busy=false mid-turn.
87
88
  flags.pendingClearedSessionUi = { result: lateResult };
88
89
  } else {
89
- applyAutoClearUi(lateResult);
90
- pushNotice(LATE_COMPLETION_NOTICE, 'info');
90
+ if (applyAutoClearUi(lateResult)) pushNotice(LATE_COMPLETION_NOTICE, 'info');
91
91
  }
92
92
  }
93
93
 
@@ -95,7 +95,7 @@ export function createAutoClearOps(bag, { reset, kickDrain }) {
95
95
  // late completion can reset the UI. Never follow compact with clear:
96
96
  // that would discard conversation/execution records the compactor kept.
97
97
  async function compactWithinTimeout(compactTimeoutMs) {
98
- const compactPromise = runtime.compact().then((result) => {
98
+ const compactPromise = runtime.compact({ requireReduction: true }).then((result) => {
99
99
  if (!result) throw new Error('no active session');
100
100
  if (result.error) throw new Error(result.error);
101
101
  return result;
@@ -161,8 +161,7 @@ export function createAutoClearOps(bag, { reset, kickDrain }) {
161
161
  // Without this, long idle clears can look like a frozen prompt followed by
162
162
  // an already-complete status row.
163
163
  await new Promise((resolve) => setTimeout(resolve, 0));
164
- applyAutoClearUi(await compactWithinTimeout(compactTimeoutMs));
165
- return true;
164
+ return applyAutoClearUi(await compactWithinTimeout(compactTimeoutMs));
166
165
  } catch (error) {
167
166
  const message = presentErrorText(error, { surface: 'compact' });
168
167
  pushItem({
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * src/tui/session/tool-card-results.mjs — the tool-card result state machine
3
3
  * (patchToolCardResult + flushToolResults) for the session runtime
4
- * (session-local.mjs), as a dependency-injection factory.
4
+ * (local-session-runtime.mjs), as a dependency-injection factory.
5
5
  *
6
6
  * These handlers own the per-turn accounting that reflects tool results into
7
7
  * store items: aggregate cards, non-aggregate/legacy agent-job cards, grouped
@@ -408,6 +408,11 @@ function turnDoneItem({ turn, stream, cards, getState, nextId }) {
408
408
  id: nextId(),
409
409
  elapsedMs: Date.now() - turn.startedAt,
410
410
  status: turnOutcome(turn.cancelled, turn.failed),
411
+ // Notifications resolve this exact terminal assistant item, never a
412
+ // session preview or a tool preamble from earlier in the turn.
413
+ ...(turn.finishedNormally && stream.currentAssistantId != null
414
+ ? { finalAssistantId: stream.currentAssistantId }
415
+ : {}),
411
416
  outputTokens: finalOutputTokens(stream, getState().spinner?.outputTokens),
412
417
  thinkingElapsedMs: stream.thinkingStartedAt ? stream.accumulatedThinkingMs : 0,
413
418
  toolCount: cards.toolCards.length,