diff --git a/docs/incidents/2026-09-23-goose-v149-prod-log-audit.md b/docs/incidents/2026-09-23-goose-v149-prod-log-audit.md
index d92edd2..6ff100a 100644
--- a/docs/incidents/2026-09-23-goose-v149-prod-log-audit.md
+++ b/docs/incidents/2026-09-23-goose-v149-prod-log-audit.md
@@ -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)。
diff --git a/mindspace-cover-meta.mjs b/mindspace-cover-meta.mjs
index 0961628..647c92e 100644
--- a/mindspace-cover-meta.mjs
+++ b/mindspace-cover-meta.mjs
@@ -35,7 +35,8 @@ export function upsertMindspaceCoverMeta(html, coverMeta, { replace = false } =
if (/]*>/i.test(source)) {
return source.replace(/]*)>/i, `
${metaTag}`);
}
- return `${metaTag}${source}`;
+ const body = source.replace(/^\s*]*>\s*/i, '');
+ return `${metaTag}${body}`;
}
export function normalizeCoverMetaSuggestion(raw) {
diff --git a/mindspace-cover-meta.test.mjs b/mindspace-cover-meta.test.mjs
index 921ac43..1491bcc 100644
--- a/mindspace-cover-meta.test.mjs
+++ b/mindspace-cover-meta.test.mjs
@@ -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 = 'Guarded release';
+ const next = upsertMindspaceCoverMeta(html, { tag: '精选页面', subtitle: 'Guarded release' });
+ assert.match(next, /^/i);
+ assert.doesNotMatch(next, /\s*/i);
+ assert.match(next, /Guarded release<\/title>/);
+});
diff --git a/mindspace-public-finish-sync.test.mjs b/mindspace-public-finish-sync.test.mjs
index 4290424..5a878f8 100644
--- a/mindspace-public-finish-sync.test.mjs
+++ b/mindspace-public-finish-sync.test.mjs
@@ -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, /Summer<\/title>/i);
+ assert.match(saved, /Summer<\/title>/i);
assert.match(saved, /name="mindspace-geo"/);
+ assert.doesNotMatch(saved, /\s*/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, /Guizhou<\/title>/i);
+ assert.match(saved, /Guizhou<\/title>/i);
assert.match(saved, /name="mindspace-geo"/);
+ assert.doesNotMatch(saved, /\s*/i);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
diff --git a/scripts/goose-v149-canary.mjs b/scripts/goose-v149-canary.mjs
index 02ac3ae..d16bde6 100644
--- a/scripts/goose-v149-canary.mjs
+++ b/scripts/goose-v149-canary.mjs
@@ -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) {
diff --git a/scripts/goose-v149-canary.test.mjs b/scripts/goose-v149-canary.test.mjs
index ca24823..86ee80f 100644
--- a/scripts/goose-v149-canary.test.mjs
+++ b/scripts/goose-v149-canary.test.mjs
@@ -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', () => {
diff --git a/scripts/switch-goose-v149-canary-103.sh b/scripts/switch-goose-v149-canary-103.sh
index ce82a74..742f2bf 100755
--- a/scripts/switch-goose-v149-canary-103.sh
+++ b/scripts/switch-goose-v149-canary-103.sh
@@ -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
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
diff --git a/scripts/verify-public-finish-sync-runtime.mjs b/scripts/verify-public-finish-sync-runtime.mjs
index 17bef61..fcebedf 100644
--- a/scripts/verify-public-finish-sync-runtime.mjs
+++ b/scripts/verify-public-finish-sync-runtime.mjs
@@ -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 =
+ 'Guarded release'
+ + 'Guarded release
';
+
const toolRequestOnlyMessages = [
{
role: 'assistant',
@@ -23,7 +27,7 @@ const toolRequestOnlyMessages = [
name: 'write',
arguments: {
path: 'public/guarded-release.html',
- content: 'Guarded release',
+ 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'),
- 'Guarded release',
- );
+ const written = fs.readFileSync(path.join(publishDir, 'public/guarded-release.html'), 'utf8');
+ assert.ok(written.includes('Guarded release'));
+ assert.ok(written.includes('Guarded release
'));
+ assert.ok(/data-mindspace-seo-outline=|name="mindspace-geo"/i.test(written));
+ assert.doesNotMatch(written, /\s*/i);
} finally {
fs.rmSync(publishDir, { recursive: true, force: true });
}
diff --git a/tkmind-proxy.mjs b/tkmind-proxy.mjs
index 00f8ae6..2272039 100644
--- a/tkmind-proxy.mjs
+++ b/tkmind-proxy.mjs
@@ -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();
}