mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-21 21:15:09 +00:00
Merge remote-tracking branch 'origin/main' into claude/analyze-traffic-sources-lUHKH
# Conflicts: # package.json
This commit is contained in:
Executable
+132
@@ -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:00–00: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 ==="
|
||||
@@ -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`);
|
||||
Reference in New Issue
Block a user