mixdog 1.2.0 → 1.2.1

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 (201) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/src/defaults/shipped-history.json +4 -0
  4. package/src/defaults/skills/pptx/SKILL.md +6 -6
  5. package/src/defaults/skills/pptx/references/review.md +6 -4
  6. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  7. package/src/defaults/skills/setup/references/actions.md +13 -3
  8. package/src/defaults/skills/visualize/SKILL.md +12 -9
  9. package/src/rules/shared/60-verification.md +3 -1
  10. package/src/rules/shared/70-delivery.md +4 -2
  11. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  12. package/src/runtime/agent/orchestrator/config.mjs +10 -4
  13. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  14. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  15. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  16. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +86 -8
  17. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +5 -1
  18. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +65 -7
  19. package/src/runtime/agent/orchestrator/mcp/client.mjs +128 -44
  20. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  21. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +19 -11
  25. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  26. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  27. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  28. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
  29. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  31. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +9 -2
  32. package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
  33. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  34. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  35. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
  36. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
  37. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
  38. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  39. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
  43. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +9 -3
  44. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
  45. package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
  46. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
  47. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  48. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  49. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +267 -84
  50. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  52. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  53. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  54. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
  55. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  56. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  57. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
  58. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  59. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  60. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  61. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  62. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  63. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  64. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  65. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  66. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  67. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
  68. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +6 -4
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
  70. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  71. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +3 -1
  73. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
  74. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
  75. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
  76. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
  77. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  78. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
  79. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  80. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
  82. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +7 -0
  83. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +19 -12
  84. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  85. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
  86. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  87. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +21 -2
  89. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  91. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  92. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  93. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
  95. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  99. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  101. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  102. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  104. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  105. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  106. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  107. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  108. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  109. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  110. package/src/runtime/attachments/store.d.mts +3 -0
  111. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  112. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  113. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  114. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  115. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  116. package/src/runtime/memory/lib/memory.mjs +1 -1
  117. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  118. package/src/runtime/shared/atomic-file.mjs +25 -4
  119. package/src/runtime/shared/extension-scopes.mjs +5 -3
  120. package/src/runtime/shared/file-lock.mjs +37 -11
  121. package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
  122. package/src/runtime/shared/llm/model-catalog.mjs +0 -12
  123. package/src/runtime/shared/llm/provider-catalog-registry.mjs +6 -0
  124. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  125. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  126. package/src/runtime/shared/pristine-execution.mjs +28 -4
  127. package/src/runtime/shared/provider-accounts.mjs +124 -20
  128. package/src/runtime/shared/provider-api-key.mjs +12 -0
  129. package/src/runtime/shared/shell-display.mjs +9 -3
  130. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  131. package/src/runtime/shared/skill-mentions.mjs +133 -0
  132. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  133. package/src/runtime/shared/tool-primitives.mjs +35 -2
  134. package/src/runtime/shared/turn-worktree-snapshot.mjs +25 -5
  135. package/src/runtime/shared/worker-requests.mjs +15 -2
  136. package/src/session-runtime/automation-agents.mjs +8 -2
  137. package/src/session-runtime/boot/apis.mjs +55 -0
  138. package/src/session-runtime/boot/config.mjs +5 -2
  139. package/src/session-runtime/boot/facade.mjs +1 -0
  140. package/src/session-runtime/boot/infrastructure.mjs +29 -18
  141. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  142. package/src/session-runtime/conversation-import/service.mjs +369 -0
  143. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
  144. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  145. package/src/session-runtime/mcp-glue/connect.mjs +57 -10
  146. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  147. package/src/session-runtime/mcp-glue/server-catalog.mjs +115 -30
  148. package/src/session-runtime/mcp-glue.mjs +22 -2
  149. package/src/session-runtime/new-session-config.mjs +3 -2
  150. package/src/session-runtime/notification-bus.mjs +9 -2
  151. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  152. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  153. package/src/session-runtime/provider-readiness.mjs +16 -10
  154. package/src/session-runtime/resource-extension-sync.mjs +20 -5
  155. package/src/session-runtime/resource-mcp-api.mjs +101 -77
  156. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  157. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  158. package/src/session-runtime/resource-skills-api.mjs +23 -2
  159. package/src/session-runtime/schedule-session-run.mjs +1 -1
  160. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  161. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  162. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  163. package/src/session-runtime/services/provider-admin.mjs +64 -18
  164. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  165. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  166. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  167. package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
  168. package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
  169. package/src/session-runtime/skills-api.mjs +7 -3
  170. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  171. package/src/session-runtime/webhook-session-run.mjs +16 -4
  172. package/src/skill-script-entry.mjs +36 -0
  173. package/src/standalone/daemon-stored-session-views.mjs +11 -3
  174. package/src/standalone/daemon.mjs +41 -4
  175. package/src/standalone/session-protocol.mjs +5 -0
  176. package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
  177. package/src/tui/App.jsx +9 -1
  178. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  179. package/src/tui/app/app-view.jsx +6 -0
  180. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  181. package/src/tui/app/model-options.mjs +2 -0
  182. package/src/tui/app/skill-mention-palette.mjs +88 -0
  183. package/src/tui/app/slash-palette.mjs +23 -7
  184. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  185. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  186. package/src/tui/app/use-prompt-state.mjs +5 -0
  187. package/src/tui/components/Message.jsx +22 -1
  188. package/src/tui/components/PromptInput.jsx +21 -3
  189. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  190. package/src/tui/components/TranscriptItem.jsx +9 -1
  191. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  192. package/src/tui/components/user-message-skills.mjs +38 -0
  193. package/src/tui/dist/index.mjs +507 -171
  194. package/src/tui/session/queue-helpers.mjs +7 -2
  195. package/src/tui/session/session-api/extensions.mjs +4 -2
  196. package/src/tui/session/session-api/settings.mjs +7 -0
  197. package/src/tui/session/session-flow/drain.mjs +14 -4
  198. package/src/tui/session/tui-steering-persist.mjs +25 -9
  199. package/src/tui/session/turn.mjs +17 -0
  200. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  201. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
