mixdog 1.0.5 → 1.0.7

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 (50) hide show
  1. package/README.md +206 -475
  2. package/package.json +11 -4
  3. package/src/defaults/skills/setup/references/actions.md +26 -6
  4. package/src/defaults/skills/setup/references/surfaces.md +8 -3
  5. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +16 -7
  6. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +6 -2
  7. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +0 -3
  8. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +0 -2
  9. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +24 -2
  10. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +25 -0
  11. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +14 -1
  12. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +3 -45
  13. package/src/runtime/channels/lib/config.mjs +0 -1
  14. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +1 -1
  15. package/src/runtime/channels/lib/scheduler.mjs +15 -56
  16. package/src/runtime/channels/lib/worker-main.mjs +2 -9
  17. package/src/runtime/shared/config.mjs +66 -1
  18. package/src/runtime/shared/llm/usage-ledger-quota.mjs +13 -2
  19. package/src/runtime/shared/path-executable.mjs +58 -0
  20. package/src/session-runtime/boot/apis.mjs +6 -2
  21. package/src/session-runtime/boot/begin.mjs +0 -2
  22. package/src/session-runtime/boot/providers.mjs +3 -0
  23. package/src/session-runtime/boot/tools.mjs +0 -1
  24. package/src/session-runtime/config-lifecycle.mjs +11 -0
  25. package/src/session-runtime/internal-tool-executor.mjs +0 -2
  26. package/src/session-runtime/runtime-feature-gates.mjs +2 -3
  27. package/src/session-runtime/schedule-session-run.mjs +0 -1
  28. package/src/session-runtime/services/channel-admin.mjs +8 -26
  29. package/src/session-runtime/settings-compaction-api.mjs +5 -1
  30. package/src/session-runtime/settings-system-api.mjs +2 -20
  31. package/src/session-runtime/setup-tool/executor.mjs +61 -3
  32. package/src/session-runtime/setup-tool/extended-actions.mjs +31 -6
  33. package/src/session-runtime/setup-tool/settings-contract.mjs +27 -3
  34. package/src/session-runtime/setup-tool/tool-defs.mjs +35 -5
  35. package/src/session-runtime/webhook-session-run.mjs +0 -2
  36. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +13 -3
  37. package/src/standalone/daemon-stored-session-views.mjs +4 -0
  38. package/src/standalone/session-protocol.mjs +0 -1
  39. package/src/standalone/session-service/session-calls/session-views.mjs +10 -4
  40. package/src/standalone/session-service/stored-reader.mjs +20 -2
  41. package/src/standalone/session-service/viewers.mjs +3 -3
  42. package/src/standalone/session-service.mjs +3 -1
  43. package/src/tui/App.jsx +6 -1
  44. package/src/tui/app/create-app-pickers.mjs +1 -0
  45. package/src/tui/app/doctor.mjs +6 -14
  46. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +8 -3
  47. package/src/tui/app/settings-picker/settings-rows.mjs +5 -1
  48. package/src/tui/app/use-ui-open-request.mjs +13 -3
  49. package/src/tui/dist/index.mjs +41 -9
  50. package/src/tui/session/session-api/settings.mjs +0 -6
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
6
+ "description": "Open-source coding agent that matches Codex on Terminal-Bench 2.1 with 63% fewer tokens.",
7
7
  "license": "Apache-2.0",
8
8
  "repository": {
9
9
  "type": "git",
@@ -14,10 +14,17 @@
14
14
  },
15
15
  "homepage": "https://github.com/tribgames/mixdog#readme",
16
16
  "keywords": [
17
+ "ai",
17
18
  "agent",
18
- "cli",
19
- "tui",
20
19
  "coding-agent",
20
+ "llm",
21
+ "claude",
22
+ "codex",
23
+ "openai",
24
+ "token-efficiency",
25
+ "terminal-bench",
26
+ "desktop",
27
+ "cli",
21
28
  "mcp"
22
29
  ],
