fix(prod): dedupe scheduled tasks and reconcile orphan delivery contracts
Memind CI / Test, build, and release guards (push) Successful in 2m58s

Prevent duplicate active scheduled tasks, fail static preparing contracts
when HTML is missing, raise MindSpace remote timeout default to 30s, and
add 103 repair scripts for inspection follow-ups.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-16 08:52:16 +08:00
parent f20302fe65
commit 843e4d1002
8 changed files with 296 additions and 5 deletions
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="${GOOSED_NATIVE_ROOT:-/Users/john/Project/tkmind_go-native}"
LAUNCHD_DIR="${HOME}/Library/LaunchAgents"
GUI="gui/$(id -u)"
PATH_EXPORT='export PATH="/opt/homebrew/opt/node@24/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"'
log() { printf '[repair-goosed-harness-path] %s\n' "$*"; }
patch_run_script() {
local script="${ROOT}/run-goosed-native.sh"
[[ -f "${script}" ]] || { log "missing ${script}"; exit 1; }
if grep -q 'opt/homebrew/opt/node@24/bin' "${script}"; then
log "run-goosed-native.sh already exports node PATH"
return 0
fi
cp "${script}" "${script}.bak-inspection-$(date +%Y%m%d-%H%M%S)"
awk -v path_line="${PATH_EXPORT}" '
/^set -euo pipefail$/ {
print
print path_line
next
}
{ print }
' "${script}" > "${script}.tmp"
mv "${script}.tmp" "${script}"
chmod +x "${script}"
log "patched ${script}"
}
patch_launchd_plist() {
local port="$1"
local plist="${LAUNCHD_DIR}/cn.tkmind.goosed-native-${port}.plist"
[[ -f "${plist}" ]] || return 0
if plutil -extract EnvironmentVariables raw "${plist}" >/dev/null 2>&1; then
plutil -replace EnvironmentVariables.PATH -string "/opt/homebrew/opt/node@24/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" "${plist}"
else
plutil -insert EnvironmentVariables -dictionary "${plist}"
plutil -insert EnvironmentVariables.PATH -string "/opt/homebrew/opt/node@24/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" "${plist}"
fi
launchctl bootout "${GUI}/cn.tkmind.goosed-native-${port}" 2>/dev/null || true
launchctl bootstrap "${GUI}" "${plist}"
log "reloaded ${plist}"
}
patch_run_script
for port in 18006 18007 18008 18009 18010 18011 18012 18013 18014; do
patch_launchd_plist "${port}"
done
log "done"
@@ -0,0 +1,105 @@
#!/usr/bin/env node
/**
* One-shot 103 production repairs from 2026-08-16 inspection.
* Dry-run by default; pass --apply to mutate database rows.
*/
import process from 'node:process';
import { reconcileStuckStaticPageDeliveryContracts } from '../scheduled-task-executor.mjs';
const TANG_USER_ID = 'a70ff537-8908-486e-9b6c-042e07cc25db';
const TASKS_TO_CANCEL = [
'781a77e5-a787-4f37-b864-e21d7e918855',
'9df3f89a-3a06-434d-8b4c-17b1aeb8911f',
'7f953d5f-6fc3-4191-b52a-bffcef48e77f',
'9b537fe8-eea2-4e39-8bcb-e8f169986327',
];
function loadPool() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error('缺少 DATABASE_URL');
}
return import('mysql2/promise').then(({ default: mysql }) =>
mysql.createPool({ uri: databaseUrl, connectionLimit: 2 }));
}
async function main() {
const apply = process.argv.includes('--apply');
const h5Root = process.env.H5_ROOT || '/Users/john/Project/Memind';
const pool = await loadPool();
const now = Date.now();
const actions = [];
for (const taskId of TASKS_TO_CANCEL) {
const [rows] = await pool.query(
`SELECT id, user_id, title, status, hour, minute
FROM h5_scheduled_tasks WHERE id = ? LIMIT 1`,
[taskId],
);
const row = rows?.[0];
if (!row) {
actions.push({ taskId, action: 'skip', reason: 'not_found' });
continue;
}
if (row.status === 'cancelled') {
actions.push({ taskId, action: 'skip', reason: 'already_cancelled', title: row.title });
continue;
}
actions.push({
taskId,
action: apply ? 'cancel' : 'would_cancel',
title: row.title,
status: row.status,
schedule: `${row.hour ?? '?'}:${String(row.minute ?? 0).padStart(2, '0')}`,
});
if (apply) {
await pool.query(
`UPDATE h5_scheduled_tasks
SET status = 'cancelled', updated_at = ?, last_error = NULL
WHERE id = ? AND user_id = ?`,
[now, taskId, TANG_USER_ID],
);
}
}
const [activeWeather] = await pool.query(
`SELECT id, title, hour, minute, status, last_run_at
FROM h5_scheduled_tasks
WHERE user_id = ? AND title LIKE '%天气预报%' AND status = 'active'
ORDER BY hour, created_at`,
[TANG_USER_ID],
);
let reconcileSummary = [];
if (apply) {
reconcileSummary = await reconcileStuckStaticPageDeliveryContracts({
pool,
h5Root,
limit: 50,
logger: console,
});
} else {
const [stuck] = await pool.query(
`SELECT user_id, workspace_relative_path, data_mode, status
FROM h5_page_delivery_contracts
WHERE status = 'preparing' AND data_mode = 'static'
ORDER BY updated_at ASC
LIMIT 50`,
);
reconcileSummary = stuck;
}
console.log(JSON.stringify({
mode: apply ? 'apply' : 'dry-run',
cancelledTasks: actions,
remainingActiveWeatherTasks: activeWeather,
deliveryContracts: reconcileSummary,
}, null, 2));
await pool.end();
}
main().catch((error) => {
console.error(error);
process.exit(1);
});