@@ -48,6 +48,13 @@ export function workspaceSummary(value) {
48
48
  };
49
49
  }
50
50
 
51
+ /** Dedupe key of an imported conversation: a session's `importOrigin.key`,
52
+ * or a summary row's projected `importKey`; null for ordinary sessions. */
53
+ export function importKey(value) {
54
+ const key = typeof value?.importKey === 'string' ? value.importKey : value?.importOrigin?.key;
55
+ return typeof key === 'string' && key.trim() ? key : null;
56
+ }
57
+
51
58
  export function desktopSession(value, cwd = '') {
52
59
  if (!value || typeof value !== 'object') return null;
53
60
  if (value.classification === 'task') return { classification: 'task', projectPath: null };
@@ -15,18 +15,15 @@ import {
15
15
  writeJsonAtomicAsync,
16
16
  writeJsonAtomicSync,
17
17
  } from '../../../shared/atomic-file.mjs';
18
- import {
19
- applySummaryLogText,
20
- encodeSummaryOps,
21
- summaryLogPath,
22
- } from './store-summary-log.mjs';
23
- import {
24
- cleanSessionPreview,
25
- isSessionPreviewNoise,
26
- sessionMessageText,
27
- } from '../runtime-core/session-text.mjs';
18
+ import { applySummaryLogText, encodeSummaryOps, summaryLogPath } from './store-summary-log.mjs';
19
+ import { cleanSessionPreview, isSessionPreviewNoise, sessionMessageText } from '../runtime-core/session-text.mjs';
28
20
  import { sessionVisibility } from './store-summary-visibility.mjs';
29
- import { isStoredSessionId, positiveNumber as _positiveNumber, workspaceSummary } from './store-summary-fields.mjs';
21
+ import {
22
+ importKey,
23
+ isStoredSessionId,
24
+ positiveNumber as _positiveNumber,
25
+ workspaceSummary,
26
+ } from './store-summary-fields.mjs';
30
27
 
31
28
  export const SESSION_SUMMARY_INDEX_VERSION = 2;
32
29
 
@@ -209,6 +206,9 @@ export function _sessionSummary(session) {
209
206
  // Lifecycle provenance for catalog filters: resume-machinery scratch
210
207
  // forks (detachedReason='cli-resume') must never surface in Recent.
211
208
  detachedReason: session.detachedReason || null,
209
+ // Imported-conversation identity, so "already imported" is answered
210
+ // from summaries without loading sessions.
211
+ importKey: importKey(session),
212
212
  };
213
213
  }
214
214
 
@@ -257,6 +257,7 @@ function _normalizeSummaryRow(row) {
257
257
  storageMtimeMs: _positiveNumber(row.storageMtimeMs, 0),
258
258
  storageSize: _positiveNumber(row.storageSize, 0),
259
259
  detachedReason: row.detachedReason || null,
260
+ importKey: importKey(row),
260
261
  };
261
262
  }
262
263
 
