fix(mindspace): restore publish guards and safe 103 goosed canary routing

Finish-sync guards now accept SEO/GEO disk enrichment instead of requiring
byte-identical HTML, and fragment writes no longer duplicate doctype. Users
canary mode keeps the full native pool while routing allowlisted identities
to :18015 without overriding memory settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-23 17:44:45 +08:00
parent 5e2f27d622
commit e64cf52f23
9 changed files with 102 additions and 38 deletions
@@ -48,3 +48,21 @@ ssh john@114.85.107.50 'shasum -a 256 /Users/john/Project/tkmind_go-native/relea
3. 新 goosed 二进制(从干净 commit 构建)按 cutover runbook 替换,旧二进制保留在 `releases/`。
每一步都需单独批准。
## 2026-09-23 后续执行记录
| 时间 (UTC+8) | 动作 | 结果 |
|---|---|---|
| 17:34 | Portal 快速发布 `20260923-172809-5e2f27d6`(`main` @ `5e2f27d6`) | 8081/公网验证通过;测试页 `a2fcf61f-6387-49fb-a52b-e3bfdb79131e` |
| 17:34+ | goosed 全池加固 + 滚动重启 ×2 | 9 路 `GOOSED_NOFILE=65536`;二进制 `goose-20260923-v149-m4-508df26f5` |
| 17:34+ | `CODEX_HARNESS_MYSQL` → Portal `scripts/goosed-harness-mysql2-cli.mjs` | harness remember 应恢复(RDS `SELECT 1` 已通过) |
| — | 9 实例池规模 | **维持 9 路**(用户决定暂不缩减) |
| — | `:18015` 独立金丝雀 | `GOOSE_MAX_TURNS=100`、`GOOSE_MAX_TOOL_REPETITIONS=5`(未扩到全池) |
备份:
- `memind-full-20260923-172809-5e2f27d6-before.tar.gz`
- `memind-persisted-20260923-172809-5e2f27d6-before.tar.gz`
- `archived_source=Memind-source-before-20260923-172809-5e2f27d6`
- goosed:`backups/pre-pool-m4-*`、`backups/pre-hardening-*`
回滚 goosed:`GOOSE_BIN=releases/goose-20260923-v149-m3-4aea5abf` + `goose-v149-pool-hardening-103.sh --rollback`(或更早 backup)。
+2 -1
View File
@@ -35,7 +35,8 @@ export function upsertMindspaceCoverMeta(html, coverMeta, { replace = false } =
if (/<html[^>]*>/i.test(source)) {
return source.replace(/<html([^>]*)>/i, `<html$1><head>${metaTag}</head>`);
}
return `<!doctype html><html lang="zh-CN"><head>${metaTag}</head><body>${source}</body></html>`;
const body = source.replace(/^\s*<!doctype[^>]*>\s*/i, '');
return `<!doctype html><html lang="zh-CN"><head>${metaTag}</head><body>${body}</body></html>`;
}
export function normalizeCoverMetaSuggestion(raw) {
+8
View File
@@ -19,3 +19,11 @@ test('upsertMindspaceCoverMeta inserts meta into head when missing', () => {
assert.match(next, /name="mindspace-cover"/);
assert.equal(parseMindspaceCoverMeta(next).tag, '美食');
});
test('upsertMindspaceCoverMeta wraps fragments without duplicating doctype', () => {
const html = '<!doctype html><title>Guarded release</title>';
const next = upsertMindspaceCoverMeta(html, { tag: '精选页面', subtitle: 'Guarded release' });
assert.match(next, /^<!doctype html>/i);
assert.doesNotMatch(next, /<body>\s*<!doctype html>/i);
assert.match(next, /<title>Guarded release<\/title>/);
});
+4 -2
View File
@@ -186,8 +186,9 @@ test('materializeMissingPublicHtmlWrites writes html from sandbox write_file too
const result = materializeMissingPublicHtmlWrites({ messages, publishDir });
assert.deepEqual(result.materialized, ['public/summer-essay.html']);
const saved = fs.readFileSync(path.join(publishDir, 'public/summer-essay.html'), 'utf8');
assert.match(saved, /<!doctype html><title>Summer<\/title>/i);
assert.match(saved, /<title>Summer<\/title>/i);
assert.match(saved, /name="mindspace-geo"/);
assert.doesNotMatch(saved, /<body>\s*<!doctype html>/i);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
@@ -420,8 +421,9 @@ test('materializeMissingPublicHtmlWrites writes html from developer write tool c
const result = materializeMissingPublicHtmlWrites({ messages, publishDir });
assert.deepEqual(result.materialized, ['public/guizhou-guide.html']);
const saved = fs.readFileSync(path.join(publishDir, 'public/guizhou-guide.html'), 'utf8');
assert.match(saved, /<!doctype html><title>Guizhou<\/title>/i);
assert.match(saved, /<title>Guizhou<\/title>/i);
assert.match(saved, /name="mindspace-geo"/);
assert.doesNotMatch(saved, /<body>\s*<!doctype html>/i);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
+34 -7
View File
@@ -99,6 +99,26 @@ const BLOCKED_HOSTS = new Set([
export const DEFAULT_GOOSE_STABLE_TARGET = 'https://127.0.0.1:18006';
export const DEFAULT_GOOSE_V149_TARGET = 'https://127.0.0.1:18049';
export function parseConfiguredPoolTargets(env = process.env) {
const csvTargets = String(env.TKMIND_API_TARGETS ?? '')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
const legacyTargets = [
env.TKMIND_API_TARGET ?? DEFAULT_GOOSE_STABLE_TARGET,
env.TKMIND_API_TARGET_1,
].filter(Boolean);
const seen = new Set();
const targets = [];
for (const raw of [...csvTargets, ...legacyTargets]) {
const normalized = assertLoopbackGooseTarget(raw, 'pool goosed target');
if (seen.has(normalized)) continue;
seen.add(normalized);
targets.push(normalized);
}
return targets;
}
function csvSet(value) {
return new Set(
String(value ?? '')
@@ -159,10 +179,12 @@ export function resolveGooseApiTargetsFromEnv(env = process.env) {
const mode = parseGooseCanaryMode(env);
if (mode === 'off') return null;
const stable = assertLoopbackGooseTarget(
env.TKMIND_API_TARGET_STABLE ?? DEFAULT_GOOSE_STABLE_TARGET,
'TKMIND_API_TARGET_STABLE',
);
const poolTargets = parseConfiguredPoolTargets(env);
const stable = poolTargets[0]
?? assertLoopbackGooseTarget(
env.TKMIND_API_TARGET_STABLE ?? DEFAULT_GOOSE_STABLE_TARGET,
'TKMIND_API_TARGET_STABLE',
);
const v149 = assertLoopbackGooseTarget(
env.TKMIND_API_TARGET_V149 ?? DEFAULT_GOOSE_V149_TARGET,
'TKMIND_API_TARGET_V149',
@@ -186,12 +208,14 @@ export function resolveGooseApiTargetsFromEnv(env = process.env) {
);
}
const targets = [...new Set([...poolTargets, v149])];
return Object.freeze({
mode,
stable,
v149,
primary: stable,
targets: [stable, v149],
targets,
policy,
});
}
@@ -201,8 +225,11 @@ export function resolveGooseTargetForIdentity(identity, config) {
return config?.stable ?? DEFAULT_GOOSE_STABLE_TARGET;
}
if (config.mode === 'all') return config.v149;
const targetKey = resolveCanaryTarget(identity, config.policy);
return targetKey === 'candidate' ? config.v149 : config.stable;
if (config.mode === 'users') {
const targetKey = resolveCanaryTarget(identity, config.policy);
return targetKey === 'candidate' ? config.v149 : null;
}
return config.stable;
}
export function describeGooseCanaryConfig(env = process.env) {
+13 -8
View File
@@ -25,29 +25,34 @@ test('resolveGooseApiTargetsFromEnv all mode pins Portal to v149', () => {
assert.deepEqual(config.targets, ['https://127.0.0.1:18049']);
});
test('resolveGooseApiTargetsFromEnv users mode exposes dual targets', () => {
test('resolveGooseApiTargetsFromEnv users mode keeps full pool and adds v149', () => {
const config = resolveGooseApiTargetsFromEnv({
TKMIND_GOOSE_CANARY: 'users',
TKMIND_GOOSE_CANARY_USER_IDS: 'user-1',
TKMIND_API_TARGETS: 'https://127.0.0.1:18006,https://127.0.0.1:18007,https://127.0.0.1:18008',
TKMIND_API_TARGET_V149: 'https://127.0.0.1:18015',
});
assert.equal(config.mode, 'users');
assert.equal(config.primary, 'https://127.0.0.1:18006');
assert.deepEqual(config.targets, ['https://127.0.0.1:18006', 'https://127.0.0.1:18049']);
assert.deepEqual(config.targets, [
'https://127.0.0.1:18006',
'https://127.0.0.1:18007',
'https://127.0.0.1:18008',
'https://127.0.0.1:18015',
]);
});
test('resolveGooseTargetForIdentity routes canary users to v149', () => {
test('resolveGooseTargetForIdentity routes canary users to v149 only', () => {
const config = resolveGooseApiTargetsFromEnv({
TKMIND_GOOSE_CANARY: 'users',
TKMIND_GOOSE_CANARY_USER_IDS: 'user-abc',
TKMIND_API_TARGET_V149: 'https://127.0.0.1:18015',
});
assert.equal(
resolveGooseTargetForIdentity({ id: 'user-abc' }, config),
'https://127.0.0.1:18049',
);
assert.equal(
resolveGooseTargetForIdentity({ id: 'other-user' }, config),
'https://127.0.0.1:18006',
'https://127.0.0.1:18015',
);
assert.equal(resolveGooseTargetForIdentity({ id: 'other-user' }, config), null);
});
test('assertLoopbackGooseTarget refuses production hosts', () => {
+12 -12
View File
@@ -1,12 +1,13 @@
#!/usr/bin/env bash
# Toggle Goose v1.49 canary routing on 103 stable Portal (.env).
# users mode keeps the full native pool in TKMIND_API_TARGETS and only routes
# allowlisted identities to TKMIND_API_TARGET_V149 (:18015 by default).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ENV_FILE="${GOOSE_V149_CANARY_ENV_FILE:-/Users/john/Project/Memind/.env}"
MARKER_BEGIN='# GOOSE_V149_CANARY_BEGIN'
MARKER_END='# GOOSE_V149_CANARY_END'
STABLE_TARGET="${TKMIND_API_TARGET_STABLE:-https://127.0.0.1:18006}"
V149_TARGET="${TKMIND_API_TARGET_V149:-https://127.0.0.1:18015}"
PORTAL_LABEL="${MEMIND_PORTAL_LABEL:-cn.tkmind.memind-portal}"
GUI="gui/$(id -u)"
@@ -16,12 +17,16 @@ usage() {
Usage: bash scripts/switch-goose-v149-canary-103.sh <command>
Commands:
on [all|users] Route Portal to v1.49 canary (${V149_TARGET})
on [all|users] Route allowlisted users (or everyone in all mode) to v1.49 canary (${V149_TARGET})
off|rollback Remove canary block from ${ENV_FILE}
status Show active block
users mode example (唐 only):
TKMIND_GOOSE_CANARY_WECHAT_USER_IDS=wx_ul610et8 bash scripts/switch-goose-v149-canary-103.sh on users
users mode example (john only):
TKMIND_GOOSE_CANARY_USERNAMES=john bash scripts/switch-goose-v149-canary-103.sh on users
Notes:
- Does not rewrite TKMIND_API_TARGETS or memory settings.
- all mode is intended for local loopback canary only; avoid on the 9-instance 103 pool.
EOF
}
@@ -45,22 +50,14 @@ write_canary_block() {
{
echo "${MARKER_BEGIN}"
echo "TKMIND_GOOSE_CANARY=${mode}"
echo "TKMIND_API_TARGET_STABLE=${STABLE_TARGET}"
echo "TKMIND_API_TARGET_V149=${V149_TARGET}"
if [[ "${mode}" == "all" ]]; then
echo "TKMIND_API_TARGET=${V149_TARGET}"
elif [[ "${mode}" == "users" ]]; then
echo "TKMIND_API_TARGET=${STABLE_TARGET}"
echo "TKMIND_API_TARGETS=${STABLE_TARGET},${V149_TARGET}"
[[ -n "${TKMIND_GOOSE_CANARY_USER_IDS:-}" ]] && echo "TKMIND_GOOSE_CANARY_USER_IDS=${TKMIND_GOOSE_CANARY_USER_IDS}"
[[ -n "${TKMIND_GOOSE_CANARY_USERNAMES:-}" ]] && echo "TKMIND_GOOSE_CANARY_USERNAMES=${TKMIND_GOOSE_CANARY_USERNAMES}"
[[ -n "${TKMIND_GOOSE_CANARY_WECHAT_USER_IDS:-}" ]] && echo "TKMIND_GOOSE_CANARY_WECHAT_USER_IDS=${TKMIND_GOOSE_CANARY_WECHAT_USER_IDS}"
fi
echo "MEMORY_BACKEND=legacy"
echo "MEMORY_VECTOR_ENABLED=0"
echo "MEMORY_AGENT_INJECTION_MODE=off"
echo "MEMORY_LIFECYCLE_ENABLED=0"
echo "MEMORY_CANDIDATE_ENABLED=0"
echo "${MARKER_END}"
} >> "${ENV_FILE}"
}
@@ -79,6 +76,9 @@ case "${cmd}" in
echo "Missing ${ENV_FILE}" >&2
exit 1
fi
if [[ "${mode}" == "all" ]]; then
echo "[goose-v149-canary-103] WARN: all mode replaces the entire pool with v149; use users mode on 103 production" >&2
fi
if [[ "${mode}" == "users" && -z "${TKMIND_GOOSE_CANARY_USER_IDS:-}${TKMIND_GOOSE_CANARY_USERNAMES:-}${TKMIND_GOOSE_CANARY_WECHAT_USER_IDS:-}" ]]; then
echo "users mode requires identity allowlist env vars" >&2
exit 2
+10 -5
View File
@@ -12,6 +12,10 @@ const root = path.resolve(new URL('..', import.meta.url).pathname);
const runtimeServer = path.join(root, '.runtime', 'portal', 'server.mjs');
const currentUser = { id: 'a6fb1e97-2b0f-447b-b138-4561d8e5c53e', username: 'john' };
const RAW_HTML =
'<!doctype html><html lang="zh-CN"><head><title>Guarded release</title></head>'
+ '<body><h1>Guarded release</h1></body></html>';
const toolRequestOnlyMessages = [
{
role: 'assistant',
@@ -23,7 +27,7 @@ const toolRequestOnlyMessages = [
name: 'write',
arguments: {
path: 'public/guarded-release.html',
content: '<!doctype html><title>Guarded release</title>',
content: RAW_HTML,
},
},
},
@@ -49,10 +53,11 @@ try {
publishDir,
});
assert.deepEqual(result.materialized, ['public/guarded-release.html']);
assert.equal(
fs.readFileSync(path.join(publishDir, 'public/guarded-release.html'), 'utf8'),
'<!doctype html><title>Guarded release</title>',
);
const written = fs.readFileSync(path.join(publishDir, 'public/guarded-release.html'), 'utf8');
assert.ok(written.includes('<title>Guarded release</title>'));
assert.ok(written.includes('<h1>Guarded release</h1>'));
assert.ok(/data-mindspace-seo-outline=|name="mindspace-geo"/i.test(written));
assert.doesNotMatch(written, /<body>\s*<!doctype html>/i);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
+1 -3
View File
@@ -1395,9 +1395,7 @@ export function createTkmindProxy({
},
gooseCanaryConfig,
);
if (target && (targets.includes(target) || target === gooseCanaryConfig.v149 || target === gooseCanaryConfig.stable)) {
if (await targetHealthy(target)) return target;
}
if (target && await targetHealthy(target)) return target;
}
return pickTarget();
}