Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/.size-baseline
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
22037 release-sdk-python.yml
19094 release-sdk.yml
14546 release-vscode-companion.yml
59855 release.yml
62799 release.yml
43717 repo-hygiene.yml
1079 scorecard-monthly.yml
10691 sdk-java.yml
Expand Down
24 changes: 23 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ jobs:
RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}'
CI_BOT_PAT: '${{ secrets.CI_BOT_PAT }}'
run: |-
git add package.json package-lock.json packages/*/package.json packages/channels/*/package.json integrations/*/package.json
git add package.json package-lock.json packages/*/package.json packages/channels/*/package.json integrations/*/package.json integrations/*/qwen-extension.json
if git diff --staged --quiet; then
echo "No version changes to commit"
else
Expand Down Expand Up @@ -942,6 +942,28 @@ jobs:
QWEN_STANDALONE_REQUIRE_AUDIO_CAPTURE_PREBUILD: "${{ github.repository == 'QwenLM/qwen-code' && '1' || '' }}"
run: 'npm run package:standalone:release -- --version "${RELEASE_VERSION}" --out-dir dist/standalone'

# npm trusted publishing can only be configured after a package exists.
# Keep this disabled until the one-time bootstrap publish and trusted
# publisher setup are complete.
- name: 'Publish @qwen-code/external-context-mem0'
if: |-
${{ github.repository == 'QwenLM/qwen-code' && vars.NPM_EXTERNAL_CONTEXT_MEM0_TRUSTED_PUBLISHING_ENABLED == 'true' }}
Comment thread
doudouOUC marked this conversation as resolved.
working-directory: 'integrations/external-context-mem0'
run: |-
PACKAGE_NAME="$(node -p "require('./package.json').name")"
PUBLISH_ARGS=(--access public "--tag=${NPM_TAG}")
if [[ "${IS_DRY_RUN}" == "true" ]]; then
PUBLISH_ARGS+=(--dry-run)
elif npm view "${PACKAGE_NAME}@${RELEASE_VERSION}" version >/dev/null 2>&1; then
echo "::notice::${PACKAGE_NAME}@${RELEASE_VERSION} already published; skipping"
exit 0
fi
npm publish --provenance "${PUBLISH_ARGS[@]}"
env:
RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}'
NPM_TAG: '${{ needs.prepare.outputs.npm_tag }}'
IS_DRY_RUN: '${{ needs.prepare.outputs.is_dry_run }}'

- name: 'Publish @qwen-code/audio-capture'
if: |-
${{ github.repository == 'QwenLM/qwen-code' }}
Expand Down
24 changes: 23 additions & 1 deletion docs/design/external-context-mem0-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,26 @@ The npm package publishes only the bundled runtime, canonical schemas,
Extension manifest, and README. It contains no administrator dialect, provider
preset, provider identifier, or provider-specific contract fixture.

The public package name is `@qwen-code/external-context-mem0`. Its package and
Extension manifest versions follow the Qwen Code release version and are
updated in the same release commit. Administrators can install the latest
release with `qwen extensions install @qwen-code/external-context-mem0` or pin
an explicit npm version. Installation never creates an instance file, dialect
file, credential, or ordinary Qwen setting.

The normal release workflow builds the self-contained bundle, checks whether
the exact package version already exists, and publishes with the release's npm
dist-tag and provenance. The package participates in the shared
already-published guard so a partial release cannot be overwritten by a retry.
Because npm requires a package to exist before trusted publishing can be
configured, the publish step remains behind the
`NPM_EXTERNAL_CONTEXT_MEM0_TRUSTED_PUBLISHING_ENABLED` repository variable
until a maintainer completes the one-time public bootstrap publish and binds
the package to the `release.yml` workflow in the `production-release`
environment. The bootstrap should publish the first actual Qwen release that
contains this change, not invent a second version line. Future releases use
trusted publishing and require no npm token.

When a service fits `DialectV1`, its administrator writes and validates a local
dialect file. When it does not fit, the service owner publishes a separate MCP
Extension implementing External Context MCP Profile v1. Qwen does not add a
Expand All @@ -243,7 +263,9 @@ built-in provider rollout for either case.
3. **PR2:** Replace the unused registry with administrator-owned
`InstanceConfigV2` and `DialectV1` files, while preserving the request engine
and profile boundary.
4. Design any portable write capability separately.
4. **Distribution follow-up:** Publish the self-contained Extension through the
normal Qwen Code npm release without adding provider data or Core wiring.
5. Design any portable write capability separately.

There is no Qwen-maintained provider-preset PR3. Administrators own compatible
dialect data; incompatible protocols use their own MCP Extension.
Expand Down
21 changes: 21 additions & 0 deletions integrations/external-context-mem0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ for administrator-configured Mem0-compatible HTTP services. It validates a
closed dialect grammar and uses a bounded HTTP request engine; it does not ship
provider presets or provider-specific configuration.

## Installation

Install the published Extension with Qwen Code:

```bash
qwen extensions install @qwen-code/external-context-mem0
```

This command becomes available after the package's first registry release.

Use an explicit package version when the deployment must remain pinned:

```bash
qwen extensions install @qwen-code/external-context-mem0@x.y.z
```

The Extension version follows the Qwen Code release version. Installing the
package does not configure a memory service or install provider data; an
administrator must supply the two files and credential environment described
below.

## Configuration

Set `QWEN_EXTERNAL_CONTEXT_MEM0_CONFIG` in the Extension process environment to
Expand Down
6 changes: 5 additions & 1 deletion integrations/external-context-mem0/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
{
"name": "@qwen-code/external-context-mem0",
"version": "0.22.3",
"private": true,
"description": "Configurable Mem0-compatible External Context Extension",
"repository": {
"type": "git",
"url": "git+https://github.com/QwenLM/qwen-code.git",
"directory": "integrations/external-context-mem0"
},
"type": "module",
"engines": {
"node": ">=22.0.0"
Expand Down
2 changes: 1 addition & 1 deletion integrations/external-context-mem0/qwen-extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"en": "Configurable retrieval from an administrator-bound Mem0-compatible service",
"zh": "从管理员绑定的 Mem0 兼容服务检索外部上下文"
},
"version": "0.1.0",
"version": "0.22.3",
"mcpServers": {
"external-context-mem0": {
"command": "node",
Expand Down
17 changes: 17 additions & 0 deletions integrations/external-context-mem0/src/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ describe('Mem0 Extension package', () => {
expect(packageJson.scripts?.['build']).toContain('--bundle');
expect(packageJson.files).toContain('dist/main.js');
expect(packageJson.dependencies).toBeUndefined();
expect(packageJson.private).not.toBe(true);
expect(packageJson.name).toBe('@qwen-code/external-context-mem0');
expect(packageJson.version).toBe(manifest.version);
expect(packageJson.repository).toEqual({
type: 'git',
url: 'git+https://github.com/QwenLM/qwen-code.git',
directory: 'integrations/external-context-mem0',
});
});

it('ships only the runtime, schemas, manifest, and documentation', async () => {
Expand All @@ -43,11 +51,20 @@ describe('Mem0 Extension package', () => {
});

interface Manifest {
version?: string;
mcpServers?: Record<string, Record<string, unknown>>;
settings?: unknown;
}

interface PackageJson {
name?: string;
version?: string;
private?: boolean;
repository?: {
type?: string;
url?: string;
directory?: string;
};
dependencies?: Record<string, string>;
scripts?: Record<string, string>;
files?: string[];
Expand Down
1 change: 1 addition & 0 deletions scripts/get-release-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ function detectRollbackAndGetBaseline(npmDistTag) {
*/
export const PUBLISHED_PACKAGES = [
'@qwen-code/qwen-code',
'@qwen-code/external-context-mem0',
'@qwen-code/audio-capture',
'@qwen-code/channel-base',
'@qwen-code/channel-dingtalk',
Expand Down
1 change: 1 addition & 0 deletions scripts/tests/get-release-version.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,7 @@ describe('assertVersionUnreleased', () => {
// release.yml so every consumer is reviewed together.
expect(PUBLISHED_PACKAGES).toEqual([
'@qwen-code/qwen-code',
'@qwen-code/external-context-mem0',
Comment thread
doudouOUC marked this conversation as resolved.
'@qwen-code/audio-capture',
'@qwen-code/channel-base',
'@qwen-code/channel-dingtalk',
Expand Down
28 changes: 28 additions & 0 deletions scripts/tests/package-scripts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,27 @@ describe('package scripts', () => {
expect(buildOrder).toContain("'packages/qwen-live',");
});

it('keeps the Mem0 Extension manifest aligned with release versions', () => {
Comment thread
doudouOUC marked this conversation as resolved.
const versionScript = readFileSync(
path.join(root, 'scripts/version.js'),
'utf8',
);

expect(versionScript).toContain(
"'integrations/external-context-mem0/qwen-extension.json'",
);
expect(versionScript).toContain(
'const mem0Manifest = readJson(mem0ManifestPath);',
);
expect(versionScript).toContain('mem0Manifest.version = newVersion');
Comment thread
doudouOUC marked this conversation as resolved.
Comment thread
doudouOUC marked this conversation as resolved.
expect(versionScript).toContain(
'writeJson(mem0ManifestPath, mem0Manifest);',
);
expect(versionScript).toContain(
"'npx prettier --experimental-cli --write integrations/external-context-mem0/qwen-extension.json'",
);
});

it('keeps the serve fast-path bundle check outside unit test scripts', () => {
const packageJson = readPackageJson();

Expand Down Expand Up @@ -515,6 +536,7 @@ describe('package scripts', () => {
const publishJob = getWorkflowJob(workflow, 'publish');

for (const stepName of [
'Publish @qwen-code/external-context-mem0',
'Publish @qwen-code/audio-capture',
'Publish @qwen-code/qwen-code',
'Publish @qwen-code/channel-base',
Expand Down Expand Up @@ -554,6 +576,11 @@ describe('package scripts', () => {

it('meets npm trusted publishing requirements', () => {
for (const [workflowPath, jobName, publishStepName] of [
[
'.github/workflows/release.yml',
'publish',
'Publish @qwen-code/external-context-mem0',
],
[
'.github/workflows/release.yml',
'publish',
Expand Down Expand Up @@ -589,6 +616,7 @@ describe('package scripts', () => {
}

for (const packageDirectory of [
'integrations/external-context-mem0',
'packages/audio-capture',
'packages/cli',
'packages/channels/base',
Expand Down
20 changes: 19 additions & 1 deletion scripts/tests/release-workflow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1305,10 +1305,28 @@ describe('release workflow', () => {

it('stages every integration package manifest after versioning', () => {
expect(workflow).toContain(
'git add package.json package-lock.json packages/*/package.json packages/channels/*/package.json integrations/*/package.json',
'git add package.json package-lock.json packages/*/package.json packages/channels/*/package.json integrations/*/package.json integrations/*/qwen-extension.json',
);
});

it('publishes the Mem0 Extension only after trusted publishing bootstrap', () => {
Comment thread
doudouOUC marked this conversation as resolved.
const publishSteps = releaseYaml.jobs.publish.steps;
const mem0Step = publishSteps.find(
(step) => step.name === 'Publish @qwen-code/external-context-mem0',
);
const audioStepIndex = publishSteps.findIndex(
(step) => step.name === 'Publish @qwen-code/audio-capture',
);

expect(mem0Step.if).toContain(
"vars.NPM_EXTERNAL_CONTEXT_MEM0_TRUSTED_PUBLISHING_ENABLED == 'true'",
);
expect(mem0Step['working-directory']).toBe(
'integrations/external-context-mem0',
);
expect(publishSteps.indexOf(mem0Step)).toBeLessThan(audioStepIndex);
});

it('fires the fleet-moving npm-published dispatch on stable releases only', () => {
// This gate is the sole protection keeping a nightly/preview/dry-run
// release from moving the ECS fleet; the triggered update workflow
Expand Down
22 changes: 17 additions & 5 deletions scripts/version.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,19 @@ for (const workspaceName of workspacesToVersion) {
const rootPackageJsonPath = resolve(process.cwd(), 'package.json');
const newVersion = readJson(rootPackageJsonPath).version;

// 5. Update the sandboxImageUri in the root package.json
// 5. Keep the published Mem0 Extension manifest aligned with its package.
const mem0ManifestPath = resolve(
process.cwd(),
'integrations/external-context-mem0/qwen-extension.json',
);
const mem0Manifest = readJson(mem0ManifestPath);
Comment thread
doudouOUC marked this conversation as resolved.
mem0Manifest.version = newVersion;
writeJson(mem0ManifestPath, mem0Manifest);
run(
'npx prettier --experimental-cli --write integrations/external-context-mem0/qwen-extension.json',
);

// 6. Update the sandboxImageUri in the root package.json
const rootPackageJson = readJson(rootPackageJsonPath);
if (rootPackageJson.config?.sandboxImageUri) {
rootPackageJson.config.sandboxImageUri =
Expand All @@ -78,7 +90,7 @@ if (rootPackageJson.config?.sandboxImageUri) {
writeJson(rootPackageJsonPath, rootPackageJson);
}

// 6. Update the sandboxImageUri in the cli package.json
// 7. Update the sandboxImageUri in the cli package.json
const cliPackageJsonPath = resolve(process.cwd(), 'packages/cli/package.json');
const cliPackageJson = readJson(cliPackageJsonPath);
if (cliPackageJson.config?.sandboxImageUri) {
Expand All @@ -90,7 +102,7 @@ if (cliPackageJson.config?.sandboxImageUri) {
writeJson(cliPackageJsonPath, cliPackageJson);
}

// 7. Pin channel adapters' semver dependency on @qwen-code/channel-base to
// 8. Pin channel adapters' semver dependency on @qwen-code/channel-base to
// the exact new version. A caret range like ^0.21.0 does not match a
// prerelease bump (e.g. 0.21.1-preview.0), so npm would replace the workspace
// link with the stale registry package and the release build would compile
Expand All @@ -110,14 +122,14 @@ for (const entry of readdirSync(channelsDir)) {
}
}

// 8. Refresh node_modules and package-lock.json against the pinned exact
// 9. Refresh node_modules and package-lock.json against the pinned exact
// versions so the adapters resolve channel-base to the workspace link again.
// --ignore-scripts prevents the root `prepare` lifecycle from triggering a
// redundant full build that fails with TS5055 when dist/ already exists from
// the initial `npm ci` install.
run('npm install --ignore-scripts');

// 9. The per-workspace `npm version` reifies above nested a stale registry
// 10. The per-workspace `npm version` reifies above nested a stale registry
// copy of channel-base under each adapter while ranges briefly mismatched.
// The install above cleans both lockfiles but can leave that directory on
// disk, where it shadows the workspace link during tsc. Remove it.
Expand Down
Loading