diff --git a/policies/MACHINE-ALIASES.yaml b/policies/MACHINE-ALIASES.yaml new file mode 100644 index 0000000..4afa086 --- /dev/null +++ b/policies/MACHINE-ALIASES.yaml @@ -0,0 +1,14 @@ +version: 1 +machines: + - hostname: mac-5 + aliases: mac5,control,openclaw-control + default_agent_suffix: mac5 + - hostname: lingyuzeng-X99-TI-D4-PLUS + aliases: rtx2080ti,gpu-gateway,group + default_agent_suffix: rtx2080ti + - hostname: mac-6 + aliases: mac6,executor,node-exec + default_agent_suffix: mac6 + - hostname: mac-7 + aliases: mac7,browser,node-browser + default_agent_suffix: mac7 diff --git a/policies/SESSION-BOOTSTRAP.md b/policies/SESSION-BOOTSTRAP.md new file mode 100644 index 0000000..49f9b27 --- /dev/null +++ b/policies/SESSION-BOOTSTRAP.md @@ -0,0 +1,48 @@ +# SESSION BOOTSTRAP (Shared Memory) + +## 目标 +- 所有客户端(OpenClaw / Claude / Codex / Cursor)共享同一记忆系统。 +- Durable memory authority 是 `collective-memory-repo`。 +- 群体权威召回只认远程 `memory-gateway`。 + +## 统一架构 +- Memory repo: `/Users/lingyuzeng/project/collective-memory-repo` +- Gateway URL: `http://100.64.0.45:8787` +- Gateway MCP URL: `http://100.64.0.45:8787/mcp` +- 记忆层规则: + - 共享长期事实:`shared/long-term/` + - 每日记录:`daily/` + - 任务协作:`tasks//` + - Agent 私域:`agents//` + +## 分支规则 +- 长期协作默认:`branch=main` +- 并发任务协作:`branch=task/` +- 群体查询必须带:`require_latest=true` + +## 读写规则 +- Runtime 文件(persona/tools/heartbeat)留在各自本机 runtime 目录。 +- Durable facts 只能写入 `collective-memory-repo` 的对应 lane。 +- `vaults/memory/*` 是兼容区,不是长期真相源。 + +## 会话初始化必填上下文 +在新会话第一条消息中提供: +- `machine_hostname` +- `machine_alias` +- `agent_id` +- `memory_branch`(默认 `main`) +- `task_id`(如有) + +## 示例(首条消息) +```text +@/Users/lingyuzeng/project/collective-memory-repo/policies/SESSION-BOOTSTRAP.md +machine_hostname=mac-5 +machine_alias=mac5 +agent_id=codex-mac5 +memory_branch=main +``` + +## MCP 接入要求 +- 先在客户端注册 MCP server(URL 指向 `...:8787/mcp`)。 +- 再通过 `@SESSION-BOOTSTRAP.md` 注入架构与规则。 +- 客户端本地检索可保留,但群体结论以 gateway 结果为准。 diff --git a/scripts/print-session-bootstrap.sh b/scripts/print-session-bootstrap.sh new file mode 100755 index 0000000..ee48dcd --- /dev/null +++ b/scripts/print-session-bootstrap.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: + scripts/print-session-bootstrap.sh [--tool ] [--branch ] [--task-id ] [--gateway-url ] [--hostname ] + +Prints a ready-to-paste first message for Claude/Codex/Cursor/OpenClaw sessions. +USAGE +} + +TOOL="codex" +BRANCH="main" +TASK_ID="" +GATEWAY_URL="http://100.64.0.45:8787" +HOSTNAME_INPUT="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --tool) + TOOL="${2:-}" + shift 2 + ;; + --branch) + BRANCH="${2:-}" + shift 2 + ;; + --task-id) + TASK_ID="${2:-}" + shift 2 + ;; + --gateway-url) + GATEWAY_URL="${2:-}" + shift 2 + ;; + --hostname) + HOSTNAME_INPUT="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "ERROR: unknown arg: $1" >&2 + usage + exit 1 + ;; + esac +done + +REPO_ROOT="$(git rev-parse --show-toplevel)" +ARGS=(--field all --tool "$TOOL") +if [[ -n "$HOSTNAME_INPUT" ]]; then + ARGS+=(--hostname "$HOSTNAME_INPUT") +fi +INFO="$($REPO_ROOT/scripts/resolve-machine-alias.sh "${ARGS[@]}")" + +hostname_v="$(printf '%s\n' "$INFO" | awk -F= '$1=="hostname"{print $2}')" +alias_v="$(printf '%s\n' "$INFO" | awk -F= '$1=="alias"{print $2}')" +agent_id_v="$(printf '%s\n' "$INFO" | awk -F= '$1=="agent_id"{print $2}')" + +cat <] [--field alias|suffix|agent-id|all] [--tool ] + +Defaults: + --hostname: current host shortname (hostname -s) + --field: all + --tool: openclaw + +Reads mapping from policies/MACHINE-ALIASES.yaml. +USAGE +} + +HOSTNAME_INPUT="$(hostname -s 2>/dev/null || hostname)" +FIELD="all" +TOOL="openclaw" + +while [[ $# -gt 0 ]]; do + case "$1" in + --hostname) + HOSTNAME_INPUT="${2:-}" + shift 2 + ;; + --field) + FIELD="${2:-}" + shift 2 + ;; + --tool) + TOOL="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "ERROR: unknown arg: $1" >&2 + usage + exit 1 + ;; + esac +done + +case "$FIELD" in + alias|suffix|agent-id|all) ;; + *) + echo "ERROR: --field must be one of alias|suffix|agent-id|all" >&2 + exit 1 + ;; +esac + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)" +if [[ -z "$REPO_ROOT" ]]; then + echo "ERROR: run inside collective-memory-repo" >&2 + exit 2 +fi + +MAP_FILE="$REPO_ROOT/policies/MACHINE-ALIASES.yaml" +if [[ ! -f "$MAP_FILE" ]]; then + echo "ERROR: mapping file not found: $MAP_FILE" >&2 + exit 2 +fi + +trim() { + local s="$1" + s="${s#${s%%[![:space:]]*}}" + s="${s%${s##*[![:space:]]}}" + printf '%s' "$s" +} + +contains_csv_value() { + local csv="$1" + local needle="$2" + IFS=',' read -r -a parts <<< "$csv" + for p in "${parts[@]}"; do + if [[ "$(trim "$p")" == "$needle" ]]; then + return 0 + fi + done + return 1 +} + +record_hostname="" +record_aliases="" +record_suffix="" +matched_alias="" +matched_suffix="" +matched_hostname="" +found="false" + +finalize_record() { + if [[ "$found" == "true" || -z "$record_hostname" ]]; then + return + fi + + if [[ "$HOSTNAME_INPUT" == "$record_hostname" ]]; then + found="true" + elif contains_csv_value "$record_aliases" "$HOSTNAME_INPUT"; then + found="true" + fi + + if [[ "$found" == "true" ]]; then + matched_hostname="$record_hostname" + matched_suffix="$record_suffix" + + if contains_csv_value "$record_aliases" "$HOSTNAME_INPUT"; then + matched_alias="$HOSTNAME_INPUT" + else + IFS=',' read -r first_alias _ <<< "$record_aliases" + matched_alias="$(trim "$first_alias")" + fi + fi +} + +while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do + line="${raw_line%%#*}" + line="$(trim "$line")" + [[ -z "$line" ]] && continue + + if [[ "$line" =~ ^-[[:space:]]hostname:[[:space:]]*(.+)$ ]]; then + finalize_record + record_hostname="$(trim "${BASH_REMATCH[1]}")" + record_aliases="" + record_suffix="" + continue + fi + + if [[ "$line" =~ ^aliases:[[:space:]]*(.+)$ ]]; then + record_aliases="$(trim "${BASH_REMATCH[1]}")" + continue + fi + + if [[ "$line" =~ ^default_agent_suffix:[[:space:]]*(.+)$ ]]; then + record_suffix="$(trim "${BASH_REMATCH[1]}")" + continue + fi +done < "$MAP_FILE" + +finalize_record + +if [[ "$found" != "true" ]]; then + # fallback: use normalized hostname directly + fallback_suffix="$(printf '%s' "$HOSTNAME_INPUT" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9-' '-')" + fallback_suffix="${fallback_suffix#-}" + fallback_suffix="${fallback_suffix%-}" + [[ -z "$fallback_suffix" ]] && fallback_suffix="host" + + matched_hostname="$HOSTNAME_INPUT" + matched_alias="$fallback_suffix" + matched_suffix="$fallback_suffix" +fi + +agent_id="${TOOL}-${matched_suffix}" + +case "$FIELD" in + alias) + printf '%s\n' "$matched_alias" + ;; + suffix) + printf '%s\n' "$matched_suffix" + ;; + agent-id) + printf '%s\n' "$agent_id" + ;; + all) + cat < --openclaw-config ~/.openclaw/openclaw.json ``` -## 3) Commit and push +## 3) Session first message (recommended) + +```bash +cd /Users/lingyuzeng/project/collective-memory-repo +scripts/print-session-bootstrap.sh --tool codex --branch main +``` + +任务会话示例: + +```bash +scripts/print-session-bootstrap.sh --tool codex --branch task/openclawd-migration-2026-03 --task-id openclawd-migration-2026-03 +``` + +## 4) Commit and push ```bash git add agents/ @@ -27,7 +51,7 @@ git commit -m "feat(agent): add memory lane" git push ``` -## 4) Verify memory-gateway visibility +## 5) Verify memory-gateway visibility ```bash curl -fsS http://100.64.0.45:8787/health @@ -36,7 +60,7 @@ printf '{"branch":"main","require_latest":true}' \ http://100.64.0.45:8787/memory/status ``` -## 5) Query smoke test +## 6) Query smoke test ```bash printf '{"query":"","branch":"main","require_latest":true,"top_k":3}' \