08ea5e05a9
Memind CI / Test, build, and release guards (pull_request) Has been cancelled
Repair a bash syntax error introduced when adding monitor and DeepSeek proxy phases. Co-authored-by: Cursor <cursoragent@cursor.com>
446 lines
15 KiB
Bash
446 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
# 103 production: Docker goosed pool -> native launchd pool (18006-18014).
|
|
# Keeps goosed-prod-1 container stopped as Docker hot standby.
|
|
#
|
|
# Usage on 103:
|
|
# bash scripts/goosed-native-103-migrate.sh backup
|
|
# bash scripts/goosed-native-103-migrate.sh migrate
|
|
# bash scripts/goosed-native-103-migrate.sh verify
|
|
# bash scripts/goosed-native-103-migrate.sh rollback # emergency Docker standby on 18006 only
|
|
set -euo pipefail
|
|
|
|
export PATH="/opt/homebrew/bin:/usr/local/bin:${PATH}"
|
|
|
|
ROOT="${MEMIND_ROOT:-/Users/john/Project/Memind}"
|
|
GOOSED_NATIVE_ROOT="${GOOSED_NATIVE_ROOT:-/Users/john/Project/tkmind_go-native}"
|
|
GOOSED_PROD_ROOT="${GOOSED_PROD_ROOT:-/Users/john/Project/goosed-prod}"
|
|
PORTS=(18006 18007 18008 18009 18010 18011 18012 18013 18014)
|
|
DOCKER_STANDBY="goosed-prod-1"
|
|
LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
|
|
GUI="gui/$(id -u)"
|
|
BACKUP_ROOT="${ROOT}/backups/goosed-native-migration"
|
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
|
BACKUP_DIR="${BACKUP_ROOT}/pre-native-${STAMP}"
|
|
|
|
log() { printf '[goosed-native-103] %s\n' "$*"; }
|
|
die() { log "ERROR: $*"; exit 1; }
|
|
|
|
require_103() {
|
|
[[ "$(hostname -s 2>/dev/null || hostname)" == "johndeMac-Studio" || "$(hostname)" == *Studio* ]] || true
|
|
[[ -d "${ROOT}" ]] || die "Memind root not found: ${ROOT}"
|
|
}
|
|
|
|
free_disk_headroom() {
|
|
log "Rotating oversized logs to free disk headroom before backup"
|
|
for f in \
|
|
"${HOME}/Library/Logs/memind-agent-run-worker.log" \
|
|
"${HOME}/Library/Logs/memind-runtime-heartbeat.log" \
|
|
"${HOME}/Library/Logs/memind-frontend-5173.log"; do
|
|
if [[ -f "${f}" ]] && [[ "$(wc -c <"${f}" | tr -d ' ')" -gt 50000000 ]]; then
|
|
mv "${f}" "${f}.bak-${STAMP}"
|
|
: >"${f}"
|
|
log "rotated ${f}"
|
|
fi
|
|
done
|
|
df -h "${ROOT}" | tail -1
|
|
}
|
|
|
|
phase_backup() {
|
|
require_103
|
|
free_disk_headroom
|
|
mkdir -p "${BACKUP_DIR}"
|
|
|
|
log "Backup directory: ${BACKUP_DIR}"
|
|
|
|
{
|
|
echo "stamp=${STAMP}"
|
|
echo "hostname=$(hostname)"
|
|
date -Iseconds
|
|
cat "${ROOT}/.release-manifest.txt" 2>/dev/null || true
|
|
grep '^TKMIND_API_TARGETS=' "${ROOT}/.env" || true
|
|
grep '^GOOSED_TAG=' "${GOOSED_PROD_ROOT}/.env" 2>/dev/null || true
|
|
docker ps -a --format '{{.Names}}\t{{.ID}}\t{{.Status}}\t{{.Ports}}' | grep goosed || true
|
|
} >"${BACKUP_DIR}/baseline.txt"
|
|
|
|
cp "${ROOT}/.env" "${BACKUP_DIR}/portal.env"
|
|
cp "${GOOSED_PROD_ROOT}/.env" "${BACKUP_DIR}/goosed-prod.env" 2>/dev/null || true
|
|
cp "${GOOSED_PROD_ROOT}/docker-compose.prod.yml" "${BACKUP_DIR}/" 2>/dev/null || true
|
|
|
|
log "pg_dump memind_sessions"
|
|
local session_url
|
|
session_url="$(grep '^GOOSE_SESSION_DB_URL=' "${GOOSED_PROD_ROOT}/.env" 2>/dev/null | cut -d= -f2- | sed 's|@host.docker.internal|@127.0.0.1|' || true)"
|
|
if [[ -z "${session_url}" ]]; then
|
|
session_url="postgresql://boot:@Abc888888@127.0.0.1:5432/memind_sessions"
|
|
fi
|
|
pg_dump "${session_url}" | gzip -1 >"${BACKUP_DIR}/memind_sessions.sql.gz"
|
|
|
|
log "pg_dump mindspace_userdata_prod"
|
|
local userdata_url
|
|
userdata_url="$(grep '^MINDSPACE_USERDATA_PG_URL=' "${ROOT}/.env" | cut -d= -f2-)"
|
|
pg_dump "${userdata_url}" | gzip -1 >"${BACKUP_DIR}/mindspace_userdata_prod.sql.gz"
|
|
|
|
log "persisted assets tar"
|
|
tar -czf "${BACKUP_DIR}/memind-persisted-${STAMP}.tar.gz" \
|
|
-C "${ROOT}" \
|
|
.env MindSpace data users .tailscale public/plaza-covers logs 2>/dev/null \
|
|
|| tar -czf "${BACKUP_DIR}/memind-persisted-${STAMP}.tar.gz" \
|
|
-C "${ROOT}" .env data users logs 2>/dev/null
|
|
|
|
log "MindSpace storage tar"
|
|
tar -czf "${BACKUP_DIR}/mindspace-storage-${STAMP}.tar.gz" \
|
|
-C /Users/john/MindSpace data/mindspace 2>/dev/null || true
|
|
|
|
log "goosed-prod directory tar"
|
|
tar -czf "${BACKUP_DIR}/goosed-prod-${STAMP}.tar.gz" \
|
|
-C "${GOOSED_PROD_ROOT}" .env docker-compose.prod.yml README.md 2>/dev/null || true
|
|
|
|
log "tkmind_go-native directory tar"
|
|
tar -czf "${BACKUP_DIR}/tkmind_go-native-${STAMP}.tar.gz" \
|
|
-C "${GOOSED_NATIVE_ROOT}" \
|
|
.env.18006 run-goosed-18006.sh deploy releases goosed 2>/dev/null || true
|
|
|
|
log "docker inspect goosed containers"
|
|
docker ps -aq --filter 'name=goosed-prod' | xargs docker inspect >"${BACKUP_DIR}/docker-goosed-inspect.json" 2>/dev/null || true
|
|
|
|
log "full Memind source tar (may take a few minutes)"
|
|
tar -czf "${BACKUP_DIR}/memind-full-${STAMP}.tar.gz" \
|
|
--exclude='./node_modules' \
|
|
--exclude='./backups' \
|
|
--exclude='./.git' \
|
|
-C "${ROOT}" . 2>/dev/null || log "WARN: full tar skipped or partial — check disk space"
|
|
|
|
ln -sfn "${BACKUP_DIR}" "${BACKUP_ROOT}/latest"
|
|
du -sh "${BACKUP_DIR}"/*
|
|
df -h "${ROOT}" | tail -1
|
|
log "Backup complete: ${BACKUP_DIR}"
|
|
}
|
|
|
|
install_native_env_files() {
|
|
local port base="${GOOSED_NATIVE_ROOT}/.env.18006"
|
|
[[ -f "${base}" ]] || die "missing ${base}"
|
|
for port in "${PORTS[@]}"; do
|
|
local env_file="${GOOSED_NATIVE_ROOT}/.env.${port}"
|
|
if [[ ! -f "${env_file}" ]]; then
|
|
sed "s/^GOOSE_PORT=.*/GOOSE_PORT=${port}/" "${base}" >"${env_file}"
|
|
log "created ${env_file}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
install_native_run_script() {
|
|
local script="${GOOSED_NATIVE_ROOT}/run-goosed-native.sh"
|
|
cat >"${script}" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
PORT="${1:?missing port argument}"
|
|
ROOT="/Users/john/Project/tkmind_go-native"
|
|
cd "${ROOT}"
|
|
set -a
|
|
# shellcheck source=/dev/null
|
|
source "${ROOT}/.env.${PORT}"
|
|
set +a
|
|
exec "${ROOT}/goosed" agent
|
|
EOF
|
|
chmod +x "${script}"
|
|
}
|
|
|
|
install_native_launchd() {
|
|
local port label plist
|
|
install_native_run_script
|
|
for port in "${PORTS[@]}"; do
|
|
label="cn.tkmind.goosed-native-${port}"
|
|
plist="${LAUNCHD_DIR}/${label}.plist"
|
|
cat >"${plist}" <<EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>${label}</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>${GOOSED_NATIVE_ROOT}/run-goosed-native.sh</string>
|
|
<string>${port}</string>
|
|
</array>
|
|
<key>WorkingDirectory</key>
|
|
<string>${GOOSED_NATIVE_ROOT}</string>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>KeepAlive</key>
|
|
<true/>
|
|
<key>StandardOutPath</key>
|
|
<string>${HOME}/Library/Logs/goosed-native-${port}.log</string>
|
|
<key>StandardErrorPath</key>
|
|
<string>${HOME}/Library/Logs/goosed-native-${port}.log</string>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
plutil -lint "${plist}" >/dev/null
|
|
launchctl bootout "${GUI}/${label}" 2>/dev/null || true
|
|
launchctl bootstrap "${GUI}" "${plist}"
|
|
launchctl enable "${GUI}/${label}"
|
|
launchctl kickstart -k "${GUI}/${label}" 2>/dev/null || launchctl kickstart "${GUI}/${label}"
|
|
log "launchd ${label} installed"
|
|
done
|
|
}
|
|
|
|
phase_start_deepseek_proxy() {
|
|
local plist="${LAUNCHD_DIR}/cn.tkmind.memind-deepseek-no-think.plist"
|
|
cat >"${plist}" <<EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key><string>cn.tkmind.memind-deepseek-no-think</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/opt/homebrew/opt/node@24/bin/node</string>
|
|
<string>${ROOT}/deepseek-no-think-proxy.mjs</string>
|
|
</array>
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>MEMIND_DEEPSEEK_PROXY_ENTRYPOINT</key><string>1</string>
|
|
<key>MEMIND_DEEPSEEK_DISABLE_THINKING</key><string>1</string>
|
|
<key>MEMIND_DEEPSEEK_NO_THINK_PORT</key><string>18036</string>
|
|
<key>MEMIND_DEEPSEEK_NO_THINK_HOST</key><string>127.0.0.1</string>
|
|
</dict>
|
|
<key>WorkingDirectory</key><string>${ROOT}</string>
|
|
<key>RunAtLoad</key><true/>
|
|
<key>KeepAlive</key><true/>
|
|
<key>StandardOutPath</key><string>${HOME}/Library/Logs/memind-deepseek-no-think.log</string>
|
|
<key>StandardErrorPath</key><string>${HOME}/Library/Logs/memind-deepseek-no-think.log</string>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
launchctl bootout "${GUI}/cn.tkmind.memind-deepseek-compat-candidate" 2>/dev/null || true
|
|
launchctl bootout "${GUI}/cn.tkmind.memind-deepseek-no-think" 2>/dev/null || true
|
|
launchctl bootstrap "${GUI}" "${plist}"
|
|
curl -fsS "http://127.0.0.1:18036/health" >/dev/null || die "DeepSeek no-think proxy :18036 failed"
|
|
log "DeepSeek no-think proxy listening on :18036"
|
|
}
|
|
|
|
phase_reconfigure_monitor() {
|
|
local plist="${LAUNCHD_DIR}/cn.tkmind.goosed-monitor.plist"
|
|
local monitor_script="${ROOT}/scripts/monitor-goosed-fds.mjs"
|
|
[[ -f "${monitor_script}" ]] || die "missing ${monitor_script}"
|
|
cp "${plist}" "${plist}.bak-docker-${STAMP}" 2>/dev/null || true
|
|
cat >"${plist}" <<EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>EnvironmentVariables</key>
|
|
<dict>
|
|
<key>GOOSED_NATIVE_POOL_PORTS</key>
|
|
<string>18006,18007,18008,18009,18010,18011,18012,18013,18014</string>
|
|
<key>GOOSED_NATIVE_LAUNCHD_LABEL_TEMPLATE</key>
|
|
<string>cn.tkmind.goosed-native-{port}</string>
|
|
<key>GOOSED_FD_RESTART</key>
|
|
<string>3200</string>
|
|
<key>GOOSED_FD_WARN</key>
|
|
<string>180</string>
|
|
<key>PATH</key>
|
|
<string>/opt/homebrew/bin:/opt/homebrew/opt/node@24/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
<key>SANDBOX_MCP_MAX_PER_ROOT</key>
|
|
<string>2</string>
|
|
<key>SANDBOX_MCP_MAX_TOTAL</key>
|
|
<string>80</string>
|
|
</dict>
|
|
<key>Label</key>
|
|
<string>cn.tkmind.goosed-monitor</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/opt/homebrew/opt/node@24/bin/node</string>
|
|
<string>${monitor_script}</string>
|
|
</array>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>StandardErrorPath</key>
|
|
<string>${HOME}/Library/Logs/goosed-monitor.log</string>
|
|
<key>StandardOutPath</key>
|
|
<string>${HOME}/Library/Logs/goosed-monitor.log</string>
|
|
<key>StartInterval</key>
|
|
<integer>60</integer>
|
|
<key>WorkingDirectory</key>
|
|
<string>${ROOT}</string>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
plutil -lint "${plist}" >/dev/null
|
|
launchctl bootout "${GUI}/cn.tkmind.goosed-monitor" 2>/dev/null || true
|
|
launchctl bootstrap "${GUI}" "${plist}"
|
|
log "goosed-monitor switched to native FD monitor"
|
|
}
|
|
|
|
stop_docker_pool_keep_standby() {
|
|
local name
|
|
for name in goosed-prod-{1..9}; do
|
|
docker update --restart=no "${name}" 2>/dev/null || true
|
|
done
|
|
for name in goosed-prod-{2..9}; do
|
|
if docker ps -q --filter "name=^/${name}$" | grep -q .; then
|
|
log "stopping docker ${name}"
|
|
docker stop "${name}" >/dev/null
|
|
fi
|
|
done
|
|
if docker ps -q --filter "name=^/${DOCKER_STANDBY}$" | grep -q .; then
|
|
log "stopping docker standby ${DOCKER_STANDBY} (container kept, restart=no)"
|
|
docker stop "${DOCKER_STANDBY}" >/dev/null
|
|
fi
|
|
docker ps -a --filter 'name=goosed-prod' --format '{{.Names}}\t{{.Status}}' || true
|
|
}
|
|
|
|
update_portal_env() {
|
|
local env_file="${ROOT}/.env"
|
|
local targets=""
|
|
local port
|
|
for port in "${PORTS[@]}"; do
|
|
targets="${targets}https://127.0.0.1:${port},"
|
|
done
|
|
targets="${targets%,}"
|
|
|
|
cp "${env_file}" "${env_file}.bak-native-${STAMP}"
|
|
|
|
if grep -q '^GOOSED_RUNTIME=' "${env_file}"; then
|
|
sed -i '' 's/^GOOSED_RUNTIME=.*/GOOSED_RUNTIME=native/' "${env_file}"
|
|
else
|
|
printf '\nGOOSED_RUNTIME=native\n' >>"${env_file}"
|
|
fi
|
|
|
|
sed -i '' "s|^TKMIND_API_TARGETS=.*|TKMIND_API_TARGETS=${targets}|" "${env_file}"
|
|
sed -i '' 's|^TKMIND_API_TARGET=.*|TKMIND_API_TARGET=https://127.0.0.1:18006|' "${env_file}"
|
|
|
|
if grep -q '^GOOSED_MCP_NODE_PATH=' "${env_file}"; then
|
|
sed -i '' 's|^GOOSED_MCP_NODE_PATH=.*|GOOSED_MCP_NODE_PATH=/opt/homebrew/opt/node@24/bin/node|' "${env_file}"
|
|
fi
|
|
if grep -q '^MEMIND_GOOSED_HOST_GATEWAY=' "${env_file}"; then
|
|
sed -i '' 's|^MEMIND_GOOSED_HOST_GATEWAY=.*|MEMIND_GOOSED_HOST_GATEWAY=127.0.0.1|' "${env_file}"
|
|
fi
|
|
if grep -q '^TKMIND_SEARCH_MCP_HOST_GATEWAY=' "${env_file}"; then
|
|
sed -i '' 's|^TKMIND_SEARCH_MCP_HOST_GATEWAY=.*|TKMIND_SEARCH_MCP_HOST_GATEWAY=127.0.0.1|' "${env_file}"
|
|
fi
|
|
|
|
grep -E '^(GOOSED_RUNTIME|TKMIND_API_TARGET|TKMIND_API_TARGETS|GOOSED_MCP_NODE_PATH|MEMIND_GOOSED_HOST_GATEWAY)=' "${env_file}" \
|
|
| tee "${BACKUP_ROOT}/latest-portal-env-snippet.txt" 2>/dev/null || true
|
|
}
|
|
|
|
wait_for_status() {
|
|
local port deadline=$((SECONDS + 90))
|
|
while (( SECONDS < deadline )); do
|
|
if curl -kfsS --connect-timeout 2 "https://127.0.0.1:${port}/status" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
return 1
|
|
}
|
|
|
|
phase_migrate() {
|
|
require_103
|
|
[[ -L "${BACKUP_ROOT}/latest" || -d "${BACKUP_ROOT}" ]] || die "run backup first"
|
|
install_native_env_files
|
|
free_disk_headroom
|
|
stop_docker_pool_keep_standby
|
|
phase_reconfigure_monitor
|
|
phase_start_deepseek_proxy
|
|
install_native_launchd
|
|
sleep 5
|
|
for port in "${PORTS[@]}"; do
|
|
wait_for_status "${port}" || die "native /status failed on ${port}"
|
|
log "${port}: OK"
|
|
done
|
|
update_portal_env
|
|
log "restarting Portal"
|
|
launchctl kickstart -k "${GUI}/cn.tkmind.memind-portal"
|
|
sleep 5
|
|
curl -fsS "http://127.0.0.1:8081/api/status" >/dev/null || die "Portal /api/status failed"
|
|
log "syncing LLM provider to all native targets"
|
|
(
|
|
cd "${ROOT}"
|
|
node <<'NODE'
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { createDbPool } from './db.mjs';
|
|
import { createLlmProviderService } from './llm-providers.mjs';
|
|
|
|
function loadEnv(file) {
|
|
if (!fs.existsSync(file)) return;
|
|
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
const t = line.trim();
|
|
if (!t || t.startsWith('#')) continue;
|
|
const eq = t.indexOf('=');
|
|
if (eq < 0) continue;
|
|
const k = t.slice(0, eq).trim();
|
|
const v = t.slice(eq + 1).trim();
|
|
if (!process.env[k]) process.env[k] = v;
|
|
}
|
|
}
|
|
|
|
loadEnv(path.join(process.cwd(), '.env'));
|
|
const targets = String(process.env.TKMIND_API_TARGETS ?? '')
|
|
.split(',')
|
|
.map((v) => v.trim())
|
|
.filter(Boolean);
|
|
const pool = createDbPool();
|
|
const svc = createLlmProviderService(pool, {
|
|
apiTarget: targets[0] ?? 'https://127.0.0.1:18006',
|
|
apiTargets: targets,
|
|
apiSecret: process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret',
|
|
});
|
|
const synced = await svc.syncSelectedToGoosed();
|
|
if (!synced.ok) {
|
|
console.error('provider sync failed:', synced.message);
|
|
process.exit(1);
|
|
}
|
|
console.log('provider synced to', synced.targets?.join(', '));
|
|
await pool.end();
|
|
NODE
|
|
) || log "WARN: provider sync failed — verify manually"
|
|
log "Migration complete. Docker standby: docker start ${DOCKER_STANDBY} (requires stopping native 18006 first)"
|
|
}
|
|
|
|
phase_verify() {
|
|
require_103
|
|
for port in "${PORTS[@]}"; do
|
|
printf '%s: ' "${port}"
|
|
curl -kfsS --connect-timeout 3 "https://127.0.0.1:${port}/status" && echo || echo FAIL
|
|
done
|
|
echo "---"
|
|
curl -fsS "http://127.0.0.1:8081/api/status" | head -c 400; echo
|
|
echo "---"
|
|
docker ps -a --filter 'name=goosed-prod' --format '{{.Names}}\t{{.Status}}'
|
|
launchctl list | grep 'goosed-native' || true
|
|
}
|
|
|
|
phase_rollback() {
|
|
require_103
|
|
log "Emergency rollback: stop native pool, start Docker standby on 18006 only"
|
|
local port label
|
|
for port in "${PORTS[@]}"; do
|
|
label="cn.tkmind.goosed-native-${port}"
|
|
launchctl bootout "${GUI}/${label}" 2>/dev/null || true
|
|
done
|
|
docker start "${DOCKER_STANDBY}"
|
|
if [[ -f "${ROOT}/.env.bak-native-${STAMP}" ]]; then
|
|
cp "${ROOT}/.env.bak-native-${STAMP}" "${ROOT}/.env"
|
|
elif [[ -f "${BACKUP_ROOT}/latest/portal.env" ]]; then
|
|
cp "${BACKUP_ROOT}/latest/portal.env" "${ROOT}/.env"
|
|
fi
|
|
launchctl kickstart -k "${GUI}/cn.tkmind.memind-portal"
|
|
curl -kfsS --connect-timeout 5 "https://127.0.0.1:18006/status"
|
|
log "Rollback to Docker standby on 18006 done. Full pool: cd goosed-prod && docker compose up -d"
|
|
}
|
|
|
|
cmd="${1:-}"
|
|
case "${cmd}" in
|
|
backup) phase_backup ;;
|
|
migrate) phase_migrate ;;
|
|
verify) phase_verify ;;
|
|
rollback) phase_rollback ;;
|
|
*)
|
|
echo "Usage: $0 {backup|migrate|verify|rollback}" >&2
|
|
exit 2
|
|
;;
|
|
esac
|