mixdog 0.9.151 → 0.9.152
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 +110 -68
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +18 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +1 -0
- package/src/tui/session-local.mjs +15 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createSkillDocument,
|
|
6
|
+
parseSkillDocument,
|
|
7
|
+
updateSkillDocument,
|
|
8
|
+
validateSkillName,
|
|
9
|
+
} from './skill-document.mjs';
|
|
10
|
+
|
|
11
|
+
test('parses standard multiline YAML and the Markdown instructions body', () => {
|
|
12
|
+
const parsed = parseSkillDocument([
|
|
13
|
+
'---',
|
|
14
|
+
'name: imported-skill',
|
|
15
|
+
'description: >',
|
|
16
|
+
' Use when an imported skill',
|
|
17
|
+
' needs multiline matching.',
|
|
18
|
+
'metadata:',
|
|
19
|
+
' author: example',
|
|
20
|
+
'allowed-tools:',
|
|
21
|
+
' - shell',
|
|
22
|
+
'---',
|
|
23
|
+
'',
|
|
24
|
+
'# Instructions',
|
|
25
|
+
'',
|
|
26
|
+
'Read `references/guide.md`.',
|
|
27
|
+
'',
|
|
28
|
+
].join('\n'));
|
|
29
|
+
|
|
30
|
+
assert.equal(parsed.name, 'imported-skill');
|
|
31
|
+
assert.equal(parsed.description,
|
|
32
|
+
'Use when an imported skill needs multiline matching.');
|
|
33
|
+
assert.deepEqual(parsed.frontmatter.metadata, { author: 'example' });
|
|
34
|
+
assert.deepEqual(parsed.frontmatter['allowed-tools'], ['shell']);
|
|
35
|
+
assert.match(parsed.body, /^# Instructions/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('updates the three editable fields without dropping optional frontmatter', () => {
|
|
39
|
+
const source = [
|
|
40
|
+
'---',
|
|
41
|
+
'# retained comment',
|
|
42
|
+
'name: old-name',
|
|
43
|
+
'description: Old trigger.',
|
|
44
|
+
'license: MIT',
|
|
45
|
+
'compatibility: Requires Python.',
|
|
46
|
+
'metadata:',
|
|
47
|
+
' author: example',
|
|
48
|
+
'allowed-tools: shell',
|
|
49
|
+
'---',
|
|
50
|
+
'',
|
|
51
|
+
'Old instructions.',
|
|
52
|
+
'',
|
|
53
|
+
].join('\n');
|
|
54
|
+
|
|
55
|
+
const updated = updateSkillDocument(source, {
|
|
56
|
+
name: 'new-name',
|
|
57
|
+
description: 'Use when the new trigger matches.',
|
|
58
|
+
body: '# New instructions',
|
|
59
|
+
});
|
|
60
|
+
const parsed = parseSkillDocument(updated);
|
|
61
|
+
|
|
62
|
+
assert.equal(parsed.name, 'new-name');
|
|
63
|
+
assert.equal(parsed.description, 'Use when the new trigger matches.');
|
|
64
|
+
assert.equal(parsed.body, '# New instructions\n');
|
|
65
|
+
assert.equal(parsed.frontmatter.license, 'MIT');
|
|
66
|
+
assert.equal(parsed.frontmatter.compatibility, 'Requires Python.');
|
|
67
|
+
assert.deepEqual(parsed.frontmatter.metadata, { author: 'example' });
|
|
68
|
+
assert.equal(parsed.frontmatter['allowed-tools'], 'shell');
|
|
69
|
+
assert.match(updated, /# retained comment/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('creates a standard document and rejects non-standard names', () => {
|
|
73
|
+
const source = createSkillDocument({
|
|
74
|
+
name: 'project-skill',
|
|
75
|
+
description: 'Use when this project needs the skill.',
|
|
76
|
+
body: '# Instructions',
|
|
77
|
+
});
|
|
78
|
+
assert.equal(parseSkillDocument(source).name, 'project-skill');
|
|
79
|
+
assert.throws(() => validateSkillName('Project_Skill'), /lowercase letters/);
|
|
80
|
+
assert.throws(() => validateSkillName('project--skill'), /consecutive hyphens/);
|
|
81
|
+
});
|
|
@@ -153,6 +153,10 @@ export function displayToolName(name, args = {}) {
|
|
|
153
153
|
case 'web_fetch':
|
|
154
154
|
case 'fetch':
|
|
155
155
|
return 'Fetch';
|
|
156
|
+
case 'browser':
|
|
157
|
+
return 'Browser';
|
|
158
|
+
case 'computer':
|
|
159
|
+
return 'Computer';
|
|
156
160
|
case 'list_mcp_resources':
|
|
157
161
|
case 'list_mcp_resource_templates':
|
|
158
162
|
case 'cwd':
|
|
@@ -275,6 +279,16 @@ export function summarizeToolArgs(name, args, { max = DEFAULT_SUMMARY_MAX } = {}
|
|
|
275
279
|
return formatCountedUnit(collectionCount(a.url, a.uri), 'URL', 'URLs');
|
|
276
280
|
}
|
|
277
281
|
return truncateToolText(a.url || a.uri || '', max);
|
|
282
|
+
case 'browser':
|
|
283
|
+
return compactParts([
|
|
284
|
+
String(a.action || ''),
|
|
285
|
+
a.url ? truncateToolText(a.url, max) : a.ref ? String(a.ref) : '',
|
|
286
|
+
]);
|
|
287
|
+
case 'computer':
|
|
288
|
+
return compactParts([
|
|
289
|
+
String(a.action || ''),
|
|
290
|
+
a.window ? truncateToolText(a.window, max) : a.ref ? String(a.ref) : a.app ? truncateToolText(a.app, max) : '',
|
|
291
|
+
]);
|
|
278
292
|
case 'read_mcp_resource':
|
|
279
293
|
return truncateToolText(a.uri || '', max);
|
|
280
294
|
case 'list_mcp_resources':
|
|
@@ -559,6 +573,10 @@ export function toolWorkUnit(name, args = {}, category = '') {
|
|
|
559
573
|
return unitDescriptor('Web Research', { count: queryCount(a, 'query', 'queries', 'keywords') || 1, noun: 'query', pluralNoun: 'queries' });
|
|
560
574
|
case 'web_fetch':
|
|
561
575
|
return unitDescriptor('Web Research', { count: queryCount(a, 'url', 'urls', 'uri', 'uris') || 1, active: 'Fetching', done: 'Fetched', noun: 'URL', pluralNoun: 'URLs' });
|
|
576
|
+
case 'browser':
|
|
577
|
+
return unitDescriptor('Browser', { count: 1, active: 'Browsing', done: 'Browsed', noun: 'action' });
|
|
578
|
+
case 'computer':
|
|
579
|
+
return unitDescriptor('Computer', { count: 1, active: 'Operating', done: 'Operated', noun: 'action' });
|
|
562
580
|
case 'fetch': {
|
|
563
581
|
const fetchLimit = Number(a.limit ?? a.messages);
|
|
564
582
|
const fetchCount = Number.isFinite(fetchLimit) && fetchLimit > 0
|
|
@@ -109,6 +109,7 @@ export async function saveTurnSnapshotRecord(sessionId, turn) {
|
|
|
109
109
|
const turns = (existing?.turns || []).filter((entry) => entry?.generation !== generation);
|
|
110
110
|
turns.push({
|
|
111
111
|
generation,
|
|
112
|
+
checkpointId: clean(turn?.checkpointId),
|
|
112
113
|
root,
|
|
113
114
|
baselineTree,
|
|
114
115
|
// Relative, forward-slashed paths this session's own tools mutated.
|
|
@@ -89,6 +89,7 @@ function createDiffTracker(meta = {}) {
|
|
|
89
89
|
return {
|
|
90
90
|
ownerSessionId: clean(meta.ownerSessionId) || null,
|
|
91
91
|
ownerGeneration: Number(meta.ownerGeneration) || 0,
|
|
92
|
+
checkpointId: clean(meta.checkpointId),
|
|
92
93
|
worktreeRequest: clean(meta.worktreeRequest),
|
|
93
94
|
worktreeContended: false,
|
|
94
95
|
valid: true,
|
|
@@ -445,20 +446,26 @@ function publicAgentReviews(sessionId) {
|
|
|
445
446
|
}
|
|
446
447
|
|
|
447
448
|
/** Start a new user turn and invalidate the prior turn's child review group. */
|
|
448
|
-
export async function beginTurnSnapshot(_worktree, sessionId) {
|
|
449
|
+
export async function beginTurnSnapshot(_worktree, sessionId, options = {}) {
|
|
449
450
|
const ownerSessionId = clean(sessionId);
|
|
450
451
|
if (DISABLED || !ownerSessionId) return;
|
|
451
452
|
const generation = (_turnsBySession.get(ownerSessionId)?.generation || 0) + 1;
|
|
453
|
+
const checkpointId = clean(options?.checkpointId);
|
|
452
454
|
for (const [trackedSessionId, tracker] of _diffTrackersBySession) {
|
|
453
455
|
if (tracker.ownerSessionId === ownerSessionId) _diffTrackersBySession.delete(trackedSessionId);
|
|
454
456
|
}
|
|
455
457
|
_turnsBySession.delete(ownerSessionId);
|
|
456
458
|
_turnsBySession.set(ownerSessionId, {
|
|
457
459
|
generation,
|
|
460
|
+
checkpointId,
|
|
458
461
|
agents: new Map(),
|
|
459
462
|
});
|
|
460
463
|
const worktreeRequest = pathKey(_worktree);
|
|
461
|
-
const tracker = resetDiffTracker(ownerSessionId, {
|
|
464
|
+
const tracker = resetDiffTracker(ownerSessionId, {
|
|
465
|
+
ownerGeneration: generation,
|
|
466
|
+
checkpointId,
|
|
467
|
+
worktreeRequest,
|
|
468
|
+
});
|
|
462
469
|
trimTurnCache();
|
|
463
470
|
if (!tracker) return;
|
|
464
471
|
for (const [otherSessionId, other] of _diffTrackersBySession) {
|
|
@@ -575,6 +582,7 @@ async function persistTurnSnapshotRecord(ownerSessionId, tracker) {
|
|
|
575
582
|
try {
|
|
576
583
|
await saveTurnSnapshotRecord(ownerSessionId, {
|
|
577
584
|
generation: _turnsBySession.get(ownerSessionId)?.generation || 0,
|
|
585
|
+
checkpointId: tracker.checkpointId,
|
|
578
586
|
root: snapshot.root,
|
|
579
587
|
baselineTree: snapshot.baselineTree,
|
|
580
588
|
toolFiles: owned,
|
|
@@ -669,6 +677,7 @@ async function scopedReviewFromRecord(ownerSessionId) {
|
|
|
669
677
|
patch: resumed.snapshot.patch || '',
|
|
670
678
|
snapshotKind: 'scoped',
|
|
671
679
|
revertMode: 'scoped',
|
|
680
|
+
checkpointId: clean(resumed.record.checkpointId),
|
|
672
681
|
patchTruncated: resumed.snapshot.patchTruncated === true,
|
|
673
682
|
authoritative: true,
|
|
674
683
|
agents: publicAgentReviews(ownerSessionId),
|
|
@@ -677,9 +686,20 @@ async function scopedReviewFromRecord(ownerSessionId) {
|
|
|
677
686
|
}
|
|
678
687
|
|
|
679
688
|
/** Restore recorded paths — every one of them, or just the file the user picked. */
|
|
680
|
-
|
|
689
|
+
function assertCheckpointMatches(expectedCheckpointId, actualCheckpointId) {
|
|
690
|
+
const expected = clean(expectedCheckpointId);
|
|
691
|
+
if (!expected) return;
|
|
692
|
+
const actual = clean(actualCheckpointId);
|
|
693
|
+
if (!actual) throw new Error('turn review checkpoint is unavailable');
|
|
694
|
+
if (actual !== expected) {
|
|
695
|
+
throw new Error('turn review checkpoint changed; refresh the review');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function revertFromRecord(worktree, ownerSessionId, file, expectedCheckpointId = '') {
|
|
681
700
|
const resumed = await resumeRecordedSnapshot(ownerSessionId);
|
|
682
701
|
if (!resumed) return null;
|
|
702
|
+
assertCheckpointMatches(expectedCheckpointId, resumed.record.checkpointId);
|
|
683
703
|
const owned = resumed.record.toolFiles;
|
|
684
704
|
const targets = file
|
|
685
705
|
? owned.filter((path) => recordedPathMatchesFile(path, resumed.record.root, file))
|
|
@@ -723,6 +743,7 @@ export async function getTurnReviewDiff(_worktree, sessionId, options = {}) {
|
|
|
723
743
|
patch: snapshot?.patch ?? tracker?.unifiedDiff ?? '',
|
|
724
744
|
snapshotKind: snapshot ? 'worktree' : 'tool',
|
|
725
745
|
revertMode: snapshot ? 'worktree' : trackedRevertAvailable ? 'tracked' : '',
|
|
746
|
+
checkpointId: clean(turn?.checkpointId || tracker?.checkpointId),
|
|
726
747
|
patchTruncated: snapshot?.patchTruncated === true,
|
|
727
748
|
authoritative: Boolean(turn && tracker),
|
|
728
749
|
agents: publicAgentReviews(ownerSessionId),
|
|
@@ -738,9 +759,10 @@ function unavailableTrackedRevertReason(tracker) {
|
|
|
738
759
|
}
|
|
739
760
|
|
|
740
761
|
/** Restore one reviewed file to this turn's worktree baseline, never to HEAD. */
|
|
741
|
-
export async function revertTurnReviewFile(_worktree, sessionId, file) {
|
|
762
|
+
export async function revertTurnReviewFile(_worktree, sessionId, file, expectedCheckpointId = '') {
|
|
742
763
|
const ownerSessionId = clean(sessionId);
|
|
743
764
|
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
765
|
+
if (tracker) assertCheckpointMatches(expectedCheckpointId, tracker.checkpointId);
|
|
744
766
|
if (tracker && !worktreeSnapshotUsable(tracker)) {
|
|
745
767
|
const pairs = trackedPairs(tracker);
|
|
746
768
|
if (pairs.length > 0) {
|
|
@@ -749,7 +771,7 @@ export async function revertTurnReviewFile(_worktree, sessionId, file) {
|
|
|
749
771
|
await revertTrackedPairs(tracker, _worktree, [pair]);
|
|
750
772
|
return await getTurnReviewDiff(_worktree, ownerSessionId);
|
|
751
773
|
}
|
|
752
|
-
const recorded = await revertFromRecord(_worktree, ownerSessionId, file);
|
|
774
|
+
const recorded = await revertFromRecord(_worktree, ownerSessionId, file, expectedCheckpointId);
|
|
753
775
|
if (recorded) return recorded;
|
|
754
776
|
throw new Error(unavailableTrackedRevertReason(tracker));
|
|
755
777
|
}
|
|
@@ -757,22 +779,23 @@ export async function revertTurnReviewFile(_worktree, sessionId, file) {
|
|
|
757
779
|
await revertTurnWorktreeFile(tracker.worktreeSnapshot, file);
|
|
758
780
|
return await getTurnReviewDiff(_worktree, ownerSessionId);
|
|
759
781
|
}
|
|
760
|
-
const recorded = await revertFromRecord(_worktree, ownerSessionId, file);
|
|
782
|
+
const recorded = await revertFromRecord(_worktree, ownerSessionId, file, expectedCheckpointId);
|
|
761
783
|
if (recorded) return recorded;
|
|
762
784
|
throw new Error('turn worktree snapshot is unavailable');
|
|
763
785
|
}
|
|
764
786
|
|
|
765
787
|
/** Restore every file in the reviewed turn to its turn-start state. */
|
|
766
|
-
export async function revertTurnReview(_worktree, sessionId) {
|
|
788
|
+
export async function revertTurnReview(_worktree, sessionId, expectedCheckpointId = '') {
|
|
767
789
|
const ownerSessionId = clean(sessionId);
|
|
768
790
|
const tracker = _diffTrackersBySession.get(ownerSessionId);
|
|
791
|
+
if (tracker) assertCheckpointMatches(expectedCheckpointId, tracker.checkpointId);
|
|
769
792
|
if (tracker && !worktreeSnapshotUsable(tracker)) {
|
|
770
793
|
const pairs = trackedPairs(tracker);
|
|
771
794
|
if (pairs.length > 0) {
|
|
772
795
|
await revertTrackedPairs(tracker, _worktree, pairs);
|
|
773
796
|
return await getTurnReviewDiff(_worktree, ownerSessionId);
|
|
774
797
|
}
|
|
775
|
-
const recorded = await revertFromRecord(_worktree, ownerSessionId, null);
|
|
798
|
+
const recorded = await revertFromRecord(_worktree, ownerSessionId, null, expectedCheckpointId);
|
|
776
799
|
if (recorded) return recorded;
|
|
777
800
|
throw new Error(unavailableTrackedRevertReason(tracker));
|
|
778
801
|
}
|
|
@@ -780,7 +803,7 @@ export async function revertTurnReview(_worktree, sessionId) {
|
|
|
780
803
|
await revertTurnWorktreeSnapshot(tracker.worktreeSnapshot);
|
|
781
804
|
return await getTurnReviewDiff(_worktree, ownerSessionId);
|
|
782
805
|
}
|
|
783
|
-
const recorded = await revertFromRecord(_worktree, ownerSessionId, null);
|
|
806
|
+
const recorded = await revertFromRecord(_worktree, ownerSessionId, null, expectedCheckpointId);
|
|
784
807
|
if (recorded) return recorded;
|
|
785
808
|
throw new Error('turn worktree snapshot is unavailable');
|
|
786
809
|
}
|
|
@@ -6,15 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* user-cwd.mjs — shared helper to resolve the user's working directory
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* precedence when set.
|
|
9
|
+
* user-cwd.mjs — shared helper to resolve the user's working directory.
|
|
10
|
+
* Inline runtimes use AsyncLocalStorage; an inherited
|
|
11
|
+
* MIXDOG_SESSION_CWD remains only a boot/isolated-process fallback.
|
|
13
12
|
*
|
|
14
13
|
* Single-source-of-truth model:
|
|
15
|
-
* - captureOriginalUserCwd() reads
|
|
16
|
-
*
|
|
17
|
-
* call. No in-memory freeze.
|
|
14
|
+
* - captureOriginalUserCwd() reads the async session scope first, then
|
|
15
|
+
* MIXDOG_SESSION_CWD/user-cwd.txt fresh on every call.
|
|
18
16
|
* - rawUserCwd() reads ONLY user-cwd.txt (no env consult) — exposed
|
|
19
17
|
* for the cwd-tool auto-init path so the env-var fallback cannot
|
|
20
18
|
* become self-referential.
|
|
@@ -32,6 +30,12 @@ import { resolvePluginData, mixdogRoot } from './plugin-paths.mjs'
|
|
|
32
30
|
|
|
33
31
|
const _cwdOverride = new AsyncLocalStorage()
|
|
34
32
|
|
|
33
|
+
function _activeCwdOverride() {
|
|
34
|
+
const store = _cwdOverride.getStore()
|
|
35
|
+
if (typeof store === 'string') return store
|
|
36
|
+
return store && typeof store.cwd === 'string' ? store.cwd : null
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
function _dataFile(name) {
|
|
36
40
|
return join(resolvePluginData(), name)
|
|
37
41
|
}
|
|
@@ -75,10 +79,9 @@ function _normalizePlatformCwd(p) {
|
|
|
75
79
|
|
|
76
80
|
/**
|
|
77
81
|
* Resolve the session cwd from EXPLICIT signals only:
|
|
78
|
-
* 1.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* 2. user-cwd.txt — single source of truth maintained by Mixdog
|
|
82
|
+
* 1. AsyncLocalStorage — current inline-session scope.
|
|
83
|
+
* 2. process.env.MIXDOG_SESSION_CWD — boot/isolated-process override.
|
|
84
|
+
* 3. user-cwd.txt — persisted fallback maintained by Mixdog
|
|
82
85
|
* (rewritten at every session start).
|
|
83
86
|
* Returns null when neither is available.
|
|
84
87
|
*
|
|
@@ -89,6 +92,8 @@ function _normalizePlatformCwd(p) {
|
|
|
89
92
|
* project_id resolution; use pwd() for relative-path resolution.
|
|
90
93
|
*/
|
|
91
94
|
export function explicitSessionCwd() {
|
|
95
|
+
const scoped = _activeCwdOverride()
|
|
96
|
+
if (scoped) return scoped
|
|
92
97
|
const sessionRaw = process.env.MIXDOG_SESSION_CWD
|
|
93
98
|
if (typeof sessionRaw === 'string' && sessionRaw.length > 0) {
|
|
94
99
|
const normalized = _normalizePlatformCwd(sessionRaw)
|
|
@@ -208,8 +213,22 @@ export function readLastSessionCwd(keyPid) {
|
|
|
208
213
|
* Run fn inside an async context where pwd() returns cwd.
|
|
209
214
|
* All descendant async calls within fn see cwd as their working directory.
|
|
210
215
|
*/
|
|
211
|
-
function runWithCwdOverride(cwd, fn) {
|
|
212
|
-
|
|
216
|
+
export function runWithCwdOverride(cwd, fn) {
|
|
217
|
+
const normalized = _normalizePlatformCwd(cwd)
|
|
218
|
+
return _cwdOverride.run({ cwd: normalized || String(cwd || '') }, fn)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Retarget the current async session after an in-turn `cwd set`.
|
|
223
|
+
* Other concurrent sessions hold different AsyncLocalStorage records.
|
|
224
|
+
*/
|
|
225
|
+
export function updateCurrentCwdOverride(cwd) {
|
|
226
|
+
const store = _cwdOverride.getStore()
|
|
227
|
+
if (!store || typeof store !== 'object') return false
|
|
228
|
+
const normalized = _normalizePlatformCwd(cwd)
|
|
229
|
+
if (!normalized) return false
|
|
230
|
+
store.cwd = normalized
|
|
231
|
+
return true
|
|
213
232
|
}
|
|
214
233
|
|
|
215
234
|
/**
|
|
@@ -217,5 +236,5 @@ function runWithCwdOverride(cwd, fn) {
|
|
|
217
236
|
* override set by runWithCwdOverride (innermost wins) ?? original user cwd.
|
|
218
237
|
*/
|
|
219
238
|
export function pwd() {
|
|
220
|
-
return
|
|
239
|
+
return _activeCwdOverride() ?? captureOriginalUserCwd()
|
|
221
240
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
explicitSessionCwd,
|
|
9
|
+
pwd,
|
|
10
|
+
runWithCwdOverride,
|
|
11
|
+
updateCurrentCwdOverride,
|
|
12
|
+
} from './user-cwd.mjs';
|
|
13
|
+
|
|
14
|
+
test('cwd overrides stay isolated across concurrent inline sessions', async () => {
|
|
15
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-user-cwd-'));
|
|
16
|
+
const a = join(root, 'a');
|
|
17
|
+
const b = join(root, 'b');
|
|
18
|
+
const next = join(root, 'next');
|
|
19
|
+
mkdirSync(a);
|
|
20
|
+
mkdirSync(b);
|
|
21
|
+
mkdirSync(next);
|
|
22
|
+
try {
|
|
23
|
+
await Promise.all([
|
|
24
|
+
runWithCwdOverride(a, async () => {
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
26
|
+
assert.equal(pwd(), a);
|
|
27
|
+
assert.equal(explicitSessionCwd(), a);
|
|
28
|
+
assert.equal(updateCurrentCwdOverride(next), true);
|
|
29
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
30
|
+
assert.equal(pwd(), next);
|
|
31
|
+
assert.equal(explicitSessionCwd(), next);
|
|
32
|
+
}),
|
|
33
|
+
runWithCwdOverride(b, async () => {
|
|
34
|
+
assert.equal(pwd(), b);
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
36
|
+
assert.equal(pwd(), b);
|
|
37
|
+
assert.equal(explicitSessionCwd(), b);
|
|
38
|
+
}),
|
|
39
|
+
]);
|
|
40
|
+
} finally {
|
|
41
|
+
rmSync(root, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { discoverPluginMcp } from './plugin-mcp.mjs';
|
|
2
2
|
import { featureEnvOverride, memoryToolsEnabled } from './config-helpers.mjs';
|
|
3
3
|
import { readSessionCoreMemoryPayload } from '../runtime/memory/lib/core-memory-file.mjs';
|
|
4
|
+
import { saveSession } from '../runtime/agent/orchestrator/session/store.mjs';
|
|
4
5
|
|
|
5
6
|
// cwd-plugins.mjs — cwd resolution/apply + plugins-status + core-memory context,
|
|
6
7
|
// extracted from mixdog-session-runtime.mjs. Dependency-injected factory that
|
|
@@ -15,16 +16,14 @@ export function createCwdPlugins({
|
|
|
15
16
|
setCurrentCwd,
|
|
16
17
|
getConfig,
|
|
17
18
|
getSession,
|
|
19
|
+
getDesktopSession,
|
|
20
|
+
setDesktopSession,
|
|
18
21
|
getRoute,
|
|
19
|
-
getLastProjectMcpKey,
|
|
20
|
-
setLastProjectMcpKey,
|
|
21
22
|
isCodeGraphPrewarmLazy,
|
|
22
23
|
isCodeGraphFirstTurnPrewarmDone,
|
|
23
24
|
getCodeGraphPrewarmDelayMs,
|
|
24
25
|
setSessionNeedsCwdRefresh,
|
|
25
26
|
// callbacks / deps
|
|
26
|
-
connectConfiguredMcp,
|
|
27
|
-
invalidatePreSessionToolSurface,
|
|
28
27
|
scheduleCodeGraphPrewarm,
|
|
29
28
|
hooks,
|
|
30
29
|
hookCommonPayload,
|
|
@@ -37,8 +36,9 @@ export function createCwdPlugins({
|
|
|
37
36
|
pluginMcpServerName,
|
|
38
37
|
mcpScriptForPlugin,
|
|
39
38
|
countSkillFiles,
|
|
40
|
-
readProjectMcpServers,
|
|
41
39
|
writeLastSessionCwd,
|
|
40
|
+
updateCurrentCwdOverride,
|
|
41
|
+
persistSession = (session) => saveSession(session, { immediate: true }),
|
|
42
42
|
// shared utils
|
|
43
43
|
clean,
|
|
44
44
|
resolve,
|
|
@@ -98,17 +98,31 @@ export function createCwdPlugins({
|
|
|
98
98
|
return next;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function applyResolvedCwd(nextCwd, {
|
|
101
|
+
function applyResolvedCwd(nextCwd, {
|
|
102
|
+
markRefresh = true,
|
|
103
|
+
waitForMcpReset = false,
|
|
104
|
+
persistProjectSelection = false,
|
|
105
|
+
} = {}) {
|
|
102
106
|
const resolved = resolve(nextCwd);
|
|
103
107
|
const stat = statSync(resolved);
|
|
104
108
|
if (!stat.isDirectory()) throw new Error(`cwd: not a directory: ${resolved}`);
|
|
105
109
|
const changed = resolve(getCurrentCwd()) !== resolved;
|
|
106
110
|
setCurrentCwd(resolved);
|
|
107
111
|
const currentCwd = resolved;
|
|
108
|
-
process.env.MIXDOG_SESSION_CWD = currentCwd;
|
|
109
|
-
writeLastSessionCwd(currentCwd);
|
|
110
112
|
const session = getSession();
|
|
111
113
|
if (session) session.cwd = currentCwd;
|
|
114
|
+
updateCurrentCwdOverride?.(currentCwd);
|
|
115
|
+
writeLastSessionCwd(currentCwd, session?.clientHostPid);
|
|
116
|
+
if (persistProjectSelection && session) {
|
|
117
|
+
const desktop = getDesktopSession?.() || session.desktopSession;
|
|
118
|
+
if (desktop && typeof desktop === 'object') {
|
|
119
|
+
const nextDesktop = { classification: 'project', projectPath: currentCwd };
|
|
120
|
+
setDesktopSession?.(nextDesktop);
|
|
121
|
+
session.desktopSession = nextDesktop;
|
|
122
|
+
}
|
|
123
|
+
session.updatedAt = Date.now();
|
|
124
|
+
persistSession(session);
|
|
125
|
+
}
|
|
112
126
|
// cwd changes NEVER recreate the session: a mid-conversation cwd switch must
|
|
113
127
|
// preserve the full message history. We retarget the live execution cwd in
|
|
114
128
|
// place; the BP3 session snapshot remains the session-start environment.
|
|
@@ -125,29 +139,14 @@ export function createCwdPlugins({
|
|
|
125
139
|
const delay = getCodeGraphPrewarmDelayMs();
|
|
126
140
|
scheduleCodeGraphPrewarm(changed ? 0 : delay, changed ? 'cwd-change' : 'cwd');
|
|
127
141
|
}
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
// an awaitable reset so the next session/turn cannot observe the old registry.
|
|
131
|
-
let mcpReset = null;
|
|
132
|
-
if (changed) {
|
|
133
|
-
try {
|
|
134
|
-
const nextKey = resolved + '\u0000' + JSON.stringify(readProjectMcpServers(resolved));
|
|
135
|
-
if (nextKey !== getLastProjectMcpKey()) {
|
|
136
|
-
setLastProjectMcpKey(nextKey);
|
|
137
|
-
mcpReset = Promise.resolve(connectConfiguredMcp({ reset: true }))
|
|
138
|
-
.then(() => invalidatePreSessionToolSurface())
|
|
139
|
-
.catch(() => {});
|
|
140
|
-
}
|
|
141
|
-
} catch {}
|
|
142
|
-
}
|
|
142
|
+
// Extension settings are global; cwd changes never reload MCP or Skills.
|
|
143
|
+
void waitForMcpReset;
|
|
143
144
|
// CwdChanged: bridge an effective cwd switch to the standard hook bus.
|
|
144
145
|
// No matcher event — payload is minimal { cwd }. Fire-and-forget.
|
|
145
146
|
if (changed) {
|
|
146
147
|
try { void hooks.dispatch('CwdChanged', hookCommonPayload({ cwd: currentCwd })); } catch {}
|
|
147
148
|
}
|
|
148
|
-
return
|
|
149
|
-
? Promise.resolve(mcpReset).then(() => currentCwd)
|
|
150
|
-
: currentCwd;
|
|
149
|
+
return currentCwd;
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
async function refreshSessionForCwdIfNeeded(reason = 'cwd-change') {
|
|
@@ -183,6 +182,7 @@ export function createCwdPlugins({
|
|
|
183
182
|
sourceUrl: clean(entry.source),
|
|
184
183
|
sourceType: clean(entry.sourceType) || 'git',
|
|
185
184
|
managed: entry.managed !== false,
|
|
185
|
+
enabled: entry.enabled !== false,
|
|
186
186
|
root,
|
|
187
187
|
installedAt: entry.installedAt || null,
|
|
188
188
|
updatedAt: entry.updatedAt || null,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, mkdirSync, rmSync, statSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { resolve, join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { createCwdPlugins } from './cwd-plugins.mjs';
|
|
8
|
+
|
|
9
|
+
test('explicit cwd selection persists execution cwd and desktop project metadata together', () => {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-cwd-persist-'));
|
|
11
|
+
const before = join(root, 'before');
|
|
12
|
+
const after = join(root, 'after');
|
|
13
|
+
mkdirSync(before);
|
|
14
|
+
mkdirSync(after);
|
|
15
|
+
let currentCwd = before;
|
|
16
|
+
let desktopSession = { classification: 'project', projectPath: before };
|
|
17
|
+
const session = {
|
|
18
|
+
id: 'sess-cwd-persist',
|
|
19
|
+
cwd: before,
|
|
20
|
+
desktopSession,
|
|
21
|
+
clientHostPid: 4321,
|
|
22
|
+
};
|
|
23
|
+
const persisted = [];
|
|
24
|
+
const sentinels = [];
|
|
25
|
+
const overrides = [];
|
|
26
|
+
try {
|
|
27
|
+
const { applyResolvedCwd } = createCwdPlugins({
|
|
28
|
+
getCurrentCwd: () => currentCwd,
|
|
29
|
+
setCurrentCwd: (next) => { currentCwd = next; },
|
|
30
|
+
getConfig: () => ({}),
|
|
31
|
+
getSession: () => session,
|
|
32
|
+
getDesktopSession: () => desktopSession,
|
|
33
|
+
setDesktopSession: (next) => { desktopSession = next; },
|
|
34
|
+
getRoute: () => ({}),
|
|
35
|
+
isCodeGraphPrewarmLazy: () => true,
|
|
36
|
+
isCodeGraphFirstTurnPrewarmDone: () => false,
|
|
37
|
+
getCodeGraphPrewarmDelayMs: () => 0,
|
|
38
|
+
setSessionNeedsCwdRefresh: () => {},
|
|
39
|
+
scheduleCodeGraphPrewarm: () => {},
|
|
40
|
+
hooks: { dispatch: async () => {} },
|
|
41
|
+
hookCommonPayload: (value) => value,
|
|
42
|
+
bootProfile: () => {},
|
|
43
|
+
getMemoryModule: async () => null,
|
|
44
|
+
listRegisteredPlugins: () => [],
|
|
45
|
+
pluginAdminStatus: () => ({}),
|
|
46
|
+
pluginManifest: () => ({}),
|
|
47
|
+
pluginMcpServerName: () => '',
|
|
48
|
+
mcpScriptForPlugin: () => '',
|
|
49
|
+
countSkillFiles: () => 0,
|
|
50
|
+
writeLastSessionCwd: (cwd, pid) => sentinels.push({ cwd, pid }),
|
|
51
|
+
updateCurrentCwdOverride: (cwd) => overrides.push(cwd),
|
|
52
|
+
persistSession: (value) => persisted.push(structuredClone(value)),
|
|
53
|
+
clean: (value) => String(value || '').trim(),
|
|
54
|
+
resolve,
|
|
55
|
+
statSync,
|
|
56
|
+
existsSync: () => false,
|
|
57
|
+
cfgMod: {},
|
|
58
|
+
STANDALONE_DATA_DIR: root,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
applyResolvedCwd(after, { persistProjectSelection: true });
|
|
62
|
+
|
|
63
|
+
assert.equal(currentCwd, after);
|
|
64
|
+
assert.equal(session.cwd, after);
|
|
65
|
+
assert.deepEqual(desktopSession, { classification: 'project', projectPath: after });
|
|
66
|
+
assert.deepEqual(session.desktopSession, desktopSession);
|
|
67
|
+
assert.equal(persisted.length, 1);
|
|
68
|
+
assert.equal(persisted[0].cwd, after);
|
|
69
|
+
assert.deepEqual(persisted[0].desktopSession, desktopSession);
|
|
70
|
+
assert.deepEqual(sentinels, [{ cwd: after, pid: 4321 }]);
|
|
71
|
+
assert.deepEqual(overrides, [after]);
|
|
72
|
+
} finally {
|
|
73
|
+
rmSync(root, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
@@ -14,6 +14,8 @@ test('cwd internal tool stays bound to its caller when another runtime owns the
|
|
|
14
14
|
mkdirSync(cwdA);
|
|
15
15
|
mkdirSync(cwdB);
|
|
16
16
|
mkdirSync(cwdNext);
|
|
17
|
+
const previousProcessCwd = process.env.MIXDOG_SESSION_CWD;
|
|
18
|
+
process.env.MIXDOG_SESSION_CWD = 'process-global-must-not-change';
|
|
17
19
|
|
|
18
20
|
const runtimeA = await createMixdogSessionRuntime({ cwd: cwdA, toolMode: 'full' });
|
|
19
21
|
const runtimeB = await createMixdogSessionRuntime({ cwd: cwdB, toolMode: 'full' });
|
|
@@ -22,6 +24,8 @@ test('cwd internal tool stays bound to its caller when another runtime owns the
|
|
|
22
24
|
runtimeA.close('cwd-routing-test', { waitForExit: false }),
|
|
23
25
|
runtimeB.close('cwd-routing-test', { waitForExit: false }),
|
|
24
26
|
]);
|
|
27
|
+
if (previousProcessCwd === undefined) delete process.env.MIXDOG_SESSION_CWD;
|
|
28
|
+
else process.env.MIXDOG_SESSION_CWD = previousProcessCwd;
|
|
25
29
|
rmSync(root, { recursive: true, force: true });
|
|
26
30
|
});
|
|
27
31
|
|
|
@@ -53,4 +57,5 @@ test('cwd internal tool stays bound to its caller when another runtime owns the
|
|
|
53
57
|
assert.equal(setResult.sessionId, callerSession.id);
|
|
54
58
|
assert.equal(runtimeA.cwd, cwdNext);
|
|
55
59
|
assert.equal(runtimeB.cwd, cwdB);
|
|
60
|
+
assert.equal(process.env.MIXDOG_SESSION_CWD, 'process-global-must-not-change');
|
|
56
61
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// In-process fan-out for machine-global extension settings. The daemon hosts
|
|
2
|
+
// multiple session runtimes in one process; a write from one runtime must make
|
|
3
|
+
// every other live runtime reload the same Skills, Plugins, and MCP state.
|
|
4
|
+
const subscribers = new Map();
|
|
5
|
+
let nextSubscriberId = 0;
|
|
6
|
+
|
|
7
|
+
export function subscribeGlobalExtensionChanges(listener) {
|
|
8
|
+
if (typeof listener !== 'function') throw new TypeError('global extension listener is required');
|
|
9
|
+
const id = ++nextSubscriberId;
|
|
10
|
+
subscribers.set(id, listener);
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
unsubscribe() {
|
|
14
|
+
subscribers.delete(id);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function publishGlobalExtensionChange(kind, originId = null) {
|
|
20
|
+
const jobs = [];
|
|
21
|
+
for (const [id, listener] of subscribers) {
|
|
22
|
+
if (id === originId) continue;
|
|
23
|
+
jobs.push(Promise.resolve().then(() => listener(kind)));
|
|
24
|
+
}
|
|
25
|
+
await Promise.allSettled(jobs);
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
publishGlobalExtensionChange,
|
|
6
|
+
subscribeGlobalExtensionChanges,
|
|
7
|
+
} from './global-extensions.mjs';
|
|
8
|
+
|
|
9
|
+
test('global extension changes fan out to every runtime except the writer', async () => {
|
|
10
|
+
const events = [];
|
|
11
|
+
const writer = subscribeGlobalExtensionChanges((kind) => events.push(['writer', kind]));
|
|
12
|
+
const peer = subscribeGlobalExtensionChanges((kind) => events.push(['peer', kind]));
|
|
13
|
+
try {
|
|
14
|
+
await publishGlobalExtensionChange('mcp', writer.id);
|
|
15
|
+
assert.deepEqual(events, [['peer', 'mcp']]);
|
|
16
|
+
} finally {
|
|
17
|
+
writer.unsubscribe();
|
|
18
|
+
peer.unsubscribe();
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('unsubscribed runtimes stop receiving global extension changes', async () => {
|
|
23
|
+
const events = [];
|
|
24
|
+
const runtime = subscribeGlobalExtensionChanges((kind) => events.push(kind));
|
|
25
|
+
runtime.unsubscribe();
|
|
26
|
+
await publishGlobalExtensionChange('skills');
|
|
27
|
+
assert.deepEqual(events, []);
|
|
28
|
+
});
|