mixdog 1.1.0 → 1.1.2
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 +13 -6
- package/package.json +1 -2
- package/scripts/latest-guard.mjs +63 -0
- package/scripts/lib/run-node-tests.mjs +103 -27
- package/scripts/lib/test-failure-summary.mjs +7 -2
- package/scripts/merge-mac-update-feed.mjs +107 -0
- package/scripts/mobile-release.mjs +210 -0
- package/scripts/post-release-check.mjs +353 -0
- package/scripts/publish-asset-release.mjs +43 -0
- package/scripts/release-guard.mjs +63 -0
- package/scripts/release-paths.mjs +39 -2
- package/scripts/test.mjs +29 -7
- package/src/defaults/shipped-history.json +2 -0
- package/src/defaults/skills/computer-use/SKILL.md +1 -1
- package/src/defaults/skills/setup/references/actions.md +1 -0
- package/src/headless-role.mjs +2 -2
- package/src/lib/keychain-cjs.cjs +582 -17
- package/src/lib/rules-builder.cjs +0 -23
- package/src/rules/lead/LEAD.md +2 -2
- package/src/runtime/agent/orchestrator/config-storage.mjs +32 -4
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/context/instruction-files.mjs +130 -0
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/lib/oauth-token-utils.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/lib/open-login-url.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +254 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +218 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +239 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +233 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-login.mjs +247 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +225 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +542 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +14 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +99 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +4 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -7
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +4 -0
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +15 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +1 -5
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +12 -14
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-call-ids.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +19 -11
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +1 -5
- package/src/runtime/agent/orchestrator/session/read-dedup.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/tool-result-status.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/turn-effort.mjs +18 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +11 -8
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +9 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +245 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/command-preflight.mjs +13 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/result-format.mjs +49 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -7
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +61 -8
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +28 -3
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +73 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +17 -6
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +23 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +93 -43
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +32 -6
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +12 -15
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +124 -91
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +19 -4
- package/src/runtime/channels/index.mjs +1 -5
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -23
- package/src/runtime/channels/lib/tool-dispatch.mjs +1 -13
- package/src/runtime/channels/lib/worker-ipc.mjs +2 -2
- package/src/runtime/channels/lib/worker-main.mjs +4 -34
- package/src/runtime/computer-bridge/client.mjs +25 -1
- package/src/runtime/computer-bridge/linux-action-capabilities.mjs +84 -0
- package/src/runtime/computer-bridge/linux-readiness.d.mts +52 -0
- package/src/runtime/computer-bridge/linux-readiness.mjs +411 -0
- package/src/runtime/computer-bridge/test-linux-display.mjs +5 -0
- package/src/runtime/effort-judge/compact-tokenizer.mjs +13 -8
- package/src/runtime/effort-judge/judge-input.mjs +5 -2
- package/src/runtime/effort-judge/model-install.mjs +9 -3
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/memory/lib/embedding-worker.mjs +9 -3
- package/src/runtime/memory/lib/pg/supervisor.mjs +3 -0
- package/src/runtime/shared/config.mjs +50 -2
- package/src/runtime/shared/developer-options.mjs +7 -0
- package/src/runtime/shared/instruction-files-mode.mjs +9 -0
- package/src/runtime/shared/llm/model-catalog.mjs +2 -1
- package/src/runtime/shared/llm/openai-daybreak.mjs +79 -0
- package/src/runtime/shared/pristine-execution-contract.json +9 -0
- package/src/runtime/shared/pristine-execution.mjs +1 -1
- package/src/runtime/shared/provider-accounts.mjs +2 -0
- package/src/runtime/shared/resource-admission.mjs +10 -3
- package/src/runtime/shared/schedule-model-ref.mjs +11 -0
- package/src/runtime/shared/semver.d.mts +1 -0
- package/src/runtime/shared/semver.mjs +57 -0
- package/src/runtime/shared/session-approval-wait.mjs +35 -0
- package/src/runtime/shared/shell-display.mjs +4 -2
- package/src/runtime/shared/tool-surface.mjs +2 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +11 -10
- package/src/runtime/shared/update-checker.mjs +2 -31
- package/src/session-runtime/boot/apis.mjs +5 -0
- package/src/session-runtime/boot/begin.mjs +0 -2
- package/src/session-runtime/boot/tools.mjs +5 -4
- package/src/session-runtime/model-recency.mjs +3 -0
- package/src/session-runtime/provider-auth/login.mjs +10 -10
- package/src/session-runtime/provider-auth/setup.mjs +22 -2
- package/src/session-runtime/provider-models/row-hydration.mjs +12 -4
- package/src/session-runtime/provider-readiness.mjs +4 -0
- package/src/session-runtime/resource-mcp-api.mjs +7 -3
- package/src/session-runtime/runtime-bootstrap.mjs +0 -4
- package/src/session-runtime/runtime-facade.mjs +5 -0
- package/src/session-runtime/runtime-paths.mjs +0 -1
- package/src/session-runtime/schedule-session-run.mjs +3 -1
- package/src/session-runtime/services/agent-tool/index-write-queue.mjs +55 -14
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +15 -3
- package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +2 -0
- package/src/session-runtime/services/agent-tool/worker-index/approval-wait.mjs +97 -0
- package/src/session-runtime/services/agent-tool/worker-index/mutation-batch.mjs +4 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -2
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +3 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-store.mjs +58 -2
- package/src/session-runtime/services/agent-tool/worker-index.mjs +69 -15
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +2 -0
- package/src/session-runtime/services/channel-admin.mjs +4 -2
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +80 -17
- package/src/session-runtime/services/seeds.mjs +7 -10
- package/src/session-runtime/services/usage-dashboard-model.mjs +6 -0
- package/src/session-runtime/settings-builtin-tools-api.mjs +15 -0
- package/src/session-runtime/settings-profile-api.mjs +27 -2
- package/src/session-runtime/setup-tool/executor.mjs +5 -0
- package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
- package/src/session-runtime/starter-guide-state.mjs +62 -0
- package/src/session-runtime/tool-policy-refresh.mjs +3 -0
- package/src/session-runtime/webhook-session-run.mjs +3 -1
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -2
- package/src/standalone/channel-binding.mjs +1 -2
- package/src/standalone/channel-transport/call-dispatch.mjs +4 -10
- package/src/standalone/daemon-channels-loader.mjs +2 -3
- package/src/standalone/local-session-runtime.mjs +2 -78
- package/src/standalone/session-protocol.mjs +2 -0
- package/src/tui/app/model-options.mjs +13 -1
- package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +10 -4
- package/src/tui/app/provider-setup/oauth-actions.mjs +5 -0
- package/src/tui/dist/index.mjs +84 -34
- package/src/tui/session/context-state.mjs +2 -0
- package/src/tui/session/live-share/owner.mjs +17 -0
- package/src/tui/session/oauth-flows.mjs +2 -0
- package/src/tui/session/session-api/extensions.mjs +12 -8
- package/src/tui/session/session-api/integrations.mjs +9 -8
- package/src/tui/session/session-api/routes.mjs +2 -2
- package/src/tui/session/session-api/settings.mjs +1 -0
- package/src/tui/session/session-api/shared.mjs +10 -6
- package/src/tui/session/tool-card-results/grouped-result.mjs +11 -2
- package/src/tui/session/transcript-spill/spill-dir.mjs +22 -6
- package/src/tui/spinner-meta.mjs +26 -10
- package/src/runtime/channels/tool-defs.mjs +0 -17
|
@@ -113,10 +113,14 @@ export function createMcpResourceApi({ deps, sync, decorate }) {
|
|
|
113
113
|
},
|
|
114
114
|
async saveMcpServer(input = {}) {
|
|
115
115
|
const servers = mcpServersOf(getConfig());
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
// An explicit originalName must exist; a bare name edits a matching
|
|
117
|
+
// server and otherwise creates a new one.
|
|
118
|
+
const explicitOriginal = clean(input.originalName);
|
|
119
|
+
if (explicitOriginal && !Object.hasOwn(servers, explicitOriginal)) {
|
|
120
|
+
throw new Error(`MCP server not configured: ${explicitOriginal}`);
|
|
119
121
|
}
|
|
122
|
+
const bareName = clean(input.name);
|
|
123
|
+
const originalName = explicitOriginal || (Object.hasOwn(servers, bareName) ? bareName : '');
|
|
120
124
|
const existing = originalName ? servers[originalName] : {};
|
|
121
125
|
const normalizedInput = normalizeMcpServerInput(mergedServerInput(input, existing, originalName));
|
|
122
126
|
const name = originalName && clean(input.name) === originalName ? originalName : normalizedInput.name;
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
RUNTIME,
|
|
11
11
|
WEB_SEARCH_TOOL_DEFS,
|
|
12
12
|
MEMORY_TOOL_DEFS,
|
|
13
|
-
CHANNEL_TOOL_DEFS,
|
|
14
13
|
CODE_GRAPH_TOOL_DEFS,
|
|
15
14
|
STATUSLINE_SESSION_ROUTES,
|
|
16
15
|
STANDALONE_ROOT,
|
|
@@ -49,7 +48,6 @@ export async function loadRuntimeModules() {
|
|
|
49
48
|
statusRoutes,
|
|
50
49
|
webSearchToolDefs,
|
|
51
50
|
memoryToolDefs,
|
|
52
|
-
channelToolDefs,
|
|
53
51
|
codeGraphToolDefs,
|
|
54
52
|
] = await Promise.all([
|
|
55
53
|
profiledImport('config', `${RUNTIME}/config.mjs`),
|
|
@@ -62,7 +60,6 @@ export async function loadRuntimeModules() {
|
|
|
62
60
|
profiledImport('status-routes', STATUSLINE_SESSION_ROUTES, { optional: true }),
|
|
63
61
|
profiledImport('web-search-tool-defs', WEB_SEARCH_TOOL_DEFS, { optional: true }),
|
|
64
62
|
profiledImport('memory-tool-defs', MEMORY_TOOL_DEFS, { optional: true }),
|
|
65
|
-
profiledImport('channel-tool-defs', CHANNEL_TOOL_DEFS, { optional: true }),
|
|
66
63
|
profiledImport('code-graph-tool-defs', CODE_GRAPH_TOOL_DEFS, { optional: true }),
|
|
67
64
|
]);
|
|
68
65
|
bootProfile('imports:ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
@@ -88,7 +85,6 @@ export async function loadRuntimeModules() {
|
|
|
88
85
|
statusRoutes,
|
|
89
86
|
webSearchToolDefs,
|
|
90
87
|
memoryToolDefs,
|
|
91
|
-
channelToolDefs,
|
|
92
88
|
codeGraphToolDefs,
|
|
93
89
|
};
|
|
94
90
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentTurnEffort } from '../runtime/agent/orchestrator/session/turn-effort.mjs';
|
|
1
2
|
import { SESSION_ID_PATTERN } from '../runtime/agent/orchestrator/runtime-core/session-id.mjs';
|
|
2
3
|
import { SessionAddressError, bindSessionAddress } from './session-address.mjs';
|
|
3
4
|
|
|
@@ -79,6 +80,10 @@ export function createRuntimeFacade({
|
|
|
79
80
|
get modelParameters() {
|
|
80
81
|
return state.route.modelParameters || {};
|
|
81
82
|
},
|
|
83
|
+
// The effort the runtime resolved for the turn in flight (Auto judge), or null.
|
|
84
|
+
get turnEffort() {
|
|
85
|
+
return currentTurnEffort(state.session);
|
|
86
|
+
},
|
|
82
87
|
get effortOptions() {
|
|
83
88
|
return state.route.effortOptions || [];
|
|
84
89
|
},
|
|
@@ -6,7 +6,6 @@ export const RUNTIME = '../runtime/agent/orchestrator';
|
|
|
6
6
|
export const WEB_SEARCH_RUNTIME = '../runtime/web-search/index.mjs';
|
|
7
7
|
export const WEB_SEARCH_TOOL_DEFS = '../runtime/web-search/tool-defs.mjs';
|
|
8
8
|
export const MEMORY_TOOL_DEFS = '../runtime/memory/tool-defs.mjs';
|
|
9
|
-
export const CHANNEL_TOOL_DEFS = '../runtime/channels/tool-defs.mjs';
|
|
10
9
|
export const CODE_GRAPH_TOOL_DEFS = '../runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs';
|
|
11
10
|
export const CODE_GRAPH_RUNTIME = '../runtime/agent/orchestrator/tools/code-graph.mjs';
|
|
12
11
|
export const STATUSLINE_SESSION_ROUTES = '../vendor/statusline/src/gateway/session-routes.mjs';
|
|
@@ -10,6 +10,7 @@ import { loadConfig } from '../runtime/agent/orchestrator/config.mjs';
|
|
|
10
10
|
import { createSession } from '../runtime/agent/orchestrator/session/manager/session-lifecycle.mjs';
|
|
11
11
|
import { askSession } from '../runtime/agent/orchestrator/session/manager/ask-session.mjs';
|
|
12
12
|
import { modelRouteFields, parseScheduleModelRef } from '../runtime/shared/schedule-model-ref.mjs';
|
|
13
|
+
import { migrateCodexRoute } from '../runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs';
|
|
13
14
|
import { automationWorkflowOpts } from './automation-workflow.mjs';
|
|
14
15
|
import { runAutomationTurns } from './automation-agents.mjs';
|
|
15
16
|
import { automationPromptContent } from '../runtime/shared/automation-attachments.mjs';
|
|
@@ -17,7 +18,8 @@ import { automationPromptContent } from '../runtime/shared/automation-attachment
|
|
|
17
18
|
/** Resolve schedule.model into a {provider,model,effort?,fast?} route. */
|
|
18
19
|
function scheduleRouteFromModelRef(modelRef, config = null) {
|
|
19
20
|
const ref = parseScheduleModelRef(modelRef);
|
|
20
|
-
|
|
21
|
+
// A model the provider catalog superseded runs on its successor.
|
|
22
|
+
if (ref && typeof ref === 'object') return migrateCodexRoute(ref);
|
|
21
23
|
const cfg = config || loadConfig({ secrets: false });
|
|
22
24
|
const preset = cfg.presets?.find((p) => p.id === ref || p.name === ref);
|
|
23
25
|
if (!preset) {
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
// the mutator: an async job replays those effects onto the fresh on-disk
|
|
11
11
|
// document under the cross-process lock, so concurrent writers of other keys
|
|
12
12
|
// are merged, never overwritten. Jobs run strictly one at a time in FIFO order.
|
|
13
|
+
//
|
|
14
|
+
// write(mutator, { rerun: true }) is the conditional-update form: the mutator
|
|
15
|
+
// itself is queued and re-run against the locked fresh document, in order with
|
|
16
|
+
// the recorded effects, so a field-level update re-checks its condition against
|
|
17
|
+
// what is really on disk instead of replacing a whole value it once saw.
|
|
13
18
|
import { updateJsonAtomic, updateJsonAtomicSync } from '../../../runtime/shared/atomic-file.mjs';
|
|
14
19
|
import { registerJsonProjection } from '../../../runtime/shared/pending-json-projection.mjs';
|
|
15
20
|
import { sleep } from '../../../runtime/shared/sleep.mjs';
|
|
@@ -85,21 +90,40 @@ function compactOps(batch) {
|
|
|
85
90
|
});
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
function replayOps(ops, args) {
|
|
93
|
+
function replayOps(ops, args, mergeSet) {
|
|
89
94
|
args.forEach((arg, index) => {
|
|
90
95
|
const op = ops[index];
|
|
91
96
|
if (!op) return;
|
|
92
97
|
if (arg instanceof Map) {
|
|
93
98
|
for (const key of op.del) arg.delete(key);
|
|
94
|
-
for (const [key, json] of op.set) arg.set(key, JSON.parse(json));
|
|
99
|
+
for (const [key, json] of op.set) arg.set(key, mergeSet(index, arg.get(key), JSON.parse(json)));
|
|
95
100
|
} else if (arg instanceof Set) {
|
|
96
101
|
for (const value of op.add) arg.add(value);
|
|
97
102
|
}
|
|
98
103
|
});
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
/** Entries are recorded op sets or re-run mutators; runs of op sets between
|
|
107
|
+
* re-run mutators compact, and every entry keeps its place in the order. */
|
|
108
|
+
function replayBatch(batch, args, mergeSet) {
|
|
109
|
+
let run = [];
|
|
110
|
+
const replayRun = () => {
|
|
111
|
+
if (run.length) replayOps(compactOps(run), args, mergeSet);
|
|
112
|
+
run = [];
|
|
113
|
+
};
|
|
114
|
+
for (const entry of batch) {
|
|
115
|
+
if (typeof entry !== 'function') {
|
|
116
|
+
run.push(entry);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
replayRun();
|
|
120
|
+
entry(...args);
|
|
121
|
+
}
|
|
122
|
+
replayRun();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function reportFailure(file, what, error) {
|
|
126
|
+
process.stderr.write(`[index-write] ${file}: ${what}: ${error?.message || error}\n`);
|
|
103
127
|
}
|
|
104
128
|
|
|
105
129
|
/**
|
|
@@ -109,8 +133,17 @@ function replayBatch(batch, args) {
|
|
|
109
133
|
* builds the next document from a raw one; `mutator` receives the keyed collections
|
|
110
134
|
* @param {() => any} options.readDoc fresh raw document from disk (sync read)
|
|
111
135
|
* @param {() => void} [options.onPersisted] the file changed on disk
|
|
136
|
+
* @param {(index: number, prev: any, next: any) => any} [options.mergeSet]
|
|
137
|
+
* value a recorded set leaves in Map argument `index`, given the locked
|
|
138
|
+
* document's current value there; default: the recorded value
|
|
112
139
|
*/
|
|
113
|
-
export function createIndexWriteQueue({
|
|
140
|
+
export function createIndexWriteQueue({
|
|
141
|
+
file,
|
|
142
|
+
rewrite,
|
|
143
|
+
readDoc,
|
|
144
|
+
onPersisted = () => {},
|
|
145
|
+
mergeSet = (_index, _prev, next) => next,
|
|
146
|
+
}) {
|
|
114
147
|
let projection = null; // raw document including unpersisted ops, else null
|
|
115
148
|
const self = { flush, flushSync: () => flushSync() };
|
|
116
149
|
if (file) registerJsonProjection(file, () => projection);
|
|
@@ -134,10 +167,15 @@ export function createIndexWriteQueue({ file, rewrite, readDoc, onPersisted = ()
|
|
|
134
167
|
async function persist(batch) {
|
|
135
168
|
for (let attempt = 0; ; attempt += 1) {
|
|
136
169
|
try {
|
|
137
|
-
await updateJsonAtomic(file, (cur) => rewrite(cur, (...args) => replayBatch(batch, args)), {
|
|
170
|
+
await updateJsonAtomic(file, (cur) => rewrite(cur, (...args) => replayBatch(batch, args, mergeSet)), {
|
|
171
|
+
lock: true,
|
|
172
|
+
});
|
|
138
173
|
return;
|
|
139
|
-
} catch {
|
|
140
|
-
if (attempt >= RETRY_DELAYS_MS.length)
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (attempt >= RETRY_DELAYS_MS.length) {
|
|
176
|
+
reportFailure(file, `dropped ${batch.length} unpersisted write(s)`, error);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
141
179
|
await sleep(RETRY_DELAYS_MS[attempt]);
|
|
142
180
|
}
|
|
143
181
|
}
|
|
@@ -181,11 +219,12 @@ export function createIndexWriteQueue({ file, rewrite, readDoc, onPersisted = ()
|
|
|
181
219
|
clearTimer();
|
|
182
220
|
pending = [];
|
|
183
221
|
try {
|
|
184
|
-
updateJsonAtomicSync(file, (cur) => rewrite(cur, (...args) => replayBatch(ops, args)), {
|
|
222
|
+
updateJsonAtomicSync(file, (cur) => rewrite(cur, (...args) => replayBatch(ops, args, mergeSet)), {
|
|
185
223
|
lock: true,
|
|
186
224
|
timeoutMs: EXIT_LOCK_TIMEOUT_MS,
|
|
187
225
|
});
|
|
188
|
-
} catch {
|
|
226
|
+
} catch (error) {
|
|
227
|
+
reportFailure(file, `synchronous flush of ${ops.length} write(s) failed; kept queued`, error);
|
|
189
228
|
pending = ops;
|
|
190
229
|
return false;
|
|
191
230
|
}
|
|
@@ -197,18 +236,20 @@ export function createIndexWriteQueue({ file, rewrite, readDoc, onPersisted = ()
|
|
|
197
236
|
}
|
|
198
237
|
|
|
199
238
|
return {
|
|
200
|
-
/** Run `mutator` now against the projection; persist its effect later
|
|
239
|
+
/** Run `mutator` now against the projection; persist its effect later
|
|
240
|
+
* (`rerun`: persist by re-running it on the locked document instead).
|
|
201
241
|
* Returns the projected document, or null (no file / mutator threw). */
|
|
202
|
-
write(mutator) {
|
|
242
|
+
write(mutator, { rerun = false } = {}) {
|
|
203
243
|
if (!file || typeof mutator !== 'function') return null;
|
|
204
244
|
try {
|
|
205
245
|
let ops = null;
|
|
206
246
|
const next = rewrite(projection ?? readDoc(), (...args) => {
|
|
207
|
-
|
|
247
|
+
if (rerun) mutator(...args);
|
|
248
|
+
else ops = recordOps(args, () => mutator(...args));
|
|
208
249
|
});
|
|
209
250
|
projection = next;
|
|
210
251
|
version += 1;
|
|
211
|
-
pending.push(ops);
|
|
252
|
+
pending.push(rerun ? mutator : ops);
|
|
212
253
|
unsettled.add(self);
|
|
213
254
|
schedule();
|
|
214
255
|
return next;
|
|
@@ -19,12 +19,15 @@ export function isActiveWorkerRow(row = {}) {
|
|
|
19
19
|
return !isTerminalWorkerStatus(effective);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function agentRow({ tag, session, runtime, progress, now }) {
|
|
22
|
+
function agentRow({ tag, session, runtime, progress, now, indexRow = null }) {
|
|
23
23
|
const status = session.closed === true ? 'closed' : session.status || 'idle';
|
|
24
24
|
const stage =
|
|
25
25
|
session.stage ||
|
|
26
26
|
(status === 'idle' || status === 'error' || status === 'closed' ? status : runtime?.stage || status);
|
|
27
|
-
|
|
27
|
+
// Additive: the current turn's start (worker index, stamped at dispatch) so
|
|
28
|
+
// surfaces time a continued worker from its turn, not from createdAt.
|
|
29
|
+
const turnStartedAt = indexRow?.turnStartedAt || session.turnStartedAt || null;
|
|
30
|
+
const row = {
|
|
28
31
|
tag,
|
|
29
32
|
sessionId: session.id,
|
|
30
33
|
agent: session.agent || null,
|
|
@@ -33,8 +36,14 @@ function agentRow({ tag, session, runtime, progress, now }) {
|
|
|
33
36
|
preset: session.presetName || null,
|
|
34
37
|
effort: session.effort || null,
|
|
35
38
|
fast: session.fast === true,
|
|
39
|
+
// Additive: route model parameters (e.g. Daybreak Blue) when the route has any.
|
|
40
|
+
...(session.modelParameters && Object.keys(session.modelParameters).length
|
|
41
|
+
? { modelParameters: { ...session.modelParameters } }
|
|
42
|
+
: {}),
|
|
36
43
|
status,
|
|
37
44
|
stage,
|
|
45
|
+
// Additive: present only while the worker's turn waits on a user approval.
|
|
46
|
+
...(runtime?.awaitingUser ? { awaitingUser: runtime.awaitingUser } : {}),
|
|
38
47
|
...progress,
|
|
39
48
|
createdAt: session.createdAt || null,
|
|
40
49
|
updatedAt: session.updatedAt || null,
|
|
@@ -51,16 +60,19 @@ function agentRow({ tag, session, runtime, progress, now }) {
|
|
|
51
60
|
: Math.max(0, Number(session.messageCount || 0)),
|
|
52
61
|
tools: Array.isArray(session.tools) ? session.tools.length : Math.max(0, Number(session.toolCount || 0)),
|
|
53
62
|
};
|
|
63
|
+
if (turnStartedAt && isActiveWorkerRow(row)) row.turnStartedAt = turnStartedAt;
|
|
64
|
+
return row;
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
export function createAgentList({ mgr, refreshTagsFromSessions, agentSessionEntries, progress }) {
|
|
57
68
|
return function list({ scanSessions = false, context = {}, includeTerminal = false } = {}) {
|
|
58
69
|
refreshTagsFromSessions({ scanSessions, context });
|
|
59
70
|
const now = Date.now();
|
|
60
|
-
const rows = agentSessionEntries({ scanSessions, context }).map(({ tag, session }) =>
|
|
71
|
+
const rows = agentSessionEntries({ scanSessions, context }).map(({ tag, session, indexRow }) =>
|
|
61
72
|
agentRow({
|
|
62
73
|
tag,
|
|
63
74
|
session,
|
|
75
|
+
indexRow,
|
|
64
76
|
runtime: mgr.getSessionRuntime?.(session.id),
|
|
65
77
|
progress: progress.sessionProgressExtras(session.id, session.agent || null, now),
|
|
66
78
|
now,
|
|
@@ -91,6 +91,8 @@ export function createSessionProgress({ mgr }) {
|
|
|
91
91
|
return {
|
|
92
92
|
workerStatus: status,
|
|
93
93
|
stage: progress.worker_stage || runtime?.stage || status,
|
|
94
|
+
// Additive: present only while the worker's turn waits on a user approval.
|
|
95
|
+
...(runtime?.awaitingUser ? { awaitingUser: runtime.awaitingUser } : {}),
|
|
94
96
|
clientHostPid: session.clientHostPid || null,
|
|
95
97
|
lastStreamDeltaAt: runtime?.lastStreamDeltaAt ? new Date(runtime.lastStreamDeltaAt).toISOString() : null,
|
|
96
98
|
...progress,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// worker-index/approval-wait.mjs
|
|
2
|
+
// Persists a worker's approval wait (runtime begin/end transitions) into the
|
|
3
|
+
// worker index, so processes that read only the stored index — the desktop
|
|
4
|
+
// Agents pane — see it. One process can hold several worker-index instances
|
|
5
|
+
// over the same file (every runtime's agent tool in the daemon); they share one
|
|
6
|
+
// row store per file, and exactly one writer per file answers each transition,
|
|
7
|
+
// so a wait costs one index write on begin and one on end.
|
|
8
|
+
//
|
|
9
|
+
// `awaitingUser` is a field of its own on the row: only conditional field
|
|
10
|
+
// updates (applyAwaitingUser, re-run on the locked document) set or clear it,
|
|
11
|
+
// fenced by session id and turn. Whole-row writes keep whatever the current
|
|
12
|
+
// row holds and drop it once the row settles (settleAwaitingUser).
|
|
13
|
+
import {
|
|
14
|
+
currentSessionApprovalWaits,
|
|
15
|
+
onSessionApprovalWaitChange,
|
|
16
|
+
} from '../../../../runtime/shared/session-approval-wait.mjs';
|
|
17
|
+
import { stampMs } from '../helpers.mjs';
|
|
18
|
+
import { isTerminalWorkerStatus } from '../worker-rows.mjs';
|
|
19
|
+
|
|
20
|
+
const writersByFile = new Map(); // file -> writer functions, first answers
|
|
21
|
+
let unsubscribe = null;
|
|
22
|
+
|
|
23
|
+
function withAwaitingUser(row, value) {
|
|
24
|
+
if (value ? row.awaitingUser === value : !('awaitingUser' in row)) return row;
|
|
25
|
+
const { awaitingUser: _prior, ...rest } = row;
|
|
26
|
+
return value ? { ...rest, awaitingUser: value } : rest;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `row` as a whole-row write may leave it: the flag `current` (the value the
|
|
30
|
+
* row holds before the write) survives unless the row settled. Returns `row`
|
|
31
|
+
* itself when nothing changes. */
|
|
32
|
+
export function settleAwaitingUser(row, current) {
|
|
33
|
+
return withAwaitingUser(row, current === 'approval' && !isTerminalWorkerStatus(row.status) ? 'approval' : null);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The flag `fence` wants on `row`, or undefined when the update is not for
|
|
37
|
+
* it: another session, or a turn that started after the transition's turn. */
|
|
38
|
+
function fencedAwaitingUser(row, { sessionId, awaitingUser, turnAt }) {
|
|
39
|
+
if (row.sessionId !== sessionId) return undefined;
|
|
40
|
+
if (turnAt > 0 && stampMs(row.turnStartedAt) > turnAt) return undefined;
|
|
41
|
+
return awaitingUser === 'approval' && !isTerminalWorkerStatus(row.status) ? 'approval' : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether applyAwaitingUser(fence) would change any of `rows`. */
|
|
45
|
+
export function awaitingUserWouldChange(rows, fence) {
|
|
46
|
+
return rows.some((row) => {
|
|
47
|
+
const wanted = fencedAwaitingUser(row, fence);
|
|
48
|
+
return wanted !== undefined && withAwaitingUser(row, wanted) !== row;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Field update: only `awaitingUser` of the fenced rows, nothing else. */
|
|
53
|
+
export function applyAwaitingUser(byKey, fence) {
|
|
54
|
+
for (const [key, row] of byKey) {
|
|
55
|
+
const wanted = fencedAwaitingUser(row, fence);
|
|
56
|
+
if (wanted === undefined) continue;
|
|
57
|
+
const next = withAwaitingUser(row, wanted);
|
|
58
|
+
if (next !== row) byKey.set(key, next);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Field update: drop every flag `isStale(row)` holds for. */
|
|
63
|
+
export function clearAwaitingUser(byKey, isStale) {
|
|
64
|
+
for (const [key, row] of byKey) {
|
|
65
|
+
if (row.awaitingUser && isStale(row)) byKey.set(key, withAwaitingUser(row, null));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function persistTransition(sessionId, awaitingUser, meta) {
|
|
70
|
+
for (const writers of writersByFile.values()) writers[0]?.(sessionId, awaitingUser, meta);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Register `writer(sessionId, awaitingUser, { turnAt })` for `file`; returns
|
|
74
|
+
* the release. The first writer of a file also receives the waits already
|
|
75
|
+
* open, so a wait that began before any index existed is not lost. */
|
|
76
|
+
export function registerApprovalWaitWriter(file, writer) {
|
|
77
|
+
if (!file || typeof writer !== 'function') return () => {};
|
|
78
|
+
const writers = writersByFile.get(file) || [];
|
|
79
|
+
writers.push(writer);
|
|
80
|
+
writersByFile.set(file, writers);
|
|
81
|
+
unsubscribe ??= onSessionApprovalWaitChange(persistTransition);
|
|
82
|
+
if (writers.length === 1) {
|
|
83
|
+
for (const { sessionId, ...meta } of currentSessionApprovalWaits()) writer(sessionId, 'approval', meta);
|
|
84
|
+
}
|
|
85
|
+
let released = false;
|
|
86
|
+
return () => {
|
|
87
|
+
if (released) return;
|
|
88
|
+
released = true;
|
|
89
|
+
const remaining = (writersByFile.get(file) || []).filter((entry) => entry !== writer);
|
|
90
|
+
if (remaining.length) writersByFile.set(file, remaining);
|
|
91
|
+
else writersByFile.delete(file);
|
|
92
|
+
if (writersByFile.size === 0 && unsubscribe) {
|
|
93
|
+
unsubscribe();
|
|
94
|
+
unsubscribe = null;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// worker-index/mutation-batch.mjs
|
|
2
2
|
// Spawn-path writes are batched onto one immediate so a parallel fanout pays a
|
|
3
|
-
// single locked rewrite instead of one per worker.
|
|
4
|
-
|
|
3
|
+
// single locked rewrite instead of one per worker. `afterFlush` runs once the
|
|
4
|
+
// batch's rows are in the store.
|
|
5
|
+
export function createMutationBatch(write, afterFlush = () => {}) {
|
|
5
6
|
const pending = [];
|
|
6
7
|
let timer = null;
|
|
7
8
|
|
|
@@ -21,6 +22,7 @@ export function createMutationBatch(write) {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
});
|
|
25
|
+
afterFlush();
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
function queue(mutator) {
|
|
@@ -14,6 +14,10 @@ const WORKER_POOL_FRESH_MS = 2 * 60 * 1000;
|
|
|
14
14
|
export const isActiveWorkerRow = (row) =>
|
|
15
15
|
ACTIVE_WORKER_STATUS.test(clean(row?.status)) || ACTIVE_WORKER_STATUS.test(clean(row?.stage));
|
|
16
16
|
|
|
17
|
+
/** A persisted approval wait whose runtime is gone: the in-memory wait died
|
|
18
|
+
* with that process (daemon restart), so the flag must not stick. */
|
|
19
|
+
export const isStaleApprovalWait = (row) => Boolean(row?.awaitingUser) && !runtimeAlive(row.runtimePid);
|
|
20
|
+
|
|
17
21
|
export function createRowLiveness({ dataDir, cfgMod }) {
|
|
18
22
|
function heartbeatFresh(sessionId, now) {
|
|
19
23
|
const id = clean(sessionId);
|
|
@@ -47,11 +51,13 @@ export function createRowLiveness({ dataDir, cfgMod }) {
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/** The row settled to idle. `touch` restamps it as finished now (this
|
|
50
|
-
* process is giving up its own work); otherwise existing stamps win.
|
|
54
|
+
* process is giving up its own work); otherwise existing stamps win. An
|
|
55
|
+
* idle worker waits on nobody, so a persisted approval wait drops too. */
|
|
51
56
|
function idleRow(row, now, touch = false) {
|
|
52
57
|
const stamp = new Date(now).toISOString();
|
|
58
|
+
const { awaitingUser: _awaitingUser, ...settled } = row;
|
|
53
59
|
return {
|
|
54
|
-
...
|
|
60
|
+
...settled,
|
|
55
61
|
status: 'idle',
|
|
56
62
|
stage: 'idle',
|
|
57
63
|
turnStartedAt: null,
|
|
@@ -55,6 +55,9 @@ export function normalizeWorkerRows(value) {
|
|
|
55
55
|
toolPermission: clean(row.toolPermission) || null,
|
|
56
56
|
messages: positiveInt(row.messages) || 0,
|
|
57
57
|
tools: positiveInt(row.tools) || 0,
|
|
58
|
+
// Additive and transient: present only while the worker's turn waits on
|
|
59
|
+
// a user approval (worker-index setAwaitingUser).
|
|
60
|
+
...(row.awaitingUser === 'approval' ? { awaitingUser: 'approval' } : {}),
|
|
58
61
|
}))
|
|
59
62
|
.filter((row) => !isLeadPoolAgent(row.agent) && keepWorkerRow(row));
|
|
60
63
|
}
|
|
@@ -1,12 +1,36 @@
|
|
|
1
1
|
// worker-index/row-store.mjs
|
|
2
2
|
// The on-disk worker index file: an mtime-keyed parse cache in front of it and
|
|
3
|
-
// the single in-process writer queue that republishes rows + tombstones.
|
|
3
|
+
// the single in-process writer queue that republishes rows + tombstones. Every
|
|
4
|
+
// worker index of a process over one file shares one store (acquireWorkerRowStore),
|
|
5
|
+
// so all of them serialize through the same projection and queue.
|
|
4
6
|
import { readFileSync, statSync } from 'node:fs';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
5
8
|
|
|
6
9
|
import { createIndexWriteQueue } from '../index-write-queue.mjs';
|
|
7
10
|
import { normalizeTagTombstones, tagTombstoneKey, workerRowKey } from '../worker-rows.mjs';
|
|
11
|
+
import { settleAwaitingUser } from './approval-wait.mjs';
|
|
8
12
|
import { keepWorkerRow, normalizeWorkerRows } from './row-shape.mjs';
|
|
9
13
|
|
|
14
|
+
const ROWS_ARG = 0; // the rows Map's position in the rewrite mutator's arguments
|
|
15
|
+
const sharedStores = new Map(); // canonical file -> { store, refs }
|
|
16
|
+
|
|
17
|
+
const canonicalFile = (file) => (process.platform === 'win32' ? resolve(file).toLowerCase() : resolve(file));
|
|
18
|
+
|
|
19
|
+
/** A whole-row write never decides `awaitingUser`: every row it changed keeps
|
|
20
|
+
* the flag the row held before (never a stale copy's), dropped once settled. */
|
|
21
|
+
function keepingAwaitingUser(mutator) {
|
|
22
|
+
return (byKey, ...rest) => {
|
|
23
|
+
const before = new Map(byKey);
|
|
24
|
+
mutator(byKey, ...rest);
|
|
25
|
+
for (const [key, row] of byKey) {
|
|
26
|
+
const prior = before.get(key);
|
|
27
|
+
if (row === prior) continue;
|
|
28
|
+
const settled = settleAwaitingUser(row, prior?.awaitingUser);
|
|
29
|
+
if (settled !== row) byKey.set(key, settled);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
10
34
|
/** One rewrite: the current document as keyed maps, the caller's mutator over
|
|
11
35
|
* them, then the v2 document to publish. */
|
|
12
36
|
function rewriteIndexDocument(cur, mutator) {
|
|
@@ -61,6 +85,9 @@ export function createWorkerRowStore(file) {
|
|
|
61
85
|
onPersisted: () => {
|
|
62
86
|
dirty = true;
|
|
63
87
|
},
|
|
88
|
+
// The same rule against the locked document: a replayed row keeps the
|
|
89
|
+
// flag that is on disk now, not the one the projection held.
|
|
90
|
+
mergeSet: (index, prev, next) => (index === ROWS_ARG ? settleAwaitingUser(next, prev?.awaitingUser) : next),
|
|
64
91
|
});
|
|
65
92
|
|
|
66
93
|
function projectedView() {
|
|
@@ -115,13 +142,17 @@ export function createWorkerRowStore(file) {
|
|
|
115
142
|
// Single writer path: the mutator runs now over keyed maps against the
|
|
116
143
|
// in-process projection (see index-write-queue.mjs); its effect is
|
|
117
144
|
// republished as rows + tombstones by one async locked writer.
|
|
118
|
-
const write = (mutator) => queue.write(mutator);
|
|
145
|
+
const write = (mutator) => queue.write(typeof mutator === 'function' ? keepingAwaitingUser(mutator) : mutator);
|
|
119
146
|
|
|
120
147
|
return {
|
|
121
148
|
readAll,
|
|
122
149
|
readSnapshot,
|
|
123
150
|
readTombstones,
|
|
124
151
|
write,
|
|
152
|
+
/** Conditional field update: `mutator` runs now on the projection and again
|
|
153
|
+
* on the locked document when persisted, so its condition is re-checked
|
|
154
|
+
* against what is on disk. The one writer of `awaitingUser`. */
|
|
155
|
+
update: (mutator) => queue.write(mutator, { rerun: true }),
|
|
125
156
|
/** Resolves once every write so far is on disk. */
|
|
126
157
|
flush: queue.flush,
|
|
127
158
|
/** Exit path: persist what is still queued, synchronously. */
|
|
@@ -131,3 +162,28 @@ export function createWorkerRowStore(file) {
|
|
|
131
162
|
},
|
|
132
163
|
};
|
|
133
164
|
}
|
|
165
|
+
|
|
166
|
+
/** The process's one store for `file` (a fresh no-op store for null), and its
|
|
167
|
+
* idempotent release; the store is dropped once its last holder releases. */
|
|
168
|
+
export function acquireWorkerRowStore(file) {
|
|
169
|
+
if (!file) return { store: createWorkerRowStore(null), release: () => {} };
|
|
170
|
+
const key = canonicalFile(file);
|
|
171
|
+
let entry = sharedStores.get(key);
|
|
172
|
+
if (!entry) {
|
|
173
|
+
entry = { store: createWorkerRowStore(file), refs: 0 };
|
|
174
|
+
sharedStores.set(key, entry);
|
|
175
|
+
}
|
|
176
|
+
entry.refs += 1;
|
|
177
|
+
// A new holder re-reads the file, as a store of its own would.
|
|
178
|
+
entry.store.invalidate();
|
|
179
|
+
let released = false;
|
|
180
|
+
return {
|
|
181
|
+
store: entry.store,
|
|
182
|
+
release: () => {
|
|
183
|
+
if (released) return;
|
|
184
|
+
released = true;
|
|
185
|
+
entry.refs -= 1;
|
|
186
|
+
if (entry.refs === 0 && sharedStores.get(key) === entry) sharedStores.delete(key);
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|