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.
- package/README.md +206 -475
- package/package.json +11 -4
- package/src/defaults/skills/setup/references/actions.md +26 -6
- package/src/defaults/skills/setup/references/surfaces.md +8 -3
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +16 -7
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +0 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +24 -2
- package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +3 -45
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +1 -1
- package/src/runtime/channels/lib/scheduler.mjs +15 -56
- package/src/runtime/channels/lib/worker-main.mjs +2 -9
- package/src/runtime/shared/config.mjs +66 -1
- package/src/runtime/shared/llm/usage-ledger-quota.mjs +13 -2
- package/src/runtime/shared/path-executable.mjs +58 -0
- package/src/session-runtime/boot/apis.mjs +6 -2
- package/src/session-runtime/boot/begin.mjs +0 -2
- package/src/session-runtime/boot/providers.mjs +3 -0
- package/src/session-runtime/boot/tools.mjs +0 -1
- package/src/session-runtime/config-lifecycle.mjs +11 -0
- package/src/session-runtime/internal-tool-executor.mjs +0 -2
- package/src/session-runtime/runtime-feature-gates.mjs +2 -3
- package/src/session-runtime/schedule-session-run.mjs +0 -1
- package/src/session-runtime/services/channel-admin.mjs +8 -26
- package/src/session-runtime/settings-compaction-api.mjs +5 -1
- package/src/session-runtime/settings-system-api.mjs +2 -20
- package/src/session-runtime/setup-tool/executor.mjs +61 -3
- package/src/session-runtime/setup-tool/extended-actions.mjs +31 -6
- package/src/session-runtime/setup-tool/settings-contract.mjs +27 -3
- package/src/session-runtime/setup-tool/tool-defs.mjs +35 -5
- package/src/session-runtime/webhook-session-run.mjs +0 -2
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +13 -3
- package/src/standalone/daemon-stored-session-views.mjs +4 -0
- package/src/standalone/session-protocol.mjs +0 -1
- package/src/standalone/session-service/session-calls/session-views.mjs +10 -4
- package/src/standalone/session-service/stored-reader.mjs +20 -2
- package/src/standalone/session-service/viewers.mjs +3 -3
- package/src/standalone/session-service.mjs +3 -1
- package/src/tui/App.jsx +6 -1
- package/src/tui/app/create-app-pickers.mjs +1 -0
- package/src/tui/app/doctor.mjs +6 -14
- package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +8 -3
- package/src/tui/app/settings-picker/settings-rows.mjs +5 -1
- package/src/tui/app/use-ui-open-request.mjs +13 -3
- package/src/tui/dist/index.mjs +41 -9
- 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.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "
|
|
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,
|
|
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
|
|
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
|
|
110
|
-
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
|
|
59
|
-
|
|
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
|
|
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,
|
|
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
|
|
134
|
-
//
|
|
135
|
-
|
|
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) && (
|
|
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
|
-
//
|
|
231
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
@@ -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 ?? [],
|
|
29
|
+
scheduler.reloadConfig(getConfig().nonInteractive ?? [], getConfig().interactive ?? [], {
|
|
30
30
|
restart: state.automationRunning || getBridgeRuntimeConnected(),
|
|
31
31
|
});
|
|
32
32
|
}
|