23
30
  "bin": {
@@ -6,8 +6,8 @@ Read this when choosing a `setup` mutation.
6
6
 
7
7
  - `status` reads one domain: summary, model, agents, workflow, websearch,
8
8
  output-style, profile, autoclear, compaction, memory, features, local-provider, shell,
9
- providers, mcp, skills, plugins, update, onboarding, capabilities, desktop,
10
- appearance, projects, connection, schedules, or webhooks.
9
+ providers, mcp, skills, plugins, update, onboarding, capabilities, developer,
10
+ desktop, appearance, projects, connection, schedules, or webhooks.
11
11
  - `open` navigates to a supported UI surface. Read `surfaces.md` for targets and
12
12
  UI-only settings.
13
13
 
@@ -41,7 +41,9 @@ Read this when choosing a `setup` mutation.
41
41
  | Permanently delete confirmed, unchanged, unused model files | `delete_local_model` |
42
42
  | TUI system shell | `set_system_shell` |
43
43
  | Automatic updates | `set_auto_update` |
44
- | Remove stored provider authentication | `forget_provider_auth` |
44
+ | Remove stored provider authentication (one OAuth account at a time) | `forget_provider_auth` |
45
+ | Select, rename, reorder, or auto-switch OAuth accounts | `set_provider_account` |
46
+ | Developer options such as risk-gated OAuth providers | `set_developer_option` |
45
47
  | Add an MCP server | `add_mcp_server` |
46
48
  | Edit or rename an MCP server | `save_mcp_server` |
47
49
  | Remove an MCP server | `remove_mcp_server` |
@@ -53,6 +55,7 @@ Read this when choosing a `setup` mutation.
53
55
  | Refresh a plugin checkout | `update_plugin` |
54
56
  | Enable or disable a plugin | `set_plugin_enabled` |
55
57
  | Remove a plugin | `remove_plugin` |
58
+ | Install or reconfigure the MCP server a plugin ships | `enable_plugin_mcp` |
56
59
  | Available hosted models and their options; optionally native-search capable only | `list_models` |
57
60
  | Agent orchestration, independent of workflow instructions | `set_orchestration_mode` |
58
61
  | Safe MCP edit metadata, excluding raw connection/credential values | `get_mcp_server` |
@@ -65,7 +68,8 @@ Read this when choosing a `setup` mutation.
65
68
  | Enable/disable a schedule/webhook | `set_automation_enabled` |
66
69
  | Webhook listener enabled state, port, or domain | `set_webhook_config` |
67
70
  | Desktop keep-awake, run in background after window close, usage pin, or Computer observe-only state | `set_desktop_settings` |
68
- | Desktop theme, display language, side panels, or zoom | `set_appearance` |
71
+ | Desktop theme, display language, or side panels | `set_appearance` |
72
+ | Which activity rail items are pinned, in order (current list in `status desktop`) | `set_activity_rail_pins` |
69
73
  | Register a Project and optionally set its display alias | `save_project` |
70
74
  | Unregister a Project without deleting its files | `remove_project` |
71
75
  | Revoke one linked device by its client id from `status connection`; re-pairing restores access | `revoke_linked_device` |
@@ -85,6 +89,8 @@ workflow` before changing the corresponding domain.
85
89
  then supply only changed fields. Workflow/agent ids cannot be renamed through
86
90
  a save; change their display name instead. User skills may be renamed with
87
91
  `originalName` and `name`. Skill deletion is not exposed; disable it instead.
92
+ Built-in and plugin skills accept only `toolDependencies` edits (`null`
93
+ restores their declared dependencies).
88
94
  - Changing orchestration does not rewrite workflow instructions or agents.
89
95
 
90
96
  ## Providers and authentication
@@ -100,14 +106,26 @@ values through chat.
100
106
  and recovery. Its narrow status domain exposes installed models and progress;
101
107
  the detail dialog lists installed models rather than the download catalog.
102
108
  - Forgetting authentication is destructive and requires explicit approval.
109
+ It removes one OAuth account; a provider with several accounts requires
110
+ `accountId` from the account list in `status providers`.
111
+ - `set_provider_account` changes the selected account, automatic switching,
112
+ account order (every account id exactly once), or an account label. Sign-in
113
+ for a new account still happens in the Providers UI.
114
+ - `status developer` lists developer options. An option with a `warning` is
115
+ enabled only after the user explicitly accepts that warning, passed as
116
+ `riskAccepted:true`; never assume acceptance.
103
117
  - Usage sign-in is completed on the Usage or Providers surface.
104
118
 
105
119
  ## Profile, session, and Memory
106
120
 
107
121
  - Output style changes do not rewrite the current answer already in progress.
122
+ - Profile `language` and `experienceLevel` take ids listed by `status profile`;
123
+ unknown ids are rejected, and an empty `experienceLevel` clears it.
108
124
  - Auto-clear supports global/provider idle durations, `minContextPercent`,
109
- `reset`, and `resetProvider`. A reset and a duration are mutually exclusive;
110
- provider reset requires a provider.
125
+ `reset`, and `resetProvider`. A provider window overrides the global
126
+ duration; `provider: "default"` sets the fallback for providers without a
127
+ built-in window. A reset and a duration are mutually exclusive; provider
128
+ reset requires a provider.
111
129
  - Compaction supports `enabled` and either `mainBufferTokens` or
112
130
  `mainBufferPercent`. Changing representation replaces the previous budget.
113
131
  - If no supported action exposes a requested Memory interval, report it as
@@ -154,6 +172,8 @@ the Browser Use / Computer Use installed and enabled markers are read with
154
172
  - Environment/header maps are partial patches. Omitted keys are preserved;
155
173
  `null` explicitly removes one key, including a stored credential.
156
174
  - Plugin enablement moves its contributed skills and MCP integrations together.
175
+ - `enable_plugin_mcp` writes the MCP server a plugin ships and reconnects; it
176
+ fails for a plugin without an MCP script.
157
177
 
158
178
  ## Desktop, Projects, and automation
159
179
 
@@ -25,6 +25,9 @@ or requests a setting with no mutation action.
25
25
  | `usage` | Provider usage sign-in |
26
26
  | `doctor` | Runtime diagnostics |
27
27
  | `context` | Context and compaction |
28
+ | `developer` | Developer options (TUI: Developer picker) |
29
+ | `voice` | Voice built-in (TUI: Settings hub) |
30
+ | `connection` | Web-app pairing; Desktop only, the TUI shows guidance |
28
31
 
29
32
  An attached Desktop or TUI may navigate and return `opened:true`.
30
33
 
@@ -39,6 +42,7 @@ An attached Desktop or TUI may navigate and return `opened:true`.
39
42
  - Settings → Connection: web-app pairing and linked devices.
40
43
  - Settings → System: update, keep-awake, run in background after the window
41
44
  closes, and Doctor.
45
+ - Settings → Developer options: risk-gated OAuth providers.
42
46
  - Projects: Project registration, name, Project Memories, and Common Memory.
43
47
  - Workflows: workflow packs, Main and Web Search defaults, and agent editing.
44
48
  - Extensions → Plugin: Built-in cards and plugins.
@@ -50,13 +54,14 @@ the user to Desktop.
50
54
  ## Desktop-backed settings
51
55
 
52
56
  Setup can read and change Desktop appearance, keep-awake, run in background,
53
- usage pin, Computer observe-only, Browser/Computer/voice installation and
57
+ usage pin, activity rail pins, Computer observe-only, Browser/Computer/voice installation and
54
58
  toggles, Projects, and linked-device revocation through the attached
55
59
  Desktop. A receipt identifies `scope:desktop-host`; appearance is not applied
56
60
  to the paired browser. No Desktop claimant means no confirmed change.
57
61
 
58
- Computer observe-only has no Desktop settings control; setup is its only
59
- supported route. Usage pin is the pin toggle on the activity rail usage card.
62
+ Computer observe-only, background Memory cycles (`set_recap_enabled`), and
63
+ Browser/Computer first-use approval have no settings control in either app;
64
+ setup is their only supported route. Usage pin is the pin toggle on the activity rail usage card.
60
65
 
61
66
  Schedules/webhooks and workflow/agent/skill definitions use their runtime
62
67
  actions directly. These do not require clicking their Desktop rails.
@@ -20,6 +20,7 @@ import { featureEnvOverride, memoryToolsEnabled, moduleEnabled } from './config-
20
20
  import { readBridgeDiscovery } from '../../../bridge-discovery.mjs';
21
21
  import { HEADLESS_MODEL_TOOL_NAMES, HEADLESS_TOOL_PROFILE, normalizeToolProfile } from './tool-profile.mjs';
22
22
  import { DEFERRED_DEFAULT_LEAD_TOOLS } from './tool-catalog-data.mjs';
23
+ import { gitExecutablePresent } from '../../../shared/path-executable.mjs';
23
24
 
24
25
  // Browser Use / Computer Use have no install marker: the desktop app publishes
25
26
  // a loopback bridge discovery file while the feature is on. The same file
@@ -51,8 +52,13 @@ export function builtinFeatureActive(configLike, id) {
51
52
  (builtinInstalled(configLike, 'memory') && memoryToolsEnabled(configLike, true))
52
53
  );
53
54
  }
55
+ // The Git extension (GitHub tools, git-requiring skills) is install-gated;
56
+ // the local `git` command tool is not — see localGitToolsActive.
54
57
  if (id === 'git') {
55
- return localGitToolsActive(configLike);
58
+ return (
59
+ featureEnvOverride('MIXDOG_FEATURE_GIT') ??
60
+ (builtinInstalled(configLike, 'git') && moduleEnabled(configLike, 'git', true))
61
+ );
56
62
  }
57
63
  if (id === 'office') {
58
64
  return (
@@ -99,14 +105,14 @@ export function builtinFeatureActive(configLike, id) {
99
105
  * overrides), which the caller passes in. */
100
106
  export function featureDisallowedToolsFor(
101
107
  configLike,
102
- { browserAvailable = false, computerAvailable = false, toolProfile = 'interactive' } = {}
108
+ { browserAvailable = false, computerAvailable = false, toolProfile = 'interactive', gitAvailable } = {}
103
109
  ) {
104
110
  const browser = featureEnvOverride('MIXDOG_FEATURE_BROWSER') ?? browserAvailable === true;
105
111
  const computer = featureEnvOverride('MIXDOG_FEATURE_COMPUTER') ?? computerAvailable === true;
106
112
  const denied = [
107
113
  ...(builtinFeatureActive(configLike, 'webSearch') ? [] : ['web_search', 'web_fetch']),
108
114
  ...(builtinFeatureActive(configLike, 'memory') ? [] : ['memory', 'recall']),
109
- ...(localGitToolsActive(configLike, toolProfile) ? [] : ['git']),
115
+ ...(localGitToolsActive(configLike, { gitAvailable }) ? [] : ['git']),
110
116
  ...(builtinFeatureActive(configLike, 'git') ? [] : ['github']),
111
117
  ...(browser ? [] : ['browser', 'browser_devtools']),
112
118
  ...(computer ? [] : ['computer']),
@@ -130,12 +136,15 @@ export function builtinInstalled(configLike, id) {
130
136
  return configLike?.builtins?.[id]?.installed === true;
131
137
  }
132
138
 
133
- // The Git command tool needs no desktop extension installation in headless
134
- // runs. Existing feature overrides and explicit OFF preferences still apply.
135
- export function localGitToolsActive(configLike, toolProfile = 'interactive') {
139
+ // The Git command tool ships with the runtime and activates by itself wherever
140
+ // a git executable is on PATH — no extension install, on any tool profile, so
141
+ // interactive and headless sessions share one surface. Without the executable
142
+ // the schema would only cost tokens. Feature overrides and explicit OFF
143
+ // preferences still apply.
144
+ export function localGitToolsActive(configLike, { gitAvailable } = {}) {
136
145
  return (
137
146
  featureEnvOverride('MIXDOG_FEATURE_GIT') ??
138
- (moduleEnabled(configLike, 'git', true) && (toolProfile === 'headless' || builtinInstalled(configLike, 'git')))
147
+ (moduleEnabled(configLike, 'git', true) && (gitAvailable ?? gitExecutablePresent()))
139
148
  );
140
149
  }
141
150
 
@@ -227,10 +227,14 @@ export function resolveAgentTerminalReapMs(config, provider) {
227
227
  }
228
228
 
229
229
  // Resolve the effective auto-clear idle window for a config + provider:
230
- // an explicit user-set idleMs (config.autoClear.idleMs) always wins; else
231
- // fall back to the provider's default; else the global 1h default.
230
+ // a provider-specific override (autoClear.providerIdleMs[provider]) wins;
231
+ // else the global user-set idleMs (autoClear.idleMs); else the provider's
232
+ // built-in window, then providerIdleMs.default / the built-in 1h default.
232
233
  export function resolveAutoClearIdleMs(config, provider) {
233
234
  const raw = config?.autoClear && typeof config.autoClear === 'object' ? config.autoClear : {};
235
+ const key = clean(provider).toLowerCase();
236
+ const providerOverride = key && key !== 'default' ? normalizeAutoClearProviderIdleMs(raw.providerIdleMs)[key] : null;
237
+ if (providerOverride) return providerOverride;
234
238
  const idleMs = Number(raw.idleMs);
235
239
  if (Number.isFinite(idleMs) && idleMs > 0) return Math.max(60_000, Math.round(idleMs));
236
240
  return autoClearIdleMsForProvider(provider, raw.providerIdleMs);
@@ -70,9 +70,6 @@ export function sessionOriginFields(opts, { profile, presetObj, providerCacheOpt
70
70
  // scheduler/daily-standup, webhook/github-push, lead/worker.
71
71
  sourceType: opts.sourceType || null,
72
72
  sourceName: opts.sourceName || null,
73
- // Automation delivery mode ('app' | 'channel' | 'both'): the desktop
74
- // sidebar hides channel-only runner sessions from Automations.
75
- sourceDelivery: opts.sourceDelivery || null,
76
73
  // Provider-scoped unified cache key — one shard per provider,
77
74
  // shared across all roles / sources (agent/maintenance/mcp/
78
75
  // scheduler/webhook). Role or source-specific context must be
@@ -175,7 +175,6 @@ export function _sessionSummary(session) {
175
175
  agent: session.agent || null,
176
176
  sourceType: session.sourceType || null,
177
177
  sourceName: session.sourceName || null,
178
- sourceDelivery: session.sourceDelivery || null,
179
178
  scopeKey: session.scopeKey || null,
180
179
  parentSessionId: session.parentSessionId || null,
181
180
  ownerSessionId: session.ownerSessionId || session.parentSessionId || null,
@@ -225,7 +224,6 @@ function _normalizeSummaryRow(row) {
225
224
  agent: row.agent || null,
226
225
  sourceType: row.sourceType || null,
227
226
  sourceName: row.sourceName || null,
228
- sourceDelivery: row.sourceDelivery || null,
229
227
  scopeKey: row.scopeKey || null,
230
228
  parentSessionId: row.parentSessionId || null,
231
229
  ownerSessionId: row.ownerSessionId || row.parentSessionId || null,
@@ -17,7 +17,11 @@ import { readTopLevelLifecycleRecord, isLifecycleUnreadable } from './lifecycle-
17
17
  import { isAgentOnlySession, isRootLeadSession, sessionVisibility } from './store-summary-visibility.mjs';
18
18
  import { applySummaryLogText, readSummaryLogText } from './store-summary-log.mjs';
19
19
  import { createStoredTranscriptCache, nextProjectionStamp } from './store-transcript-cache.mjs';
20
- import { OFFLOAD_MIN_CHARS, projectStoredTranscriptOffThread } from './store-transcript-worker.mjs';
20
+ import {
21
+ OFFLOAD_MIN_CHARS,
22
+ projectStoredTranscriptOffThread,
23
+ storedTranscriptWorkerRunning,
24
+ } from './store-transcript-worker.mjs';
21
25
  import { projectStoredTranscript } from './store-transcript-projection.mjs';
22
26
  import { dataDir, sessionHeartbeatMtimes } from './store-summary-locations.mjs';
23
27
  import { desktopSession, isStoredSessionId, positiveNumber } from './store-summary-fields.mjs';
@@ -49,6 +53,12 @@ export function forgetStoredSessionTranscript(sessionId) {
49
53
  storedTranscriptCache.forget(`${String(sessionId || '').trim()}|`);
50
54
  }
51
55
 
56
+ /** Its last view left: keep the cold projections for a revisit, within the
57
+ * cache's idle budget. */
58
+ export function releaseStoredSessionTranscript(sessionId) {
59
+ storedTranscriptCache.release(`${String(sessionId || '').trim()}|`);
60
+ }
61
+
52
62
  /** Test/diagnostic seam: size of the retained cold projections. */
53
63
  export function storedTranscriptCacheStats() {
54
64
  return storedTranscriptCache.stats();
@@ -184,7 +194,6 @@ function normalizedRow(row, heartbeatAt = 0) {
184
194
  agent: row.agent || null,
185
195
  sourceType: row.sourceType || null,
186
196
  sourceName: row.sourceName || null,
187
- sourceDelivery: row.sourceDelivery || null,
188
197
  scopeKey: row.scopeKey || null,
189
198
  ownerSessionId: row.ownerSessionId || row.parentSessionId || null,
190
199
  visibility: sessionVisibility(row),
@@ -487,6 +496,8 @@ export async function readStoredSessionTranscript(id, options = {}) {
487
496
  // checkpoint skips recovery: an unreadable probe must not silently
488
497
  // downgrade an interrupted turn to a plain cold read.
489
498
  const startedAt = performance.now();
499
+ // Phase durations of the parse this call ran (absent on a cache hit).
500
+ const timing = {};
490
501
  const recordStat = probePath(recordPath);
491
502
  if (recordStat.state === PROBE_ABSENT) return readArchivedAgentResult(sessionId);
492
503
  if (recordStat.state !== PROBE_PRESENT) return null;
@@ -517,6 +528,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
517
528
  ].join(':'),
518
529
  fileStat: recordStat,
519
530
  loadText: async () => {
531
+ const readStartedAt = performance.now();
520
532
  const before = observeStamp(recordPath);
521
533
  let body = null;
522
534
  if (offloadable) {
@@ -529,6 +541,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
529
541
  }
530
542
  }
531
543
  body ||= readTextFile(recordPath);
544
+ timing.readMs = performance.now() - readStartedAt;
532
545
  readState = body.state;
533
546
  if (body.state !== PROBE_PRESENT) return null;
534
547
  readStamp = before && sameSessionStamp(before, observeStamp(recordPath)) ? before : null;
@@ -541,7 +554,11 @@ export async function readStoredSessionTranscript(id, options = {}) {
541
554
  // no parse to the load cache (a later resume reads the file itself).
542
555
  if (offloadable && text.length >= OFFLOAD_MIN_CHARS) {
543
556
  try {
557
+ timing.worker = storedTranscriptWorkerRunning() ? 'warm' : 'cold';
558
+ const workerStartedAt = performance.now();
544
559
  const answer = await projectStoredTranscriptOffThread({ sessionId, text, itemLimit });
560
+ timing.workerMs = performance.now() - workerStartedAt;
561
+ Object.assign(timing, answer.timing);
545
562
  if (answer.unreadable) return null;
546
563
  if (stamp) readCanonicalLifecycle.rememberStrictVerdict(recordPath, stamp, answer.lifecycle);
547
564
  // Stamps are process-unique per module realm; re-issue in this one.
@@ -551,7 +568,9 @@ export async function readStoredSessionTranscript(id, options = {}) {
551
568
  // Worker unavailable: fall through to the in-process pipeline.
552
569
  }
553
570
  }
571
+ const parseStartedAt = performance.now();
554
572
  const record = readTopLevelLifecycleRecord(text);
573
+ timing.parseMs = performance.now() - parseStartedAt;
555
574
  if (isLifecycleUnreadable(record) || record.id !== sessionId) return null;
556
575
  // The same strict verdict the lifecycle authority would reach for these
557
576
  // bytes (storedSessionExists runs next when the pane's runtime loads).
@@ -574,6 +593,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
574
593
  // a turn checkpoint the projection recovers through loadSession, the very
575
594
  // load the hand-off serves, and projects the load cache's session
576
595
  // exactly as before; only that projection is detached.
596
+ const projectStartedAt = performance.now();
577
597
  const value = await projectStoredTranscript(
578
598
  sessionId,
579
599
  handedOver && checkpointAbsent ? JSON.parse(text) : record.doc,
@@ -583,6 +603,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
583
603
  checkpointAbsent,
584
604
  }
585
605
  );
606
+ timing.projectMs = performance.now() - projectStartedAt;
586
607
  if (!handedOver || checkpointAbsent || !value || mode !== 'items') return value;
587
608
  try {
588
609
  return structuredClone(value);
@@ -601,6 +622,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
601
622
  ms: performance.now() - startedAt,
602
623
  chars: recordStat.size,
603
624
  items: Array.isArray(value?.items) ? value.items.length : 0,
625
+ timing,
604
626
  });
605
627
  }
606
628
  return value;
@@ -35,6 +35,10 @@ const SETTLED_FILE_AGE_MS = 2_500;
35
35
  // newcomer is served uncached.
36
36
  const ACTIVE_ENTRY_MS = 5_000;
37
37
  const GHOST_LIMIT = 256;
38
+ // Projections nobody is being served (a view that left, a hover/boot
39
+ // prefetch never opened) stay for a quick revisit only within this budget;
40
+ // the full budget above is for the views still refreshing.
41
+ const STORED_TRANSCRIPT_IDLE_MAX_BYTES = 4 * 1024 * 1024;
38
42
 
39
43
  function sameFileStat(left, right) {
40
44
  return (
@@ -60,6 +64,7 @@ export function createStoredTranscriptCache({
60
64
  // LRU reparsed every unchanged record when nine visible sessions refreshed.
61
65
  maxEntries = Number.POSITIVE_INFINITY,
62
66
  maxBytes = STORED_TRANSCRIPT_CACHE_MAX_BYTES,
67
+ maxIdleBytes = STORED_TRANSCRIPT_IDLE_MAX_BYTES,
63
68
  } = {}) {
64
69
  // Retain a content digest, not a second full transcript beside its projection.
65
70
  // The bound is the size of what is actually retained: the projection.
@@ -126,6 +131,17 @@ export function createStoredTranscriptCache({
126
131
  entries.delete(key);
127
132
  entries.set(key, entry);
128
133
  };
134
+ // Oldest idle entries go first until the idle ones fit their own budget.
135
+ const trimIdle = (now) => {
136
+ let idleBytes = 0;
137
+ for (const entry of entries.values()) if (now - entry.usedAt > ACTIVE_ENTRY_MS) idleBytes += entry.bytes;
138
+ for (const [key, entry] of [...entries]) {
139
+ if (idleBytes <= maxIdleBytes) break;
140
+ if (now - entry.usedAt <= ACTIVE_ENTRY_MS) continue;
141
+ idleBytes -= entry.bytes;
142
+ drop(key);
143
+ }
144
+ };
129
145
 
130
146
  return {
131
147
  /** The cached projection for this exact content, or a fresh one from
@@ -133,6 +149,7 @@ export function createStoredTranscriptCache({
133
149
  * the entry. Concurrent callers with the same content share one parse.
134
150
  * Entries naming the same `group` replace each other. */
135
151
  async read({ key, group = null, fingerprint, fileStat = null, loadText, produce, now = Date.now() }) {
152
+ trimIdle(now);
136
153
  const cached = entries.get(key);
137
154
  if (
138
155
  cached &&
@@ -181,6 +198,14 @@ export function createStoredTranscriptCache({
181
198
  if (inFlight.get(key) === record) inFlight.delete(key);
182
199
  }
183
200
  },
201
+ /** The last view of these projections left: they turn idle now and stay
202
+ * only within the idle budget, ready for a revisit. */
203
+ release(keyPrefix, now = Date.now()) {
204
+ for (const [key, entry] of entries) {
205
+ if (key.startsWith(keyPrefix)) entry.usedAt = Math.min(entry.usedAt, now - ACTIVE_ENTRY_MS - 1);
206
+ }
207
+ trimIdle(now);
208
+ },
184
209
  forget(keyPrefix) {
185
210
  for (const key of inFlight.keys()) {
186
211
  if (key.startsWith(keyPrefix)) inFlight.delete(key);
@@ -36,19 +36,32 @@ export function projectStoredTranscriptOffThread({ sessionId, text, itemLimit })
36
36
  return client({ sessionId, text, itemLimit });
37
37
  }
38
38
 
39
+ /** Whether a projection worker is attached (a request now skips its start). */
40
+ export function storedTranscriptWorkerRunning() {
41
+ return client?.running() === true;
42
+ }
43
+
39
44
  async function project({ sessionId, text, itemLimit }) {
45
+ const importStartedAt = performance.now();
40
46
  const [{ readTopLevelLifecycleRecord, isLifecycleUnreadable }, { projectStoredTranscript }] = await Promise.all([
41
47
  import('./lifecycle-scan.mjs'),
42
48
  import('./store-transcript-projection.mjs'),
43
49
  ]);
50
+ const parseStartedAt = performance.now();
44
51
  const record = readTopLevelLifecycleRecord(text);
45
52
  if (isLifecycleUnreadable(record) || record.id !== sessionId) return { unreadable: true };
53
+ const projectStartedAt = performance.now();
46
54
  const value = await projectStoredTranscript(sessionId, record.doc, {
47
55
  itemLimit,
48
56
  includeMessages: false,
49
57
  checkpointAbsent: true,
50
58
  });
51
- return { value, lifecycle: { id: record.id, closed: record.closed, generation: record.generation } };
59
+ const timing = {
60
+ importMs: parseStartedAt - importStartedAt,
61
+ parseMs: projectStartedAt - parseStartedAt,
62
+ projectMs: performance.now() - projectStartedAt,
63
+ };
64
+ return { value, lifecycle: { id: record.id, closed: record.closed, generation: record.generation }, timing };
52
65
  }
53
66
 
54
67
  if (!isMainThread && workerData?.kind === WORKER_KIND) serveWorkerRequests(project);
@@ -1,51 +1,9 @@
1
- import { accessSync, constants as fsConstants, readdirSync, readFileSync, statSync } from 'node:fs';
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { readdir as readdirAsync } from 'node:fs/promises';
4
4
  import { runGitOffThread } from '../../../../shared/git-runner.mjs';
5
- import {
6
- delimiter as pathDelimiter,
7
- dirname as pathDirname,
8
- join as pathJoin,
9
- resolve as pathResolveAbsolute,
10
- } from 'node:path';
11
-
12
- function _pathDirectory(value) {
13
- const text = String(value || '').trim();
14
- return text.length >= 2 && text.startsWith('"') && text.endsWith('"') ? text.slice(1, -1) : text;
15
- }
16
-
17
- function _executableNames(name, platform) {
18
- const text = String(name || '').trim();
19
- if (!text) return [];
20
- if (platform !== 'win32' || /\.(?:exe|cmd|bat|com)$/i.test(text)) return [text];
21
- return [text, `${text}.exe`, `${text}.cmd`, `${text}.bat`, `${text}.com`];
22
- }
23
-
24
- function findPathExecutable(
25
- name,
26
- { pathValue = process.env.PATH || '', platform = process.platform, maxDirectories = 64 } = {}
27
- ) {
28
- const names = _executableNames(name, platform);
29
- if (!names.length) return null;
30
- const seenDirectories = new Set();
31
- for (const rawDirectory of String(pathValue).split(pathDelimiter)) {
32
- const directory = _pathDirectory(rawDirectory);
33
- if (!directory) continue;
34
- const key = platform === 'win32' ? directory.toLowerCase() : directory;
35
- if (seenDirectories.has(key)) continue;
36
- seenDirectories.add(key);
37
- if (seenDirectories.size > maxDirectories) break;
38
- for (const executable of names) {
39
- const file = pathJoin(directory, executable);
40
- try {
41
- if (!statSync(file).isFile()) continue;
42
- accessSync(file, platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK);
43
- return `${executable} (${directory.replace(/\\/g, '/')})`;
44
- } catch {}
45
- }
46
- }
47
- return null;
48
- }
5
+ import { findPathExecutable } from '../../../../shared/path-executable.mjs';
6
+ import { dirname as pathDirname, join as pathJoin, resolve as pathResolveAbsolute } from 'node:path';
49
7
 
50
8
  // Startup shell-tool line. An earlier inventory was dropped because it was
51
9
  // measured in this process's PATH while commands run under the login-shell
@@ -53,7 +53,6 @@ async function loadConfig() {
53
53
  return applyDefaults({
54
54
  ...DEFAULT_CONFIG,
55
55
  ...raw,
56
- channelId: '',
57
56
  access: {
58
57
  ...DEFAULT_ACCESS,
59
58
  ...(raw.access || {}),
@@ -26,7 +26,7 @@ export function createConfigReloader({
26
26
  // startAutomationRuntime() is already past its automationRunning guard, so
27
27
  // nobody would call scheduler.start() again and every saved/edited schedule
28
28
  // stayed silently disarmed until the daemon restarted.
29
- scheduler.reloadConfig(getConfig().nonInteractive ?? [], getConfig().interactive ?? [], getConfig().channelId, {
29
+ scheduler.reloadConfig(getConfig().nonInteractive ?? [], getConfig().interactive ?? [], {
30
30
  restart: state.automationRunning || getBridgeRuntimeConnected(),
31
31
  });
32
32
  }