From de91ed30fbdfe1ba9a72074a0421f1fb74b69251 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 9 Oct 2026 19:21:11 +0000 Subject: [PATCH 1/2] Read Mcode Skills with bounded concurrency --- packages/mcode-harness/README.md | 2 +- packages/mcode-harness/native-skills.test.mjs | 102 ++++++++++++++++++ packages/mcode-harness/patch-native.mjs | 66 +++++++++++- scripts/build-mcode-harness.sh | 2 +- 4 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 packages/mcode-harness/native-skills.test.mjs diff --git a/packages/mcode-harness/README.md b/packages/mcode-harness/README.md index 7e64c4ba4..bfab088a7 100644 --- a/packages/mcode-harness/README.md +++ b/packages/mcode-harness/README.md @@ -2,7 +2,7 @@ This companion package lets the agent host run MiniMax Code with OpenAgentCore's workspace. MiniMax Code keeps its own ACP Session, model loop and history. The package supplies a trusted MCP server (`bridge.mjs`), which the daemon registers as `oac_workspace` (its MCP server info names it `oac-workspace`). It exposes six native MiniMax Code tools rooted at the Session's workspace: `workspace_read`, `workspace_write`, `workspace_edit`, `workspace_bash`, `workspace_grep` and `workspace_glob`. The bridge and its tools run beside the CLI in the Session's agent-host view, where Bash, `rg` and `git` run in the sandbox; the package adds no inner sandbox. The [maintainer guide](../../docs/maintainers.md#runtime-images-and-helpers) owns the agent-host image build; [Harness qualification](../../contracts/agents-api/harness-onboarding.md#qualify-the-view) owns deployment evidence. -One patch script (`patch-native.mjs`) patches the pinned native CLI source. The SQLite task-admission transaction enforces the daemon's Subagent concurrency limit before child work starts; foreground, background, nested and idle-child append admissions share that transaction, and terminal native tasks release capacity. ACP initialization reports `oac/subagents` metadata: its version, the applied workspace tool policy and the admission limit. The native tool catalog applies the `protected-mcp-v1` tool gate described under [Subagents and cancellation](#subagents-and-cancellation). Under the same policy, the CLI ignores the workspace's project `.mcp.json`, so the Session's MCP comes only from the daemon. No second model or scheduling loop is introduced. Hosted public execution is not qualified by this package alone. +One patch script (`patch-native.mjs`) patches the pinned native CLI source. Native Skill refresh reads at most four independent files per root concurrently, retaining the upstream identity checks, ordered diagnostics, cache keys and precedence. The SQLite task-admission transaction enforces the daemon's Subagent concurrency limit before child work starts; foreground, background, nested and idle-child append admissions share that transaction, and terminal native tasks release capacity. ACP initialization reports `oac/subagents` metadata: its version, the applied workspace tool policy and the admission limit. The native tool catalog applies the `protected-mcp-v1` tool gate described under [Subagents and cancellation](#subagents-and-cancellation). Under the same policy, the CLI ignores the workspace's project `.mcp.json`, so the Session's MCP comes only from the daemon. No second model or scheduling loop is introduced. Hosted public execution is not qualified by this package alone. ## Workspace tools diff --git a/packages/mcode-harness/native-skills.test.mjs b/packages/mcode-harness/native-skills.test.mjs new file mode 100644 index 000000000..3b2faf3d2 --- /dev/null +++ b/packages/mcode-harness/native-skills.test.mjs @@ -0,0 +1,102 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { promises as fs } from 'node:fs'; +import { createRequire } from 'node:module'; +import { join } from 'node:path'; +import { homedir } from 'node:os'; +import { pathToFileURL } from 'node:url'; + +const root = process.env.MCODE_SOURCE; +test('native Skill refresh bounds independent reads and retains file identity and snapshot order', { skip: !root }, async t => { + const require = createRequire(join(root, 'package.json')); + const { build } = require('esbuild'); + const artifacts = join(homedir(), '.oac', 'tests'); + await fs.mkdir(artifacts, { recursive: true }); + const directory = await fs.mkdtemp(join(artifacts, 'mcode-skills-')); + t.after(() => fs.rm(directory, { recursive: true, force: true })); + const result = await build({ + entryPoints: [join(root, 'packages/agent-modules/skills/src/registry.ts')], + bundle: true, write: false, platform: 'node', format: 'esm', target: 'node22', + nodePaths: [join(root, 'node_modules')], + }); + const bundle = join(directory, 'skills.mjs'); + await fs.writeFile(bundle, result.outputFiles[0].text); + const { SkillRegistry } = await import(pathToFileURL(bundle).href); + const global = join(directory, 'global'), project = join(directory, 'project'); + const outside = join(directory, 'outside'); + for (const path of [global, project, outside]) await fs.mkdir(path); + const body = name => `---\nname: ${name}\ndescription: Test ${name}.\n---\n# ${name}\nTest body.\n`; + for (let i = 0; i < 9; i++) { + await fs.mkdir(join(global, `skill-${i}`)); + await fs.writeFile(join(global, `skill-${i}`, 'SKILL.md'), body(`skill-${i}`)); + } + await fs.mkdir(join(project, 'duplicate')); + await fs.writeFile(join(project, 'duplicate/SKILL.md'), body('skill-0')); + await fs.writeFile(join(outside, 'SKILL.md'), body('outside')); + await fs.symlink(outside, join(global, 'linked-directory')); + await fs.mkdir(join(global, 'linked-file')); + await fs.symlink(join(outside, 'SKILL.md'), join(global, 'linked-file/SKILL.md')); + const roots = [ + { id: 'global', kind: 'global', rootPath: global }, + { id: 'project', kind: 'project', rootPath: project }, + { id: 'absent', kind: 'global', rootPath: join(directory, 'absent') }, + ]; + const registry = new SkillRegistry(roots); + const first = await registry.refresh(); + assert.equal(first.metrics.filesRead, 11); + assert.equal(first.metrics.winners, 10); + assert.equal(first.winners.find(entry => entry.name === 'skill-0').rootId, 'project'); + assert.deepEqual(first.diagnostics.map(value => value.code), ['skill_symlink_rejected', 'root_unreadable']); + const cached = await registry.refresh(); + assert.deepEqual(cached.entries, first.entries); + assert.deepEqual(cached.diagnostics, first.diagnostics); + assert.equal(cached.metrics.filesRead, 0); + assert.equal(cached.metrics.filesReused, 11); + + await fs.writeFile(join(global, 'skill-3/SKILL.md'), body('skill-3') + 'Changed.\n'); + await fs.rm(join(global, 'skill-1'), { recursive: true }); + await fs.rm(join(global, 'skill-2/SKILL.md')); + await fs.symlink(join(outside, 'SKILL.md'), join(global, 'skill-2/SKILL.md')); + const changed = await registry.refresh(); + assert.equal(changed.metrics.filesRead, 1); + assert.equal(changed.metrics.filesReused, 8); + assert.equal(changed.entries.some(entry => ['skill-1', 'skill-2'].includes(entry.name)), false); + assert.match(changed.entries.find(entry => entry.name === 'skill-3').content, /Changed\./); + + // Deliberately reverse completion order; result and diagnostic order must + // remain the native candidate order, with no more than four opens in flight. + const open = fs.open; + let active = 0, maximum = 0; + fs.open = async (path, ...args) => { + active++; + maximum = Math.max(maximum, active); + try { + await new Promise(resolve => setTimeout(resolve, String(path).includes('skill-4') ? 20 : 5)); + return await open(path, ...args); + } finally { active--; } + }; + try { + const reordered = await new SkillRegistry(roots).refresh(); + assert.deepEqual(reordered.entries, changed.entries); + assert.deepEqual(reordered.diagnostics, changed.diagnostics); + assert.ok(maximum > 1 && maximum <= 4, `concurrent opens: ${maximum}`); + } finally { fs.open = open; } + + // The same open-file identity check must still reject replacement between + // inspection and content read, even when other files finish independently. + let opens = 0; + fs.open = async (path, ...args) => { + const handle = await open(path, ...args); + if (String(path) !== join(global, 'skill-3/SKILL.md') || ++opens !== 2) return handle; + return new Proxy(handle, { get(target, key) { + if (key === 'stat') return async () => { const stat = await target.stat(); stat.ino++; return stat; }; + const value = Reflect.get(target, key); + return typeof value === 'function' ? value.bind(target) : value; + } }); + }; + try { + const replaced = await new SkillRegistry(roots).refresh(); + assert.ok(replaced.diagnostics.some(value => value.code === 'skill_changed_during_read')); + assert.equal(replaced.entries.some(entry => entry.name === 'skill-3'), false); + } finally { fs.open = open; } +}); diff --git a/packages/mcode-harness/patch-native.mjs b/packages/mcode-harness/patch-native.mjs index 35bc86b67..758ebd705 100644 --- a/packages/mcode-harness/patch-native.mjs +++ b/packages/mcode-harness/patch-native.mjs @@ -84,7 +84,7 @@ copyFileSync(join(here, 'native-subagent-admission.mjs'), join(root, 'packages/l function replaceNative(file, before, after) { const path = join(root, file); const source = readFileSync(path, 'utf8'); - if (source.split(before).length !== 2) throw new Error('Pinned native MCP lifecycle source changed: ' + file); + if (source.split(before).length !== 2) throw new Error('Pinned native source changed: ' + file); writeFileSync(path, source.replace(before, after)); } const poolFile = 'packages/agent-modules/mcp/src/runtime/connection-pool.ts'; @@ -234,6 +234,70 @@ replaceNative('packages/tui/src/acp/agent.ts', }); app.onNotification(acp.methods.agent.session.cancel, async ({ params }) => {`); +// Read independent Skills concurrently while committing the existing snapshot +// and cache in candidate order. All stable-file checks stay in the native reader. +replaceNative('packages/agent-modules/skills/src/registry.ts', + ` for (const candidate of candidates) { + const cacheLocation = \`\${root.id}:\${candidate.entryDir}\`; + seenCacheLocations.add(cacheLocation); + metrics.filesSeen += 1; + const stat = await statSkillFile(root, candidate, diagnostics); + if (!stat) { + this.cache.delete(cacheLocation); + continue; + } + const cacheKey = \`\${candidate.fileLocation}:\${stat.dev}:\${stat.ino}:\${stat.size}:\${stat.mtimeMs}\`; + const cached = this.cache.get(cacheLocation); + let parsed: CachedSkillFile; + + if (cached?.key === cacheKey) { + metrics.filesReused += 1; + parsed = cached; + } else { + metrics.filesRead += 1; + parsed = await parseSkillFile(root, candidate, stat, cacheKey); + this.cache.set(cacheLocation, parsed); + } + + diagnostics.push(...parsed.diagnostics); + if (parsed.entry) { + entries.push(parsed.entry); + } + }`, + ` for (let offset = 0; offset < candidates.length; offset += 4) { + const results = await Promise.allSettled( + candidates.slice(offset, offset + 4).map(async (candidate) => { + const cacheLocation = \`\${root.id}:\${candidate.entryDir}\`; + const diagnostics: SkillDiagnostic[] = []; + const stat = await statSkillFile(root, candidate, diagnostics); + if (!stat) return { cacheLocation, diagnostics }; + const cacheKey = \`\${candidate.fileLocation}:\${stat.dev}:\${stat.ino}:\${stat.size}:\${stat.mtimeMs}\`; + const cached = this.cache.get(cacheLocation); + const reused = cached?.key === cacheKey; + const parsed = reused ? cached : await parseSkillFile(root, candidate, stat, cacheKey); + return { cacheLocation, diagnostics, parsed, reused }; + }), + ); + for (const result of results) { + if (result.status === 'rejected') throw result.reason; + const value = result.value; + seenCacheLocations.add(value.cacheLocation); + metrics.filesSeen += 1; + diagnostics.push(...value.diagnostics); + if (!value.parsed) { + this.cache.delete(value.cacheLocation); + continue; + } + if (value.reused) { + metrics.filesReused += 1; + } else { + metrics.filesRead += 1; + this.cache.set(value.cacheLocation, value.parsed); + } + diagnostics.push(...value.parsed.diagnostics); + if (value.parsed.entry) entries.push(value.parsed.entry); + } + }`); const digest = name => createHash('sha256').update(readFileSync(join(here, name))).digest('hex'); writeFileSync(join(root, '.oac-native-patch.json'), JSON.stringify({ revision: pin.revision, files: { 'patch-native.mjs':digest('patch-native.mjs'), 'native-subagent-admission.mjs':digest('native-subagent-admission.mjs') } }, null, 2)+'\n'); diff --git a/scripts/build-mcode-harness.sh b/scripts/build-mcode-harness.sh index 236ac4ba3..093c057b2 100644 --- a/scripts/build-mcode-harness.sh +++ b/scripts/build-mcode-harness.sh @@ -63,7 +63,7 @@ test "$(node "$native/cli.js" --version)" = "$version" ) ( cd "$context/upstream" - MCODE_SOURCE="$context/upstream" node --test "$context/native-prompt.test.mjs" "$context/native-mcp-lifecycle.test.mjs" + MCODE_SOURCE="$context/upstream" node --test "$context/native-prompt.test.mjs" "$context/native-mcp-lifecycle.test.mjs" "$context/native-skills.test.mjs" node scripts/build.mjs ) artifact="$context/artifact" From 1f1cf2c5cbeef1b0782d27b7999c7d545643a147 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 9 Oct 2026 19:23:43 +0000 Subject: [PATCH 2/2] Check asynchronous Skill diagnostic ordering --- packages/mcode-harness/native-skills.test.mjs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/mcode-harness/native-skills.test.mjs b/packages/mcode-harness/native-skills.test.mjs index 3b2faf3d2..e0a6d9151 100644 --- a/packages/mcode-harness/native-skills.test.mjs +++ b/packages/mcode-harness/native-skills.test.mjs @@ -82,6 +82,24 @@ test('native Skill refresh bounds independent reads and retains file identity an assert.ok(maximum > 1 && maximum <= 4, `concurrent opens: ${maximum}`); } finally { fs.open = open; } + // Candidate inspection failures finish in reverse order within a batch. + // Preserve the original diagnostic order, not completion order. + const completed = []; + fs.open = async (path, ...args) => { + const name = ['skill-6', 'skill-7'].find(name => String(path) === join(global, name, 'SKILL.md')); + if (!name) return open(path, ...args); + await new Promise(resolve => setTimeout(resolve, name === 'skill-6' ? 20 : 1)); + completed.push(name); + throw Object.assign(new Error('Fixture permission denied'), { code: 'EACCES' }); + }; + try { + const failed = await new SkillRegistry(roots).refresh(); + assert.deepEqual(completed, ['skill-7', 'skill-6']); + const failures = failed.diagnostics.filter(value => value.code === 'skill_stat_failed'); + assert.deepEqual(failures.map(value => value.locationUri), + ['skill-6', 'skill-7'].map(name => changed.entries.find(entry => entry.name === name).locationUri)); + } finally { fs.open = open; } + // The same open-file identity check must still reject replacement between // inspection and content read, even when other files finish independently. let opens = 0;