@@ -354,7 +355,9 @@ function _scheduleSummaryRetry() {
354
355
  // Defer the flush off the caller's stack: file I/O has no business on the
355
356
  // keystroke/session-save path.
356
357
  function _scheduleSummaryFlush() {
357
- if (_summaryFlushScheduled) return;
358
+ // A scheduled retry will write whatever is pending; a flush in flight
359
+ // schedules one follow-up when it settles. Either way, don't start another.
360
+ if (_summaryFlushScheduled || _summaryRetryTimer || _summaryFlushInflight > 0) return;
358
361
  _summaryFlushScheduled = true;
359
362
  setImmediate(() => {
360
363
  _summaryFlushScheduled = false;
@@ -365,6 +368,9 @@ function _scheduleSummaryFlush() {
365
368
 
366
369
  export function _flushPendingSummaryOps({ sync = false } = {}) {
367
370
  if (_pendingUpserts.size === 0 && _pendingRemovals.size === 0) return;
371
+ // One async flush in flight per process; ops queued meanwhile stay pending
372
+ // and are drained by the follow-up flush scheduled when it settles.
373
+ if (!sync && _summaryFlushInflight > 0) return;
368
374
  // Snapshot + clear first: ops queued by other callers DURING the flush
369
375
  // must not be lost, and a failed flush re-queues only what it took
370
376
  // (without clobbering anything newer queued meanwhile).
@@ -420,6 +426,7 @@ export function _flushPendingSummaryOps({ sync = false } = {}) {
420
426
  .catch(requeue)
421
427
  .finally(() => {
422
428
  _summaryFlushInflight--;
429
+ if (_pendingUpserts.size > 0 || _pendingRemovals.size > 0) _scheduleSummaryFlush();
423
430
  _notifySummarySettled();
424
431
  });
425
432
  }
@@ -0,0 +1,158 @@
1
+ // Shared fixture of the ordinary-catalog tests: a scratch data dir, the
2
+ // writers that touch it (index-updating, rename-only, in-place) and a spy on
3
+ // the filesystem work the cold reader does.
4
+ import assert from 'node:assert/strict';
5
+ import fs, {
6
+ appendFileSync,
7
+ mkdirSync,
8
+ mkdtempSync,
9
+ renameSync,
10
+ rmSync,
11
+ statSync,
12
+ unlinkSync,
13
+ writeFileSync,
14
+ } from 'node:fs';
15
+ import fsp from 'node:fs/promises';
16
+ import { syncBuiltinESMExports } from 'node:module';
17
+ import { tmpdir } from 'node:os';
18
+ import { join } from 'node:path';
19
+ import test from 'node:test';
20
+
21
+ import { encodeSummaryOps } from './store-summary-log.mjs';
22
+ import { listStoredSessionSummaries } from './store-summary-reader.mjs';
23
+
24
+ const roots = [];
25
+ let root = '';
26
+ test.after(() => {
27
+ for (const dir of roots) rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 });
28
+ });
29
+
30
+ /** Point the reader at a fresh data dir with an empty sessions directory. */
31
+ export function useDataDir(prefix) {
32
+ root = mkdtempSync(join(tmpdir(), prefix));
33
+ roots.push(root);
34
+ mkdirSync(join(root, 'sessions'));
35
+ process.env.MIXDOG_DATA_DIR = root;
36
+ }
37
+
38
+ export const sessionsDir = () => join(root, 'sessions');
39
+ export const fileOf = (id) => join(sessionsDir(), `${id}.json`);
40
+ export const indexPath = () => join(root, 'session-summaries.json');
41
+ export const logPath = () => join(root, 'session-summaries.log');
42
+
43
+ const record = (id, text) =>
44
+ JSON.stringify({
45
+ id,
46
+ owner: 'user',
47
+ agent: 'lead',
48
+ closed: false,
49
+ generation: 1,
50
+ updatedAt: 1,
51
+ messages: [{ role: 'user', content: text }],
52
+ });
53
+
54
+ /** The store's writers replace a record by temp file + rename. */
55
+ export function replace(id, text) {
56
+ writeFileSync(`${fileOf(id)}.tmp`, record(id, text));
57
+ renameSync(`${fileOf(id)}.tmp`, fileOf(id));
58
+ }
59
+
60
+ /** A writer outside the store that rewrites the record in place. */
61
+ export const rewriteInPlace = (id, text) => writeFileSync(fileOf(id), record(id, text));
62
+
63
+ /** What the summary index records for the file as it is now; the `indexed:`
64
+ * title tells an index row from a row parsed from the file. */
65
+ export const indexRow = (id, text, fingerprint = statSync(fileOf(id))) => ({
66
+ id,
67
+ owner: 'user',
68
+ agent: 'lead',
69
+ status: 'idle',
70
+ messageCount: 1,
71
+ title: `indexed:${text}`,
72
+ preview: text,
73
+ updatedAt: 1,
74
+ storageMtimeMs: fingerprint.mtimeMs,
75
+ storageSize: fingerprint.size,
76
+ });
77
+
78
+ export function writeBase(rows) {
79
+ writeFileSync(`${indexPath()}.tmp`, JSON.stringify({ version: 2, rows }));
80
+ renameSync(`${indexPath()}.tmp`, indexPath());
81
+ }
82
+
83
+ export const appendLog = (upserts, removals = []) => appendFileSync(logPath(), encodeSummaryOps(upserts, removals));
84
+
85
+ /** An index-updating writer: the record, then its summary row. */
86
+ export function saveIndexed(id, text) {
87
+ replace(id, text);
88
+ appendLog([indexRow(id, text)]);
89
+ }
90
+
91
+ /** A namespace change that touches no session. */
92
+ export function touchDirectory() {
93
+ writeFileSync(join(sessionsDir(), 'unrelated.tmp'), '');
94
+ unlinkSync(join(sessionsDir(), 'unrelated.tmp'));
95
+ }
96
+
97
+ export const list = () => listStoredSessionSummaries();
98
+ export const view = (rows = list()) => Object.fromEntries(rows.map((row) => [row.id, `${row.title}|${row.preview}`]));
99
+
100
+ /** The authoritative refresh parses every file, so it agrees with the
101
+ * ordinary catalog on which sessions exist and what they show, not on
102
+ * index-only titles. */
103
+ export function assertMatchesStorage() {
104
+ const previews = (rows) => Object.fromEntries(rows.map((row) => [row.id, row.preview]));
105
+ assert.deepEqual(previews(list()), previews(listStoredSessionSummaries({ refreshFromStorage: true })));
106
+ }
107
+
108
+ /** A directory stamp younger than the racy window is never trusted; wait it
109
+ * out so the following calls exercise the stamp-keyed path. */
110
+ export const settle = () => new Promise((resolve) => setTimeout(resolve, 2_100));
111
+
112
+ /** Counts the filesystem work the reader does; `failing` paths throw EACCES
113
+ * on every call, `failingReads` paths on reads only (a stat still works). */
114
+ export function spyFs(t, failing = new Set(), failingReads = new Set()) {
115
+ const counts = { stats: 0, sessionFileStats: new Set(), readdirs: 0, reads: 0, asyncStats: 0 };
116
+ const original = {
117
+ statSync: fs.statSync,
118
+ readdirSync: fs.readdirSync,
119
+ readFileSync: fs.readFileSync,
120
+ stat: fsp.stat,
121
+ };
122
+ const denied = (path) => {
123
+ if (!failing.has(String(path))) return;
124
+ throw Object.assign(new Error(`EACCES: ${path}`), { code: 'EACCES' });
125
+ };
126
+ fs.statSync = function (path, ...rest) {
127
+ counts.stats++;
128
+ if (String(path).endsWith('.json') && String(path).startsWith(sessionsDir())) {
129
+ counts.sessionFileStats.add(String(path));
130
+ }
131
+ denied(path);
132
+ return original.statSync.call(this, path, ...rest);
133
+ };
134
+ fs.readdirSync = function (...args) {
135
+ counts.readdirs++;
136
+ return original.readdirSync.apply(this, args);
137
+ };
138
+ fs.readFileSync = function (path, ...rest) {
139
+ counts.reads++;
140
+ denied(path);
141
+ if (failingReads.has(String(path))) throw Object.assign(new Error(`EIO: ${path}`), { code: 'EIO' });
142
+ return original.readFileSync.call(this, path, ...rest);
143
+ };
144
+ fsp.stat = async function (path, ...rest) {
145
+ counts.asyncStats++;
146
+ denied(path);
147
+ return original.stat.call(this, path, ...rest);
148
+ };
149
+ syncBuiltinESMExports();
150
+ t.after(() => {
151
+ fs.statSync = original.statSync;
152
+ fs.readdirSync = original.readdirSync;
153
+ fs.readFileSync = original.readFileSync;
154
+ fsp.stat = original.stat;
155
+ syncBuiltinESMExports();
156
+ });
157
+ return counts;
158
+ }