mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
* feat(gateway): stream node.invoke progress with idle timeouts and cancel * feat(node-host): opt-in approval-gated claude cli agent run command * feat(agents): run node-placed claude-cli turns through streaming node invoke * feat(anthropic): continue claude catalog sessions on advertising nodes * docs(nodes): document paired-node claude agent runs and continuation * chore(plugin-sdk): admit spawn-invocation helper into surface budget * fix(nodes): harden streaming invoke and node run policy from review findings * test(node-host): prove multi-token tool-policy argv fails closed * fix(nodes): allowlist cancel event for apps and break node-host type cycle * chore(protocol): regenerate Swift gateway models for node invoke progress * chore(node-host): drop test-only export surface flagged by deadcode ratchet * style(node-host): merge invoke-types imports * refactor(nodes): split node agent run modules to satisfy the LOC ratchet * chore(protocol): regenerate android gateway models and sync export baseline * chore(plugin-sdk): admit spawn-invocation helper and inherited deprecated drift into surface budget * chore(plugin-sdk): tolerate inherited agent-core deprecated export * fix(anthropic): drop node param parser orphaned by upstream catalog split * ci: sync unused-export baseline with healed main * test(agents): adopt renamed cli backend prepare helper * style(node-host): format claude run spawn invocation * ci: retrigger checks * chore(plugin-sdk): pin surface budgets to healed-base counts * fix(protocol): restore untyped lazy validators after upstream style change
107 lines
3.2 KiB
TypeScript
107 lines
3.2 KiB
TypeScript
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
|
|
const MAX_TRANSCRIPT_ITEM_BYTES = 4 * 1024 * 1024;
|
|
const MAX_TRANSCRIPT_TEXT_LENGTH = 1_000_000;
|
|
|
|
export type ClaudeTranscriptItem = {
|
|
type: string;
|
|
text?: string;
|
|
content?: unknown;
|
|
timestamp?: string;
|
|
model?: string;
|
|
uuid?: string;
|
|
truncated?: true;
|
|
};
|
|
|
|
function transcriptItemType(role: string, content: unknown): string {
|
|
if (!Array.isArray(content)) {
|
|
return role === "user" ? "userMessage" : "agentMessage";
|
|
}
|
|
const types = content.flatMap((block) =>
|
|
isRecord(block) && typeof block.type === "string" ? [block.type] : [],
|
|
);
|
|
if (types.length > 0 && types.every((type) => type === "tool_result")) {
|
|
return "toolResult";
|
|
}
|
|
if (types.length > 0 && types.every((type) => type === "tool_use")) {
|
|
return "toolCall";
|
|
}
|
|
if (types.length > 0 && types.every((type) => type === "thinking")) {
|
|
return "reasoning";
|
|
}
|
|
return role === "user" ? "userMessage" : "agentMessage";
|
|
}
|
|
|
|
export function collectTranscriptText(value: unknown, fragments: string[]): void {
|
|
if (typeof value === "string") {
|
|
if (value.trim()) {
|
|
fragments.push(value);
|
|
}
|
|
return;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) {
|
|
collectTranscriptText(item, fragments);
|
|
}
|
|
return;
|
|
}
|
|
if (!isRecord(value)) {
|
|
return;
|
|
}
|
|
for (const key of ["text", "thinking", "content", "input"]) {
|
|
if (key in value) {
|
|
collectTranscriptText(value[key], fragments);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function parseTranscriptLine(
|
|
line: Buffer,
|
|
optionalString: (value: unknown, maxLength?: number) => string | undefined,
|
|
): ClaudeTranscriptItem | undefined {
|
|
let raw: unknown;
|
|
try {
|
|
raw = JSON.parse(line.toString("utf8")) as unknown;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
if (!isRecord(raw) || raw.isSidechain === true || !isRecord(raw.message)) {
|
|
return undefined;
|
|
}
|
|
const role = raw.message.role;
|
|
if ((role !== "user" && role !== "assistant") || raw.type !== role) {
|
|
return undefined;
|
|
}
|
|
const content = raw.message.content;
|
|
if (typeof content !== "string" && !Array.isArray(content)) {
|
|
return undefined;
|
|
}
|
|
const fragments: string[] = [];
|
|
collectTranscriptText(content, fragments);
|
|
const text = [...new Set(fragments)].join("\n\n");
|
|
const item: ClaudeTranscriptItem = {
|
|
type: transcriptItemType(role, content),
|
|
...(text ? { text } : {}),
|
|
content,
|
|
...(optionalString(raw.timestamp, 128)
|
|
? { timestamp: optionalString(raw.timestamp, 128) }
|
|
: {}),
|
|
...(optionalString(raw.message.model, 256)
|
|
? { model: optionalString(raw.message.model, 256) }
|
|
: {}),
|
|
...(optionalString(raw.uuid, 256) ? { uuid: optionalString(raw.uuid, 256) } : {}),
|
|
};
|
|
if (Buffer.byteLength(JSON.stringify(item), "utf8") <= MAX_TRANSCRIPT_ITEM_BYTES) {
|
|
return item;
|
|
}
|
|
return {
|
|
type: item.type,
|
|
text: `${truncateUtf16Safe(text, MAX_TRANSCRIPT_TEXT_LENGTH)}\n\n[oversized Claude item truncated]`,
|
|
...(item.timestamp ? { timestamp: item.timestamp } : {}),
|
|
...(item.model ? { model: item.model } : {}),
|
|
...(item.uuid ? { uuid: item.uuid } : {}),
|
|
truncated: true,
|
|
};
|
|
}
|