Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
58
scripts/repro/code-mode-namespace-live-docker.sh
Executable file
58
scripts/repro/code-mode-namespace-live-docker.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
ROOT_DIR="${OPENCLAW_LIVE_DOCKER_REPO_ROOT:-$SCRIPT_ROOT_DIR}"
|
||||
ROOT_DIR="$(cd "$ROOT_DIR" && pwd)"
|
||||
TRUSTED_HARNESS_DIR="${OPENCLAW_LIVE_DOCKER_TRUSTED_HARNESS_DIR:-$SCRIPT_ROOT_DIR}"
|
||||
TRUSTED_HARNESS_DIR="$(cd "$TRUSTED_HARNESS_DIR" && pwd)"
|
||||
source "$TRUSTED_HARNESS_DIR/scripts/lib/docker-e2e-image.sh"
|
||||
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-code-mode-namespace-live-e2e" OPENCLAW_CODE_MODE_NAMESPACE_LIVE_E2E_IMAGE)"
|
||||
SKIP_BUILD="${OPENCLAW_CODE_MODE_NAMESPACE_LIVE_E2E_SKIP_BUILD:-0}"
|
||||
PROFILE_FILE="${OPENCLAW_CODE_MODE_NAMESPACE_LIVE_PROFILE_FILE:-${OPENCLAW_TESTBOX_PROFILE_FILE:-$HOME/.openclaw-testbox-live.profile}}"
|
||||
run_log=""
|
||||
if [ ! -f "$PROFILE_FILE" ] && [ -f "$HOME/.profile" ]; then
|
||||
PROFILE_FILE="$HOME/.profile"
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
if [ -n "${run_log:-}" ]; then
|
||||
rm -f "$run_log"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" code-mode-namespace-live "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "$SKIP_BUILD"
|
||||
|
||||
PROFILE_MOUNT=()
|
||||
PROFILE_STATUS="none"
|
||||
if [ -f "$PROFILE_FILE" ] && [ -r "$PROFILE_FILE" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$PROFILE_FILE"
|
||||
set +a
|
||||
PROFILE_MOUNT=(-v "$PROFILE_FILE":/home/appuser/.profile:ro)
|
||||
PROFILE_STATUS="$PROFILE_FILE"
|
||||
fi
|
||||
|
||||
echo "Running code mode namespace live Docker E2E..."
|
||||
echo "Profile file: $PROFILE_STATUS"
|
||||
run_log="$(docker_e2e_run_log code-mode-namespace-live)"
|
||||
if ! docker_e2e_run_with_harness \
|
||||
--user root \
|
||||
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
-e OPENAI_API_KEY \
|
||||
-e OPENAI_BASE_URL \
|
||||
-e "OPENCLAW_CODE_MODE_LIVE_MODEL=${OPENCLAW_CODE_MODE_LIVE_MODEL:-gpt-5.4-mini}" \
|
||||
-e "OPENCLAW_CODE_MODE_LIVE_TASKS=${OPENCLAW_CODE_MODE_LIVE_TASKS:-3}" \
|
||||
-v "$ROOT_DIR":/src:ro \
|
||||
"${PROFILE_MOUNT[@]}" \
|
||||
"$IMAGE_NAME" \
|
||||
bash /src/scripts/repro/code-mode-namespace-live-scenario.sh >"$run_log" 2>&1; then
|
||||
docker_e2e_print_log "$run_log"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker_e2e_print_log "$run_log"
|
||||
echo "Code mode namespace live Docker E2E passed"
|
||||
36
scripts/repro/code-mode-namespace-live-scenario.sh
Executable file
36
scripts/repro/code-mode-namespace-live-scenario.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source scripts/lib/live-docker-stage.sh
|
||||
|
||||
for profile_path in "$HOME/.profile" /home/appuser/.profile; do
|
||||
if [ -f "$profile_path" ] && [ -r "$profile_path" ]; then
|
||||
set +e +u
|
||||
# shellcheck disable=SC1090
|
||||
source "$profile_path"
|
||||
set -euo pipefail
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "${OPENAI_API_KEY:-}" ]; then
|
||||
echo "ERROR: OPENAI_API_KEY is required for the code mode namespace live Docker test." >&2
|
||||
exit 1
|
||||
fi
|
||||
export OPENAI_API_KEY
|
||||
if [ -n "${OPENAI_BASE_URL:-}" ]; then
|
||||
export OPENAI_BASE_URL
|
||||
fi
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$tmp_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
openclaw_live_stage_source_tree "$tmp_dir"
|
||||
openclaw_live_stage_node_modules "$tmp_dir"
|
||||
openclaw_live_link_runtime_tree "$tmp_dir"
|
||||
|
||||
cd "$tmp_dir"
|
||||
tsx scripts/repro/code-mode-namespace-live.ts
|
||||
662
scripts/repro/code-mode-namespace-live.ts
Executable file
662
scripts/repro/code-mode-namespace-live.ts
Executable file
@@ -0,0 +1,662 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
// Code Mode Namespace Live script supports OpenClaw repository automation.
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { Type } from "typebox";
|
||||
import type { Model } from "../../packages/agent-core/src/llm.js";
|
||||
import type { AgentEvent, AgentTool } from "../../packages/agent-core/src/types.js";
|
||||
import {
|
||||
clearCodeModeNamespacesForPlugin,
|
||||
createCodeModeNamespaceTool,
|
||||
registerCodeModeNamespaceForPlugin,
|
||||
} from "../../src/agents/code-mode-namespaces.js";
|
||||
import { applyCodeModeCatalog, createCodeModeTools } from "../../src/agents/code-mode.js";
|
||||
import { Agent } from "../../src/agents/runtime/index.js";
|
||||
import { createToolSearchCatalogRef } from "../../src/agents/tool-search.js";
|
||||
import { jsonResult, type AnyAgentTool } from "../../src/agents/tools/common.js";
|
||||
import { setPluginToolMeta } from "../../src/plugins/tools.js";
|
||||
|
||||
type Mode = "regular" | "code-catalog" | "code-namespace";
|
||||
|
||||
type FictionTitle = {
|
||||
id: string;
|
||||
title: string;
|
||||
lead: string;
|
||||
status: string;
|
||||
riskScore: number;
|
||||
dependencies: Array<{ id: string; cleared: boolean }>;
|
||||
};
|
||||
|
||||
type FictionScene = {
|
||||
id: string;
|
||||
titleId: string;
|
||||
pages: number;
|
||||
blocked: boolean;
|
||||
};
|
||||
|
||||
type FictionDefect = {
|
||||
id: string;
|
||||
titleId: string;
|
||||
sceneId: string;
|
||||
state: "open" | "closed";
|
||||
};
|
||||
|
||||
type FictionInvoice = {
|
||||
id: string;
|
||||
titleId: string;
|
||||
author: string;
|
||||
amount: number;
|
||||
paid: boolean;
|
||||
};
|
||||
|
||||
type FictionServiceState = {
|
||||
titles: FictionTitle[];
|
||||
scenes: FictionScene[];
|
||||
defects: FictionDefect[];
|
||||
invoices: FictionInvoice[];
|
||||
};
|
||||
|
||||
type FictionService = ReturnType<typeof createFictionService>;
|
||||
|
||||
type Task = {
|
||||
id: string;
|
||||
prompt: string;
|
||||
validate(answer: unknown, service: FictionService): { ok: boolean; reason?: string };
|
||||
};
|
||||
|
||||
type RunMetrics = {
|
||||
mode: Mode;
|
||||
task: string;
|
||||
ok: boolean;
|
||||
reason?: string;
|
||||
latencyMs: number;
|
||||
modelTurns: number;
|
||||
assistantMessages: number;
|
||||
topLevelToolCalls: number;
|
||||
serviceCalls: number;
|
||||
finalText: string;
|
||||
stopReason?: string;
|
||||
errorMessage?: string;
|
||||
toolResults?: unknown[];
|
||||
};
|
||||
|
||||
const PLUGIN_ID = "fictions-live";
|
||||
const POSITIVE_INTEGER_PATTERN = /^[1-9]\d*$/u;
|
||||
|
||||
function cloneState(): FictionServiceState {
|
||||
return {
|
||||
titles: [
|
||||
{
|
||||
id: "PX-73",
|
||||
title: "The Glass Orchard",
|
||||
lead: "Mira Vale",
|
||||
status: "draft",
|
||||
riskScore: 77,
|
||||
dependencies: [
|
||||
{ id: "outline", cleared: true },
|
||||
{ id: "rights", cleared: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "NM-12",
|
||||
title: "Night Market of Moons",
|
||||
lead: "Oren Quill",
|
||||
status: "revision",
|
||||
riskScore: 91,
|
||||
dependencies: [
|
||||
{ id: "continuity", cleared: true },
|
||||
{ id: "copyedit", cleared: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "RS-40",
|
||||
title: "River Static",
|
||||
lead: "Nia Rowan",
|
||||
status: "locked",
|
||||
riskScore: 54,
|
||||
dependencies: [{ id: "legal", cleared: true }],
|
||||
},
|
||||
],
|
||||
scenes: [
|
||||
{ id: "PX-73-S1", titleId: "PX-73", pages: 32, blocked: false },
|
||||
{ id: "PX-73-S2", titleId: "PX-73", pages: 28, blocked: false },
|
||||
{ id: "PX-73-S3", titleId: "PX-73", pages: 36, blocked: false },
|
||||
{ id: "NM-12-S1", titleId: "NM-12", pages: 44, blocked: false },
|
||||
{ id: "NM-12-S2", titleId: "NM-12", pages: 39, blocked: true },
|
||||
{ id: "RS-40-S1", titleId: "RS-40", pages: 51, blocked: false },
|
||||
],
|
||||
defects: [
|
||||
{ id: "D-101", titleId: "NM-12", sceneId: "NM-12-S1", state: "open" },
|
||||
{ id: "D-102", titleId: "NM-12", sceneId: "NM-12-S2", state: "open" },
|
||||
{ id: "D-103", titleId: "NM-12", sceneId: "NM-12-S2", state: "open" },
|
||||
{ id: "D-104", titleId: "PX-73", sceneId: "PX-73-S3", state: "closed" },
|
||||
{ id: "D-105", titleId: "RS-40", sceneId: "RS-40-S1", state: "open" },
|
||||
],
|
||||
invoices: [
|
||||
{ id: "I-200", titleId: "PX-73", author: "Mira Vale", amount: 4200, paid: false },
|
||||
{ id: "I-201", titleId: "NM-12", author: "Oren Quill", amount: 6100, paid: false },
|
||||
{ id: "I-202", titleId: "RS-40", author: "Nia Rowan", amount: 3700, paid: true },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function createFictionService() {
|
||||
const state = cloneState();
|
||||
let calls = 0;
|
||||
const note = () => {
|
||||
calls += 1;
|
||||
};
|
||||
const title = (id: string) => state.titles.find((entry) => entry.id === id);
|
||||
return {
|
||||
get calls() {
|
||||
return calls;
|
||||
},
|
||||
snapshot() {
|
||||
note();
|
||||
return structuredClone(state);
|
||||
},
|
||||
listTitles() {
|
||||
note();
|
||||
return structuredClone(state.titles);
|
||||
},
|
||||
getTitle(id: string) {
|
||||
note();
|
||||
return title(id) ?? null;
|
||||
},
|
||||
listScenes(titleId?: string) {
|
||||
note();
|
||||
return structuredClone(state.scenes.filter((entry) => !titleId || entry.titleId === titleId));
|
||||
},
|
||||
listDefects(titleId?: string) {
|
||||
note();
|
||||
return state.defects
|
||||
.filter((entry) => !titleId || entry.titleId === titleId)
|
||||
.map((entry) => structuredClone(entry));
|
||||
},
|
||||
listInvoices(author?: string) {
|
||||
note();
|
||||
return structuredClone(state.invoices.filter((entry) => !author || entry.author === author));
|
||||
},
|
||||
updateStatus(id: string, status: string) {
|
||||
note();
|
||||
const entry = title(id);
|
||||
if (!entry) {
|
||||
return { ok: false, error: "unknown title", id };
|
||||
}
|
||||
entry.status = status;
|
||||
return { ok: true, id, status };
|
||||
},
|
||||
currentStatus(id: string) {
|
||||
return title(id)?.status;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function stringParam(params: Record<string, unknown>, key: string): string {
|
||||
const value = params[key];
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function makeTool(
|
||||
name: string,
|
||||
description: string,
|
||||
properties: Record<string, unknown>,
|
||||
execute: (params: Record<string, unknown>) => unknown,
|
||||
): AnyAgentTool {
|
||||
const tool = {
|
||||
name,
|
||||
label: name,
|
||||
description,
|
||||
parameters: Type.Object(properties),
|
||||
execute: async (_toolCallId: string, params: unknown) =>
|
||||
jsonResult(
|
||||
execute((params && typeof params === "object" ? params : {}) as Record<string, unknown>),
|
||||
),
|
||||
} satisfies AnyAgentTool;
|
||||
setPluginToolMeta(tool, { pluginId: PLUGIN_ID, optional: true });
|
||||
return tool;
|
||||
}
|
||||
|
||||
function createFictionTools(service: FictionService): AnyAgentTool[] {
|
||||
return [
|
||||
makeTool("fictions_list_titles", "List fiction titles with status and risk.", {}, () =>
|
||||
service.listTitles(),
|
||||
),
|
||||
makeTool(
|
||||
"fictions_get_title",
|
||||
"Get one fiction title by id.",
|
||||
{ id: Type.String() },
|
||||
(params) => service.getTitle(stringParam(params, "id")),
|
||||
),
|
||||
makeTool(
|
||||
"fictions_list_scenes",
|
||||
"List scenes, optionally filtered by title id.",
|
||||
{ titleId: Type.Optional(Type.String()) },
|
||||
(params) =>
|
||||
service.listScenes(typeof params.titleId === "string" ? params.titleId : undefined),
|
||||
),
|
||||
makeTool(
|
||||
"fictions_list_defects",
|
||||
"List defects, optionally filtered by title id.",
|
||||
{ titleId: Type.Optional(Type.String()) },
|
||||
(params) =>
|
||||
service.listDefects(typeof params.titleId === "string" ? params.titleId : undefined),
|
||||
),
|
||||
makeTool(
|
||||
"fictions_list_invoices",
|
||||
"List invoices, optionally filtered by author.",
|
||||
{ author: Type.Optional(Type.String()) },
|
||||
(params) =>
|
||||
service.listInvoices(typeof params.author === "string" ? params.author : undefined),
|
||||
),
|
||||
makeTool(
|
||||
"fictions_update_status",
|
||||
"Update a fiction title status.",
|
||||
{ id: Type.String(), status: Type.String() },
|
||||
(params) => service.updateStatus(stringParam(params, "id"), stringParam(params, "status")),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function createFictionNamespaceTools(service: FictionService): AnyAgentTool[] {
|
||||
return [
|
||||
makeTool("fictions_snapshot", "Return the complete fiction production snapshot.", {}, () =>
|
||||
service.snapshot(),
|
||||
),
|
||||
makeTool("fictions_risk_audit", "Return highest-risk title audit.", {}, () => {
|
||||
const data = service.snapshot();
|
||||
const highest = data.titles.toSorted((a, b) => b.riskScore - a.riskScore)[0];
|
||||
if (!highest) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
task: "risk-audit",
|
||||
id: highest.id,
|
||||
lead: highest.lead,
|
||||
status: highest.status,
|
||||
unresolvedDefects: data.defects.filter(
|
||||
(defect) => defect.titleId === highest.id && defect.state === "open",
|
||||
).length,
|
||||
blockedScenes: data.scenes
|
||||
.filter((scene) => scene.titleId === highest.id && scene.blocked)
|
||||
.map((scene) => scene.id),
|
||||
};
|
||||
}),
|
||||
makeTool(
|
||||
"fictions_promote_if_ready",
|
||||
"Promote a title if dependencies and page count allow it.",
|
||||
{ id: Type.String(), status: Type.String() },
|
||||
(params) => {
|
||||
const id = stringParam(params, "id");
|
||||
const status = stringParam(params, "status");
|
||||
const data = service.snapshot();
|
||||
const title = data.titles.find((entry) => entry.id === id);
|
||||
const scenes = data.scenes.filter((scene) => scene.titleId === id);
|
||||
const totalPages = scenes.reduce((sum, scene) => sum + scene.pages, 0);
|
||||
const dependenciesCleared =
|
||||
title?.dependencies.every((dependency) => dependency.cleared) ?? false;
|
||||
if (!title || totalPages >= 110 || !dependenciesCleared) {
|
||||
return {
|
||||
task: "promote",
|
||||
id,
|
||||
action: "blocked",
|
||||
totalPages,
|
||||
finalStatus: title?.status ?? null,
|
||||
};
|
||||
}
|
||||
const updated = service.updateStatus(id, status);
|
||||
return {
|
||||
task: "promote",
|
||||
id,
|
||||
action: updated.ok ? "updated" : "blocked",
|
||||
totalPages,
|
||||
finalStatus: service.currentStatus(id) ?? null,
|
||||
};
|
||||
},
|
||||
),
|
||||
makeTool(
|
||||
"fictions_unpaid_over",
|
||||
"Return unpaid invoices over a numeric threshold.",
|
||||
{ amount: Type.Number() },
|
||||
(params) => {
|
||||
const amount = typeof params.amount === "number" ? params.amount : 0;
|
||||
const data = service.snapshot();
|
||||
const invoices = data.invoices.filter(
|
||||
(invoice) => !invoice.paid && invoice.amount > amount,
|
||||
);
|
||||
return {
|
||||
task: "invoice",
|
||||
invoiceIds: invoices.map((invoice) => invoice.id),
|
||||
totalUnpaidOver5000: invoices.reduce((sum, invoice) => sum + invoice.amount, 0),
|
||||
};
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function registerFictionNamespace(): void {
|
||||
clearCodeModeNamespacesForPlugin(PLUGIN_ID);
|
||||
registerCodeModeNamespaceForPlugin(PLUGIN_ID, {
|
||||
id: "fictions",
|
||||
globalName: "Fictions",
|
||||
description: "Fiction production service helpers.",
|
||||
requiredToolNames: [
|
||||
"fictions_promote_if_ready",
|
||||
"fictions_risk_audit",
|
||||
"fictions_snapshot",
|
||||
"fictions_unpaid_over",
|
||||
],
|
||||
prompt:
|
||||
"Use Fictions.riskAudit(), Fictions.promoteIfReady(id, status), Fictions.unpaidOver(amount), and Fictions.snapshot().",
|
||||
createScope: () => ({
|
||||
snapshot: createCodeModeNamespaceTool("fictions_snapshot"),
|
||||
riskAudit: createCodeModeNamespaceTool("fictions_risk_audit"),
|
||||
promoteIfReady: createCodeModeNamespaceTool("fictions_promote_if_ready", ([id, status]) => ({
|
||||
id: typeof id === "string" ? id : "",
|
||||
status: typeof status === "string" ? status : "",
|
||||
})),
|
||||
unpaidOver: createCodeModeNamespaceTool("fictions_unpaid_over", ([amount]) => ({
|
||||
amount: typeof amount === "number" ? amount : 0,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function createModel(modelId: string): Model<"openai-responses"> {
|
||||
const baseUrl = process.env.OPENAI_BASE_URL?.trim() || "https://api.openai.com/v1";
|
||||
return {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
api: "openai-responses",
|
||||
provider: "openai",
|
||||
baseUrl,
|
||||
reasoning: modelId.startsWith("gpt-5"),
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 400_000,
|
||||
maxTokens: 128_000,
|
||||
};
|
||||
}
|
||||
|
||||
function systemPromptForMode(mode: Mode): string {
|
||||
const base =
|
||||
"You are testing a fiction production service. Use the available tools, never invent data, and return only one minified JSON object. No markdown.";
|
||||
if (mode === "regular") {
|
||||
return `${base} Use the Fictions tools directly.`;
|
||||
}
|
||||
if (mode === "code-catalog") {
|
||||
return `${base} Use code_mode_exec with JavaScript and always return the final JSON object from the code. In code, call direct tool helpers such as await tools.fictions_list_titles({}), await tools.fictions_list_scenes({titleId:"PX-73"}), await tools.fictions_list_defects({titleId:"NM-12"}), await tools.fictions_list_invoices({}), and await tools.fictions_update_status({id:"PX-73",status:"preproduction"}). Call code_mode_wait until the code result is completed, then return that completed value as your final answer.`;
|
||||
}
|
||||
return `${base} Use code_mode_exec with JavaScript and always return the final JSON object from the code. In code, prefer the namespace helpers: return await Fictions.riskAudit(); return await Fictions.promoteIfReady("PX-73","preproduction"); return await Fictions.unpaidOver(5000). Call code_mode_wait until the code result is completed, then return that completed value as your final answer.`;
|
||||
}
|
||||
|
||||
function toolsForMode(mode: Mode, service: FictionService): AgentTool[] {
|
||||
const fictionTools = createFictionTools(service);
|
||||
if (mode === "regular") {
|
||||
return fictionTools as AgentTool[];
|
||||
}
|
||||
if (mode === "code-namespace") {
|
||||
registerFictionNamespace();
|
||||
} else {
|
||||
clearCodeModeNamespacesForPlugin(PLUGIN_ID);
|
||||
}
|
||||
const config = {
|
||||
tools: {
|
||||
codeMode: {
|
||||
enabled: true,
|
||||
timeoutMs: 20_000,
|
||||
maxPendingToolCalls: 32,
|
||||
},
|
||||
},
|
||||
};
|
||||
const catalogRef = createToolSearchCatalogRef();
|
||||
const codeModeTools = createCodeModeTools({
|
||||
config,
|
||||
runtimeConfig: config,
|
||||
sessionId: `live-${mode}`,
|
||||
sessionKey: `agent:live-${mode}:main`,
|
||||
agentId: "live",
|
||||
runId: `run-${mode}`,
|
||||
catalogRef,
|
||||
});
|
||||
const catalogTools =
|
||||
mode === "code-namespace" ? createFictionNamespaceTools(service) : fictionTools;
|
||||
return applyCodeModeCatalog({
|
||||
tools: [...codeModeTools, ...catalogTools],
|
||||
config,
|
||||
sessionId: `live-${mode}`,
|
||||
sessionKey: `agent:live-${mode}:main`,
|
||||
agentId: "live",
|
||||
runId: `run-${mode}`,
|
||||
catalogRef,
|
||||
}).tools as AgentTool[];
|
||||
}
|
||||
|
||||
function textFromMessageContent(content: unknown): string {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
}
|
||||
if (!Array.isArray(content)) {
|
||||
return "";
|
||||
}
|
||||
return content
|
||||
.filter(
|
||||
(entry) => entry && typeof entry === "object" && (entry as { type?: string }).type === "text",
|
||||
)
|
||||
.map((entry) => (entry as { text?: string }).text ?? "")
|
||||
.join("");
|
||||
}
|
||||
|
||||
function parseFirstJson(text: string): unknown {
|
||||
const trimmed = text.trim();
|
||||
try {
|
||||
return JSON.parse(trimmed) as unknown;
|
||||
} catch {
|
||||
const start = trimmed.indexOf("{");
|
||||
const end = trimmed.lastIndexOf("}");
|
||||
if (start >= 0 && end > start) {
|
||||
return JSON.parse(trimmed.slice(start, end + 1)) as unknown;
|
||||
}
|
||||
throw new Error("assistant did not return JSON");
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
const tasks: Task[] = [
|
||||
{
|
||||
id: "risk-audit",
|
||||
prompt:
|
||||
'Find the fiction title with the highest riskScore. Return JSON with keys task, id, lead, status, unresolvedDefects, blockedScenes. blockedScenes must be an array of blocked scene ids, not a count. task must be "risk-audit".',
|
||||
validate(answer) {
|
||||
if (!isRecord(answer)) {
|
||||
return { ok: false, reason: "answer is not an object" };
|
||||
}
|
||||
const blockedScenes = Array.isArray(answer.blockedScenes)
|
||||
? answer.blockedScenes.map(String).toSorted()
|
||||
: [];
|
||||
const unresolvedDefects = Array.isArray(answer.unresolvedDefects)
|
||||
? answer.unresolvedDefects.length
|
||||
: answer.unresolvedDefects;
|
||||
const ok =
|
||||
answer.task === "risk-audit" &&
|
||||
answer.id === "NM-12" &&
|
||||
answer.lead === "Oren Quill" &&
|
||||
answer.status === "revision" &&
|
||||
unresolvedDefects === 3 &&
|
||||
JSON.stringify(blockedScenes) === JSON.stringify(["NM-12-S2"]);
|
||||
return ok ? { ok } : { ok, reason: `unexpected risk audit: ${JSON.stringify(answer)}` };
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "promote",
|
||||
prompt:
|
||||
'For PX-73, if total scene pages are below 110 and every dependency is cleared, update its status to "preproduction". If a Fictions.promoteIfReady helper exists, use it. Return JSON with keys task, id, action, totalPages, finalStatus. action must be exactly "updated" when the command succeeds. task must be "promote".',
|
||||
validate(answer, service) {
|
||||
if (!isRecord(answer)) {
|
||||
return { ok: false, reason: "answer is not an object" };
|
||||
}
|
||||
const ok =
|
||||
answer.task === "promote" &&
|
||||
answer.id === "PX-73" &&
|
||||
typeof answer.action === "string" &&
|
||||
answer.action.includes("updated") &&
|
||||
answer.totalPages === 96 &&
|
||||
answer.finalStatus === "preproduction" &&
|
||||
service.currentStatus("PX-73") === "preproduction";
|
||||
return ok ? { ok } : { ok, reason: `unexpected promote result: ${JSON.stringify(answer)}` };
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "invoice",
|
||||
prompt:
|
||||
'For unpaid invoices over 5000, return JSON with keys task, invoiceIds, totalUnpaidOver5000. task must be "invoice".',
|
||||
validate(answer) {
|
||||
if (!isRecord(answer)) {
|
||||
return { ok: false, reason: "answer is not an object" };
|
||||
}
|
||||
const invoiceIds = Array.isArray(answer.invoiceIds)
|
||||
? answer.invoiceIds.map(String).toSorted()
|
||||
: [];
|
||||
const ok =
|
||||
answer.task === "invoice" &&
|
||||
JSON.stringify(invoiceIds) === JSON.stringify(["I-201"]) &&
|
||||
answer.totalUnpaidOver5000 === 6100;
|
||||
return ok ? { ok } : { ok, reason: `unexpected invoice result: ${JSON.stringify(answer)}` };
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
async function runOne(mode: Mode, task: Task, model: string, apiKey: string): Promise<RunMetrics> {
|
||||
const service = createFictionService();
|
||||
const counts = {
|
||||
modelTurns: 0,
|
||||
assistantMessages: 0,
|
||||
topLevelToolCalls: 0,
|
||||
};
|
||||
const toolResults: unknown[] = [];
|
||||
const agent = new Agent({
|
||||
sessionId: `code-mode-live-${mode}-${task.id}`,
|
||||
initialState: {
|
||||
model: createModel(model),
|
||||
systemPrompt: systemPromptForMode(mode),
|
||||
tools: toolsForMode(mode, service),
|
||||
thinkingLevel: "off",
|
||||
},
|
||||
getApiKey: (provider) => (provider === "openai" ? apiKey : undefined),
|
||||
toolExecution: "parallel",
|
||||
maxRetryDelayMs: 10_000,
|
||||
});
|
||||
agent.subscribe((event: AgentEvent) => {
|
||||
if (event.type === "turn_start") {
|
||||
counts.modelTurns += 1;
|
||||
} else if (event.type === "message_end" && event.message.role === "assistant") {
|
||||
counts.assistantMessages += 1;
|
||||
} else if (event.type === "tool_execution_start") {
|
||||
counts.topLevelToolCalls += 1;
|
||||
} else if (event.type === "tool_execution_end") {
|
||||
toolResults.push(event.result);
|
||||
}
|
||||
});
|
||||
|
||||
const started = performance.now();
|
||||
await agent.prompt(task.prompt);
|
||||
const latencyMs = Math.round(performance.now() - started);
|
||||
const lastAssistant = agent.state.messages
|
||||
.toReversed()
|
||||
.find((message) => message.role === "assistant");
|
||||
const finalText = textFromMessageContent(lastAssistant?.content).trim();
|
||||
let validation: { ok: boolean; reason?: string };
|
||||
try {
|
||||
validation = task.validate(parseFirstJson(finalText), service);
|
||||
} catch (error) {
|
||||
validation = {
|
||||
ok: false,
|
||||
reason: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
return {
|
||||
mode,
|
||||
task: task.id,
|
||||
ok: validation.ok,
|
||||
...(validation.reason ? { reason: validation.reason } : {}),
|
||||
latencyMs,
|
||||
modelTurns: counts.modelTurns,
|
||||
assistantMessages: counts.assistantMessages,
|
||||
topLevelToolCalls: counts.topLevelToolCalls,
|
||||
serviceCalls: service.calls,
|
||||
finalText,
|
||||
...(lastAssistant?.stopReason ? { stopReason: lastAssistant.stopReason } : {}),
|
||||
...(lastAssistant?.errorMessage ? { errorMessage: lastAssistant.errorMessage } : {}),
|
||||
...(process.env.OPENCLAW_CODE_MODE_LIVE_DEBUG === "1" ? { toolResults } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function readArg(name: string): string | undefined {
|
||||
const prefix = `--${name}=`;
|
||||
const match = process.argv.find((arg) => arg.startsWith(prefix));
|
||||
return match?.slice(prefix.length);
|
||||
}
|
||||
|
||||
export function parseTaskLimit(raw: string | undefined, label: string): number {
|
||||
const text = raw?.trim() ?? "3";
|
||||
if (!POSITIVE_INTEGER_PATTERN.test(text)) {
|
||||
throw new Error(`${label} must be a positive integer`);
|
||||
}
|
||||
const parsed = Number(text);
|
||||
if (!Number.isSafeInteger(parsed)) {
|
||||
throw new Error(`${label} must be a safe positive integer`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const model = readArg("model") ?? process.env.OPENCLAW_CODE_MODE_LIVE_MODEL ?? "gpt-5.4-mini";
|
||||
const modeArg = readArg("modes");
|
||||
const modes = (modeArg ? modeArg.split(",") : ["regular", "code-namespace"]) as Mode[];
|
||||
const taskArg = readArg("tasks");
|
||||
const taskLimit = parseTaskLimit(
|
||||
taskArg ?? process.env.OPENCLAW_CODE_MODE_LIVE_TASKS,
|
||||
taskArg === undefined ? "OPENCLAW_CODE_MODE_LIVE_TASKS" : "--tasks",
|
||||
);
|
||||
const apiKey = process.env.OPENAI_API_KEY?.trim();
|
||||
if (!apiKey) {
|
||||
throw new Error("OPENAI_API_KEY is required");
|
||||
}
|
||||
const selectedTasks = tasks.slice(0, taskLimit);
|
||||
const results: RunMetrics[] = [];
|
||||
for (const task of selectedTasks) {
|
||||
for (const mode of modes) {
|
||||
results.push(await runOne(mode, task, model, apiKey));
|
||||
}
|
||||
}
|
||||
const summary = {
|
||||
model,
|
||||
tasks: selectedTasks.map((task) => task.id),
|
||||
results,
|
||||
aggregate: modes.map((mode) => {
|
||||
const entries = results.filter((entry) => entry.mode === mode);
|
||||
return {
|
||||
mode,
|
||||
ok: entries.filter((entry) => entry.ok).length,
|
||||
total: entries.length,
|
||||
latencyMs: entries.reduce((sum, entry) => sum + entry.latencyMs, 0),
|
||||
modelTurns: entries.reduce((sum, entry) => sum + entry.modelTurns, 0),
|
||||
topLevelToolCalls: entries.reduce((sum, entry) => sum + entry.topLevelToolCalls, 0),
|
||||
serviceCalls: entries.reduce((sum, entry) => sum + entry.serviceCalls, 0),
|
||||
};
|
||||
}),
|
||||
};
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
if (results.some((entry) => !entry.ok)) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
await main().finally(() => {
|
||||
clearCodeModeNamespacesForPlugin(PLUGIN_ID);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Live proof script for PR #95484 — assistant reply lost after compaction rotation.
|
||||
*
|
||||
* Demonstrates that:
|
||||
* 1. BEFORE fix: successor context shows [compactionSummary, user, ...]
|
||||
* — the assistant reply is silently dropped.
|
||||
* 2. AFTER fix: successor context shows [compactionSummary, assistant, user, ...]
|
||||
* — the assistant reply is preserved.
|
||||
*
|
||||
* Usage: node --import tsx scripts/repro/issue-76729-compaction-assistant-loss-proof.mts
|
||||
*/
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { SessionManager } from "openclaw/plugin-sdk/agent-sessions";
|
||||
import type { AgentMessage } from "openclaw/plugin-sdk/agent-core";
|
||||
import type { TextContent } from "openclaw/plugin-sdk/llm";
|
||||
import { makeAgentAssistantMessage } from "../../src/agents/test-helpers/agent-message-fixtures.js";
|
||||
import { rotateTranscriptAfterCompaction } from "../../src/agents/embedded-agent-runner/compaction-successor-transcript.js";
|
||||
|
||||
const sessionDir = mkdtempSync(join(tmpdir(), "compaction-proof-"));
|
||||
console.log("Session dir:", sessionDir);
|
||||
console.log();
|
||||
|
||||
const manager = SessionManager.create(sessionDir, sessionDir);
|
||||
|
||||
// Build session: user("Summarize") → assistant("Here is the summary") → user("Analyze Q3") → assistant("Q3 analysis...") → compaction(firstKept=user_Analyze_Q3)
|
||||
manager.appendMessage({ role: "user", content: "Summarize reports", timestamp: 1 });
|
||||
manager.appendMessage(makeAgentAssistantMessage({ content: [{ type: "text", text: "Here is the summary" }], timestamp: 2 }));
|
||||
const firstKeptId = manager.appendMessage({ role: "user", content: "Analyze Q3", timestamp: 3 });
|
||||
manager.appendMessage(makeAgentAssistantMessage({ content: [{ type: "text", text: "Q3 analysis shows..." }], timestamp: 4 }));
|
||||
manager.appendCompaction("Summary of previous work.", firstKeptId, 5000);
|
||||
|
||||
// Post-compaction
|
||||
manager.appendMessage({ role: "user", content: "Any more insights?", timestamp: 5 });
|
||||
manager.appendMessage(makeAgentAssistantMessage({ content: [{ type: "text", text: "Additional insights" }], timestamp: 6 }));
|
||||
|
||||
const sessionFile = manager.getSessionFile()!;
|
||||
console.log("Source session file:", sessionFile);
|
||||
console.log();
|
||||
|
||||
const result = await rotateTranscriptAfterCompaction({
|
||||
sessionManager: manager,
|
||||
sessionFile,
|
||||
now: () => new Date("2026-06-21T12:00:00.000Z"),
|
||||
});
|
||||
|
||||
console.log("Rotation result:", JSON.stringify(result, null, 2));
|
||||
console.log();
|
||||
|
||||
// Open successor and inspect context
|
||||
const successor = SessionManager.open(result.sessionFile!);
|
||||
const context = successor.buildSessionContext();
|
||||
|
||||
console.log("=== SUCCESSOR CONTEXT ===");
|
||||
console.log("Roles:", JSON.stringify(context.messages.map((m) => m.role)));
|
||||
|
||||
for (const msg of context.messages) {
|
||||
if (msg.role === "compactionSummary") {
|
||||
const summary = (msg as AgentMessage & { summary: string }).summary;
|
||||
console.log(` [compactionSummary] summary="${summary}"`);
|
||||
} else if ("content" in msg) {
|
||||
const text = Array.isArray(msg.content)
|
||||
? (msg.content[0] as TextContent)?.text ?? JSON.stringify(msg.content)
|
||||
: msg.content;
|
||||
console.log(` [${msg.role}] "${text}"`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
|
||||
const roles = context.messages.map((m) => m.role);
|
||||
console.log("=== VERIFICATION ===");
|
||||
console.log("Role sequence:", JSON.stringify(roles));
|
||||
console.log("compactionSummary → assistant (no gap):", roles[0] === "compactionSummary" && roles[1] === "assistant");
|
||||
console.log("BEFORE fix shows: [compactionSummary, user, assistant, ...] ← missing assistant");
|
||||
console.log("AFTER fix shows:", JSON.stringify(roles));
|
||||
console.log();
|
||||
|
||||
// Cleanup
|
||||
rmSync(sessionDir, { recursive: true, force: true });
|
||||
console.log("Temp dir cleaned up.");
|
||||
36
scripts/repro/issue-95198-openrouter-short-model.mts
Normal file
36
scripts/repro/issue-95198-openrouter-short-model.mts
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Live repro for issue #95198: OpenRouter short model IDs get double-prefixed.
|
||||
*
|
||||
* Verifies that `openrouter/deepseek-v4-flash` resolves to the upstream API
|
||||
* model ID `deepseek/deepseek-v4-flash` instead of the broken
|
||||
* `openrouter/openrouter/deepseek-v4-flash`.
|
||||
*/
|
||||
import { normalizeOpenRouterApiModelId } from "../../extensions/openrouter/models.js";
|
||||
|
||||
const cases = [
|
||||
{ input: "openrouter/deepseek-v4-flash", expected: "deepseek/deepseek-v4-flash" },
|
||||
{ input: "openrouter/deepseek-v4-pro", expected: "deepseek/deepseek-v4-pro" },
|
||||
{ input: "openrouter/deepseek/deepseek-v4-flash", expected: "deepseek/deepseek-v4-flash" },
|
||||
{ input: "openrouter/auto", expected: "openrouter/auto" },
|
||||
{ input: "openrouter/free", expected: "openrouter/free" },
|
||||
{ input: "deepseek/deepseek-v4-flash", expected: "deepseek/deepseek-v4-flash" },
|
||||
];
|
||||
|
||||
let failed = false;
|
||||
console.log("=== Reproduction for issue #95198 ===");
|
||||
for (const { input, expected } of cases) {
|
||||
const actual = normalizeOpenRouterApiModelId(input);
|
||||
const pass = actual === expected;
|
||||
console.log(`${pass ? "PASS" : "FAIL"}: ${input} -> ${actual} (expected ${expected})`);
|
||||
if (!pass) {
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
console.error("\nFAIL: one or more OpenRouter model ID normalizations are incorrect.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log("\nPASS: short OpenRouter model IDs resolve to the correct upstream slugs.");
|
||||
}
|
||||
133
scripts/repro/limit-edge-case-live-proof.mjs
Normal file
133
scripts/repro/limit-edge-case-live-proof.mjs
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env node
|
||||
// Live repro for numeric limit edge cases across diagnostics, usage, and voice-call CLI.
|
||||
import assert from "node:assert/strict";
|
||||
/**
|
||||
* Live repro for limit/CLI numeric fixes (PR #82679). Run: pnpm exec tsx scripts/repro/limit-edge-case-live-proof.mjs
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { testing as voiceCallCliTesting } from "../../extensions/voice-call/src/cli.ts";
|
||||
import { loadSessionLogs, loadSessionUsageTimeSeries } from "../../src/infra/session-cost-usage.ts";
|
||||
import {
|
||||
getRecentDiagnosticPhases,
|
||||
recordDiagnosticPhase,
|
||||
resetDiagnosticPhasesForTest,
|
||||
} from "../../src/logging/diagnostic-phase.ts";
|
||||
|
||||
/**
|
||||
* Creates and cleans a temp root for live proof fixtures.
|
||||
*/
|
||||
export async function withProofTempRoot(callback) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-proof-"));
|
||||
try {
|
||||
return await callback(root);
|
||||
} finally {
|
||||
fs.rmSync(root, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
resetDiagnosticPhasesForTest();
|
||||
recordDiagnosticPhase({
|
||||
name: "phase-a",
|
||||
startedAt: 1,
|
||||
endedAt: 2,
|
||||
durationMs: 1,
|
||||
cpuUserMs: 0,
|
||||
cpuSystemMs: 0,
|
||||
cpuTotalMs: 0,
|
||||
cpuCoreRatio: 0,
|
||||
});
|
||||
recordDiagnosticPhase({
|
||||
name: "phase-b",
|
||||
startedAt: 3,
|
||||
endedAt: 4,
|
||||
durationMs: 1,
|
||||
cpuUserMs: 0,
|
||||
cpuSystemMs: 0,
|
||||
cpuTotalMs: 0,
|
||||
cpuCoreRatio: 0,
|
||||
});
|
||||
const zeroPhases = getRecentDiagnosticPhases(0);
|
||||
assert.equal(zeroPhases.length, 0);
|
||||
console.log("getRecentDiagnosticPhases(0).length =", zeroPhases.length);
|
||||
|
||||
await withProofTempRoot(async (root) => {
|
||||
const sessionFile = path.join(root, "s.jsonl");
|
||||
fs.writeFileSync(
|
||||
sessionFile,
|
||||
[
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
timestamp: "2026-01-01T00:00:00.000Z",
|
||||
message: { role: "user", content: "a" },
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
timestamp: "2026-01-01T00:01:00.000Z",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: "b",
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
usage: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 3,
|
||||
cost: { total: 0.001 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
timestamp: "2026-01-01T00:02:00.000Z",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: "c",
|
||||
provider: "openai",
|
||||
model: "gpt-5.5",
|
||||
usage: {
|
||||
input: 3,
|
||||
output: 4,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 7,
|
||||
cost: { total: 0.002 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const logs = await loadSessionLogs({ sessionFile, limit: 0 });
|
||||
const series = await loadSessionUsageTimeSeries({ sessionFile, maxPoints: 0 });
|
||||
const positiveLogs = await loadSessionLogs({ sessionFile, limit: 10 });
|
||||
const positiveSeries = await loadSessionUsageTimeSeries({ sessionFile, maxPoints: 10 });
|
||||
assert.equal(logs?.length, 0);
|
||||
assert.equal(series.points.length, 0);
|
||||
assert.equal(positiveLogs?.length, 3);
|
||||
assert.equal(positiveSeries.points.length, 2);
|
||||
console.log("loadSessionLogs({ limit: 0 }).length =", logs?.length);
|
||||
console.log(
|
||||
"loadSessionUsageTimeSeries({ maxPoints: 0 }).points.length =",
|
||||
series?.points.length,
|
||||
);
|
||||
|
||||
try {
|
||||
voiceCallCliTesting.parseVoiceCallIntOption("nope", "--port", { min: 1 });
|
||||
assert.fail("expected invalid voicecall --port value to throw");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
assert.equal(message, "Invalid numeric value for --port: nope");
|
||||
console.log("parseVoiceCallIntOption('nope', '--port') error:", message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main();
|
||||
}
|
||||
4
scripts/repro/tsx-name-repro.ts
Normal file
4
scripts/repro/tsx-name-repro.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Tsx Name Repro script supports OpenClaw repository automation.
|
||||
import "../../src/logging/subsystem.js";
|
||||
|
||||
console.log("tsx-name-repro: loaded logging/subsystem");
|
||||
Reference in New Issue
Block a user