Make bmem Gitea sync failures trigger immediate server repair and acceptance

This commit is contained in:
bowei
2026-09-15 18:24:08 +08:00
parent a36cbfda2a
commit 84fbcae620
3 changed files with 94 additions and 7 deletions
+9 -1
View File
@@ -1,6 +1,6 @@
--- ---
name: bmem name: bmem
description: 用 bmem 记忆系统读写跨机器、跨 agent 的长期记忆——判断一条信息该进全局/项目/机器/log 哪一层、把 ./Notes 下的 PRxx/Taskxx/Resultxx 导入记忆、会话收尾该沉淀什么、怎么查以前做过什么、以及 bmem 命令报错时怎么修。当用户提到"记住"、"记一下"、"存到记忆里"、"之前/以前是不是做过"、"上次怎么解决的"、"查一下历史"、"这台机器装了什么"、"这个项目在别的机器上在哪"、"账号密码记下来"、"同步记忆"、"这个项目不做了/从记忆里删掉"、"bmem"、"记忆系统",或者在一个根目录有 bmem_<prjID>.md 的项目里开始实质性工作、需要知道过去的决策和踩过的坑时,必须使用此技能——即使用户没有说"skill"或"bmem"两个字。用户改完 ./Notes 下的需求/方案/结果文档、或一段实质性工作收尾时也要用它。bmem 命令报错(未加入 bmem / 不在项目内 / PG 不可达 / git 冲突 / 重复注册)时同样用它。 description: 用 bmem 管理跨机器长期记忆、导入 Notes,并处理 bmem sync 或 Gitea 同步故障;当用户要求记住、查历史、同步记忆或排障时使用。
--- ---
# bmem:Claude Code 与 Codex 共享的跨机器长期记忆 # bmem:Claude Code 与 Codex 共享的跨机器长期记忆
@@ -192,6 +192,14 @@ bmem add --machine --section "账号/服务配置" "gitea 账号 bowei,凭证
`bmem sync` 做两件事:git 同步三层 Markdown(工作副本 ↔ `~/.bmem/repo` clone ↔ gitea), `bmem sync` 做两件事:git 同步三层 Markdown(工作副本 ↔ `~/.bmem/repo` clone ↔ gitea),
以及与中心 PostgreSQL 双向同步 log 层。**它只碰 bmem 自己的文件,不会动你项目的 git 分支。** 以及与中心 PostgreSQL 双向同步 log 层。**它只碰 bmem 自己的文件,不会动你项目的 git 分支。**
## `bmem sync` 的 Gitea 故障:立即修复并实测闭环
`bmem sync` 的 Git 层失败时先判型。若 refs/API 仍可读而 clone/fetch 报 `bad pack header`、`early EOF`、`RPC failed` 或 `git-upload-pack` 异常,这是 Gitea pack 传输故障;**当轮立即使用 `tools-sync` skill 阶段 1.5 登录服务器诊断并修复**,不要只重试 `bmem sync`、只报“远端待恢复”,或把 PostgreSQL 成功当成整个同步成功。
1. 从 `~/.bmem/repo` 的 `origin` 取得 bmem 仓库 URL;运行 `tools-sync/scripts/gitea_doctor.ps1 -RepoUrl <URL>` 确认 refs 与真正的浅克隆两个面,然后 `-Fix`。服务器地址、容器名、SSH 主机指纹取自本机 `~/skills_env.md`,凭据只从本机已授权的凭据存储或 bmem 既有记录读取,绝不输出、硬编码到 skill/仓库或提交。若 OpenSSH 私钥缺失,doctor 应尝试已登记密码和固定主机指纹的 PuTTY 通道;仍不能登录时立即说明缺的是哪种访问能力并请求用户提供,不能声称完成。
2. 在服务端先核对精确目标,再对异常全局 Git 配置或注入脚本做可恢复隔离、重启并重新检查;不进行强推、仓库手术或不可恢复清理,除非另有明确授权。修复后的判断依据是**从客户端真正完成 fetch/clone**,不是 Gitea 网页、`ls-remote` 或容器自称 healthy。
3. 立刻重跑 `bmem sync`。传输恢复后若出现真实 rebase 冲突,保留本地和远端语义,解决 `~/.bmem/repo` 及会重新覆盖仓库的本机 Markdown 工作副本,特别核对自动编号唯一;只做非强制推送,再次运行 `bmem sync`。
4. 只有 `bmem sync` 返回成功、Git 拉取/推送成功、远端 `main` 与本地提交相同、客户端浅克隆可读,才报告 Markdown 层已同步;PostgreSQL 另按自身结果报告。若新故障持续,继续做安全范围内的诊断;真正缺少登录权限或需要额外授权时报告具体阻塞和所需输入,不把未同步状态遗留为“已完成”。
## 多机与本机状态 ## 多机与本机状态
```bash ```bash
+70 -1
View File
@@ -77,7 +77,15 @@ function Test-Transport {
$out = & git -c core.askPass=true clone --quiet --bare --depth 1 --filter=tree:0 $RepoUrl $tmp 2>&1 | ForEach-Object { "$_" } $out = & git -c core.askPass=true clone --quiet --bare --depth 1 --filter=tree:0 $RepoUrl $tmp 2>&1 | ForEach-Object { "$_" }
$code = $LASTEXITCODE $code = $LASTEXITCODE
$ErrorActionPreference = $prev $ErrorActionPreference = $prev
if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } $tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd([IO.Path]::DirectorySeparatorChar)
$resolvedTmp = [IO.Path]::GetFullPath($tmp)
if (-not $resolvedTmp.StartsWith($tempRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or
-not ([IO.Path]::GetFileName($resolvedTmp) -match '^gitea-doctor-[0-9a-f]{32}$')) {
throw "unsafe diagnostic clone cleanup target: $resolvedTmp"
}
if (Test-Path -LiteralPath $resolvedTmp) {
Remove-Item -LiteralPath $resolvedTmp -Recurse -Force -ErrorAction SilentlyContinue
}
if ($code -eq 0) { $result.Pack = $true } else { $result.Detail = ($out -join " ") } if ($code -eq 0) { $result.Pack = $true } else { $result.Detail = ($out -join " ") }
return $result return $result
} }
@@ -123,6 +131,57 @@ function Invoke-Remote([string]$HostName, [string]$Port, [string]$LocalScript, [
return @{ Code = $code; Out = ($out -join "`n") } return @{ Code = $code; Out = ($out -join "`n") }
} }
function Find-PuttyExe([string]$Name) {
$found = Get-Command $Name -ErrorAction SilentlyContinue
if ($found) { return $found.Source }
$candidate = Join-Path $env:ProgramFiles ("PuTTY\" + $Name)
if (Test-Path -LiteralPath $candidate) { return $candidate }
return ""
}
function Get-BmemGiteaPassword {
# A previously user-authorized machine/global record is a credential source,
# not a value to print or copy into any repository. Use it only when the
# key-based SSH path is unavailable on this machine.
$memoryFile = Join-Path $env:USERPROFILE "bmem.md"
if (-not (Test-Path -LiteralPath $memoryFile)) { return "" }
$memoryText = Get-Content -LiteralPath $memoryFile -Raw -Encoding UTF8
$entry = [regex]::Match($memoryText, '(?m)^\s*-\s*\[G-0009\].*$').Value
if (-not $entry) { return "" }
$pattern = 'SSH\s*\u767B\u5F55\s*=\s*\u7528\u6237\u540D\s*bowei\s*[\u3001,\uFF0C]\s*\u5BC6\u7801\s*([^;\uFF1B,\uFF0C\u3001\u3002\s]+)'
return [regex]::Match($entry, $pattern).Groups[1].Value
}
function Invoke-RemotePutty([string]$HostName, [string]$Port, [string]$LocalScript, [string[]]$ScriptArgs) {
$fingerprint = Get-EnvKey "GITEA_HOST_KEY_FP"
$password = Get-BmemGiteaPassword
$plink = Find-PuttyExe "plink.exe"
$pscp = Find-PuttyExe "pscp.exe"
if (-not $fingerprint -or -not $password -or -not $plink -or -not $pscp) {
return @{ Code = 255; Out = "PuTTY fallback lacks a pinned host fingerprint, previously authorized bmem credential, or PuTTY executables" }
}
$remote = "/tmp/tools-sync-triage.$PID.putty.sh"
$prev = $ErrorActionPreference; $ErrorActionPreference = "Continue"
$plinkArgs = @("-batch", "-ssh", "-hostkey", $fingerprint, "-P", $Port,
"-pw", $password, "$sshUser@$HostName")
$probe = & $plink @plinkArgs "test ! -e $remote" 2>&1 | ForEach-Object { "$_" }
if ($LASTEXITCODE -ne 0) {
$ErrorActionPreference = $prev
return @{ Code = 255; Out = "remote triage staging path is occupied or SSH authentication failed: " + ($probe -join " ") }
}
$cp = & $pscp -batch -hostkey $fingerprint -P $Port -pw $password $LocalScript "${sshUser}@${HostName}:$remote" 2>&1 | ForEach-Object { "$_" }
if ($LASTEXITCODE -ne 0) {
$ErrorActionPreference = $prev
return @{ Code = 255; Out = "PuTTY script transfer failed: " + ($cp -join " ") }
}
$cmd = "sh $remote " + ($ScriptArgs -join " ") + "; rc=`$?; rm -f $remote; exit `$rc"
$out = & $plink @plinkArgs $cmd 2>&1 | ForEach-Object { "$_" }
$code = $LASTEXITCODE
$ErrorActionPreference = $prev
return @{ Code = $code; Out = ($out -join "`n") }
}
$triage = Join-Path $PSScriptRoot "gitea_triage.sh" $triage = Join-Path $PSScriptRoot "gitea_triage.sh"
if (-not (Test-Path $triage)) { Fail "gitea_triage.sh missing next to this script." } if (-not (Test-Path $triage)) { Fail "gitea_triage.sh missing next to this script." }
# Normalise to LF and no BOM before shipping it: a CRLF or BOM on the shebang line makes # Normalise to LF and no BOM before shipping it: a CRLF or BOM on the shebang line makes
@@ -146,6 +205,16 @@ foreach ($tg in $targets) {
Write-Host " unreachable: $(($r.Out -split "`n")[0])" Write-Host " unreachable: $(($r.Out -split "`n")[0])"
$r = $null $r = $null
} }
if (-not $r) {
Write-Host " OpenSSH authentication unavailable; trying the pinned PuTTY credential path"
foreach ($tg in $targets) {
Write-Host "=== server triage via pinned PuTTY: $sshUser@$($tg.H):$($tg.P) ==="
$r = Invoke-RemotePutty $tg.H $tg.P $triageStaged $triageArgs
if ($r.Code -ne 255) { break }
Write-Host " unavailable: $(($r.Out -split "`n")[0])"
$r = $null
}
}
Remove-Item $triageStaged -Force -ErrorAction SilentlyContinue Remove-Item $triageStaged -Force -ErrorAction SilentlyContinue
if (-not $r) { Fail "could not reach the gitea host on any configured path (see GITEA_SSH_HOST / _ALT)." } if (-not $r) { Fail "could not reach the gitea host on any configured path (see GITEA_SSH_HOST / _ALT)." }
+15 -5
View File
@@ -87,10 +87,19 @@ if [ "$HOOKCFG" != 0 ] || [ "$CFG_SIZE" -gt 65536 ] 2>/dev/null; then
bad gitconfig "size=${CFG_SIZE}B suspicious-directives=${HOOKCFG} (expected a few hundred B, 0)" bad gitconfig "size=${CFG_SIZE}B suspicious-directives=${HOOKCFG} (expected a few hundred B, 0)"
dex "grep -inE 'packObjectsHook|core\\.hooksPath|sshCommand' $GCFG | head -5" dex "grep -inE 'packObjectsHook|core\\.hooksPath|sshCommand' $GCFG | head -5"
if [ "$FIX" = 1 ]; then if [ "$FIX" = 1 ]; then
# gitea rewrites a clean global gitconfig at startup, so quarantine + restart is # Preserve both the injected config and its referenced executable as
# both the repair and the proof: hand-editing it would only paper over the entry. # reversible evidence before startup rebuilds the global config.
dex "cp $GCFG /data/gitea/home/.gitconfig.quarantine.$TS 2>/dev/null || cp $GCFG $GCFG.quarantine.$TS" HOOK_SCRIPT=$(dex "grep -oE '/data/gitea/home/[A-Za-z0-9_.-]+\\.sh' $GCFG | head -1" | tr -d '\r')
dex "rm -f $GCFG" if [ -n "$HOOK_SCRIPT" ]; then
case "$HOOK_SCRIPT" in
/data/gitea/home/*.sh)
if dex "[ -f $HOOK_SCRIPT ] && [ ! -e $HOOK_SCRIPT.quarantine.$TS ]" >/dev/null 2>&1; then
dex "mv $HOOK_SCRIPT $HOOK_SCRIPT.quarantine.$TS"
echo "INFO injected hook script quarantined: $HOOK_SCRIPT.quarantine.$TS"
fi ;;
esac
fi
dex "[ ! -e $GCFG.quarantine.$TS ] && mv $GCFG $GCFG.quarantine.$TS"
$D restart "$CONTAINER" >/dev/null 2>&1 $D restart "$CONTAINER" >/dev/null 2>&1
sleep 12 sleep 12
NEW=$(dex "wc -c < $GCFG 2>/dev/null" | tr -d '\r ') NEW=$(dex "wc -c < $GCFG 2>/dev/null" | tr -d '\r ')
@@ -180,10 +189,11 @@ fi
PUB=$(dex 'ls -d /data/git/repositories/*/*.git 2>/dev/null | head -5' | tr -d '\r') PUB=$(dex 'ls -d /data/git/repositories/*/*.git 2>/dev/null | head -5' | tr -d '\r')
CLONE_OK=0 CLONE_OK=0
CLONE_MSG="no public repo reachable anonymously" CLONE_MSG="no public repo reachable anonymously"
CLONE_DIR="/tmp/tools-sync-httpclone.$TS.$$"
for r in $PUB; do for r in $PUB; do
owner=$(basename "$(dirname "$r")") owner=$(basename "$(dirname "$r")")
name=$(basename "$r" .git) name=$(basename "$r" .git)
out=$(dex "rm -rf /tmp/tsdoc; GIT_TERMINAL_PROMPT=0 git clone -q --filter=tree:0 --depth 1 http://localhost:3000/$owner/$name.git /tmp/tsdoc 2>&1; ls /tmp/tsdoc/.git >/dev/null 2>&1 && echo TSDOC_OK; rm -rf /tmp/tsdoc") out=$(dex "test ! -e $CLONE_DIR || exit 3; GIT_TERMINAL_PROMPT=0 git clone -q --filter=tree:0 --depth 1 http://localhost:3000/$owner/$name.git $CLONE_DIR 2>&1; ls $CLONE_DIR/.git >/dev/null 2>&1 && echo TSDOC_OK; rm -rf $CLONE_DIR")
case "$out" in case "$out" in
*TSDOC_OK*) CLONE_OK=1; CLONE_MSG="$owner/$name"; break ;; *TSDOC_OK*) CLONE_OK=1; CLONE_MSG="$owner/$name"; break ;;
*"could not read Username"*|*"Authentication"*) continue ;; *"could not read Username"*|*"Authentication"*) continue ;;