Merge remote-tracking branch 'origin/main' into claude/analyze-traffic-sources-lUHKH

# Conflicts:
#	package.json
This commit is contained in:
Claude
2026-05-07 06:46:01 +00:00
56 changed files with 6473 additions and 1095 deletions
+132
View File
@@ -0,0 +1,132 @@
#!/bin/bash
# One-shot field-evidence gatherer for OCP v3.12.0 SSE heartbeat.
# Scheduled by ~/Library/LaunchAgents/dev.ocp.heartbeat-check.plist to fire
# once at 2026-05-02 09:00 Australia/Brisbane. Gathers evidence from local
# OCP logs + GitHub issue #47 + repo issue search, posts a summary comment
# on #47, and exits. Does NOT open PRs or change code — the maintainer
# decides after reading the summary.
#
# Dry-run: ./heartbeat-field-check.sh --dry-run (prints summary, skips post)
set -euo pipefail
REPO="dtzp555-max/ocp"
SHIP_DATE="2026-04-25"
# Baseline captured at script-install time so internal testing entries from
# Phase 3 verification (~5 entries from 2026-04-25T00:0000:48Z) don't get
# counted as field evidence. Any heartbeat_active log entry with ts >= this
# timestamp is treated as a real opt-in.
BASELINE_TS="2026-04-25T01:00:00Z"
PROXY_LOG="$HOME/ocp/logs/proxy.log"
OUT_DIR="$HOME/ocp/logs"
SELF_LOG="$OUT_DIR/heartbeat-field-check-$(date +%Y-%m-%d).log"
DRY_RUN=0
[ "${1:-}" = "--dry-run" ] && DRY_RUN=1
mkdir -p "$OUT_DIR"
exec > >(tee -a "$SELF_LOG") 2>&1
echo "=== heartbeat field-evidence check: $(date -u +%Y-%m-%dT%H:%M:%SZ) (dry_run=$DRY_RUN) ==="
# ── signal 1: local proxy log ─────────────────────────────────────────────
if [ -r "$PROXY_LOG" ]; then
# Only count entries with ts >= BASELINE_TS (string sort works on RFC3339)
HEARTBEAT_COUNT=$(grep '"event":"heartbeat_active"' "$PROXY_LOG" 2>/dev/null \
| awk -v base="$BASELINE_TS" '
match($0, /"ts":"[^"]+"/) {
ts = substr($0, RSTART+6, RLENGTH-7);
if (ts >= base) c++
}
END { print c+0 }')
else
HEARTBEAT_COUNT=0
fi
echo "signal 1 — heartbeat_active log entries since $BASELINE_TS: $HEARTBEAT_COUNT"
# ── signal 2: comments on #47 since ship ──────────────────────────────────
NEW_47_JSON="/tmp/ocp-47-new-comments-$$.json"
gh issue view 47 --repo "$REPO" --json comments \
--jq '[.comments[] | select(.createdAt >= "'"$SHIP_DATE"'T00:00:00Z")]' \
> "$NEW_47_JSON" 2>/dev/null || echo "[]" > "$NEW_47_JSON"
NEW_COMMENTS=$(jq 'length' "$NEW_47_JSON")
echo "signal 2 — new comments on #47 since $SHIP_DATE: $NEW_COMMENTS"
# Build a compact, human-readable excerpt for the summary body
NEW_47_EXCERPT=""
if [ "$NEW_COMMENTS" -gt 0 ]; then
NEW_47_EXCERPT=$(jq -r '.[] | "- **@\(.author.login)** (\(.createdAt)): " + (.body | gsub("\r"; "") | split("\n")[0])[:180]' "$NEW_47_JSON")
fi
# ── signal 3: other heartbeat-related issues since ship ──────────────────
OTHER_ISSUES_JSON="/tmp/ocp-heartbeat-issues-$$.json"
gh search issues "repo:$REPO heartbeat" --json number,title,state,createdAt --limit 30 \
--jq '[.[] | select(.createdAt >= "'"$SHIP_DATE"'T00:00:00Z" and .number != 47 and .number != 48)]' \
> "$OTHER_ISSUES_JSON" 2>/dev/null || echo "[]" > "$OTHER_ISSUES_JSON"
OTHER_ISSUES=$(jq 'length' "$OTHER_ISSUES_JSON")
echo "signal 3 — other heartbeat-related issues since ship: $OTHER_ISSUES"
OTHER_ISSUES_EXCERPT=""
if [ "$OTHER_ISSUES" -gt 0 ]; then
OTHER_ISSUES_EXCERPT=$(jq -r '.[] | "- #\(.number) [\(.state)] \(.title)"' "$OTHER_ISSUES_JSON")
fi
# ── compose summary ──────────────────────────────────────────────────────
BODY_FILE="/tmp/ocp-47-summary-$$.md"
{
echo "### Automated 7-day field-evidence check (v3.12.0)"
echo
echo "_Triggered by a local launchd scheduled task on the maintainer's rig at $(date -u +%Y-%m-%dT%H:%M:%SZ)._"
echo
echo "| Signal | Count |"
echo "|---|---|"
echo "| \`heartbeat_active\` log entries on prod rig (since baseline $BASELINE_TS) | $HEARTBEAT_COUNT |"
echo "| New comments on #47 since $SHIP_DATE | $NEW_COMMENTS |"
echo "| Other heartbeat-related issues filed since $SHIP_DATE | $OTHER_ISSUES |"
echo
if [ -n "$NEW_47_EXCERPT" ]; then
echo "**New #47 comments (first line each):**"
echo
echo "$NEW_47_EXCERPT"
echo
fi
if [ -n "$OTHER_ISSUES_EXCERPT" ]; then
echo "**Other heartbeat-related issues:**"
echo
echo "$OTHER_ISSUES_EXCERPT"
echo
fi
echo "**Decision guidance for maintainer (manual):**"
echo
echo "- If any of the above indicate a **crash report** on \`: keepalive\` comment frames → leave default at \`0\` and file a \`CLAUDE_HEARTBEAT_FORMAT=empty-delta\` follow-up issue (spec \`§D2\` fallback plan)."
echo "- If there is at least one **opt-in confirmation** (a user reports \`CLAUDE_HEARTBEAT_INTERVAL\` fixed their timeout issue) and no crash reports → consider opening a PR for v3.13.0 flipping the default to \`30000\`, following the same ALIGNMENT + independent-reviewer + release-kit discipline as PR #49."
echo "- If all three signals are zero → extend the soak window or close this follow-up as \"no field evidence.\""
echo
echo "This bot does not open PRs or change code. The maintainer reviews and acts."
} > "$BODY_FILE"
echo "--- summary preview ---"
cat "$BODY_FILE"
echo "--- end preview ---"
# ── post (unless dry-run) ────────────────────────────────────────────────
if [ "$DRY_RUN" -eq 1 ]; then
echo "DRY RUN — skipping gh issue comment"
else
gh issue comment 47 --repo "$REPO" --body-file "$BODY_FILE" && echo "comment posted on #47"
fi
# ── cleanup + self-disable so the plist doesn't linger loaded forever ────
rm -f "$NEW_47_JSON" "$OTHER_ISSUES_JSON" "$BODY_FILE"
if [ "$DRY_RUN" -eq 0 ]; then
# Unload + remove the plist so this never fires again
PLIST="$HOME/Library/LaunchAgents/dev.ocp.heartbeat-check.plist"
if [ -f "$PLIST" ]; then
launchctl bootout "gui/$(id -u)" "$PLIST" 2>/dev/null || launchctl unload "$PLIST" 2>/dev/null || true
rm -f "$PLIST"
echo "self-disabled: removed $PLIST"
fi
fi
echo "=== done ==="
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env node
// Idempotently sync OCP's claude-local provider models into OpenClaw's registry.
// Only touches:
// - config.models.providers["claude-local"].models
// - config.agents.defaults.models["claude-local/*"] keys
// All other fields and providers are preserved.
import { readFileSync, writeFileSync, existsSync, copyFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir } from "node:os";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, "..");
const OPENCLAW_CONFIG = join(homedir(), ".openclaw", "openclaw.json");
const PROVIDER_NAME = "claude-local";
const QUIET = process.argv.includes("--quiet");
function log(msg) { if (!QUIET) console.log(`${msg}`); }
function warn(msg) { console.warn(`${msg}`); }
if (!existsSync(OPENCLAW_CONFIG)) {
log(`OpenClaw not installed at ${OPENCLAW_CONFIG} — skipping (this is fine for non-OpenClaw users)`);
process.exit(0);
}
const modelsConfig = JSON.parse(readFileSync(join(REPO_ROOT, "models.json"), "utf-8"));
const desiredModels = modelsConfig.models.map(m => ({
id: m.id,
name: m.openclawName,
reasoning: m.reasoning,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: m.contextWindow,
maxTokens: m.maxTokens,
}));
const desiredAliases = Object.fromEntries(
modelsConfig.models.map(m => [`${PROVIDER_NAME}/${m.id}`, { alias: m.displayName }])
);
const config = JSON.parse(readFileSync(OPENCLAW_CONFIG, "utf-8"));
// Compute diff before writing
const existingModels = config?.models?.providers?.[PROVIDER_NAME]?.models ?? [];
const existingIds = new Set(existingModels.map(m => m.id));
const desiredIds = new Set(desiredModels.map(m => m.id));
const added = [...desiredIds].filter(id => !existingIds.has(id));
const removed = [...existingIds].filter(id => !desiredIds.has(id));
if (added.length === 0 && removed.length === 0 && existingModels.length === desiredModels.length) {
// Check deep equality too in case names/maxTokens changed
const changed = desiredModels.some((d) => {
const e = existingModels.find(x => x.id === d.id);
return !e || e.name !== d.name || e.maxTokens !== d.maxTokens;
});
if (!changed) {
log("OpenClaw registry already in sync");
process.exit(0);
}
}
// Backup
const backupPath = `${OPENCLAW_CONFIG}.bak.${Date.now()}`;
copyFileSync(OPENCLAW_CONFIG, backupPath);
log(`Backed up to ${backupPath}`);
// Surgical patch: only touch claude-local provider and claude-local/* aliases
if (!config.models) config.models = {};
if (!config.models.providers) config.models.providers = {};
if (!config.models.providers[PROVIDER_NAME]) {
// First-time registration
config.models.providers[PROVIDER_NAME] = {
baseUrl: "http://127.0.0.1:3456/v1",
api: "openai-completions",
authHeader: false,
models: desiredModels,
};
} else {
// Update only the models array; leave baseUrl/api/authHeader untouched (user may have customized port)
config.models.providers[PROVIDER_NAME].models = desiredModels;
}
if (!config.agents) config.agents = {};
if (!config.agents.defaults) config.agents.defaults = {};
if (!config.agents.defaults.models) config.agents.defaults.models = {};
// Remove stale claude-local/* aliases, then add desired ones
for (const key of Object.keys(config.agents.defaults.models)) {
if (key.startsWith(`${PROVIDER_NAME}/`)) delete config.agents.defaults.models[key];
}
Object.assign(config.agents.defaults.models, desiredAliases);
writeFileSync(OPENCLAW_CONFIG, JSON.stringify(config, null, 2) + "\n");
if (added.length > 0) log(`Added: ${added.join(", ")}`);
if (removed.length > 0) log(`Removed (no longer in models.json): ${removed.join(", ")}`);
log(`OpenClaw registry synced: ${desiredModels.length} models registered`);