mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
clawdbot-d02.1.9.1.20: add public SDK transcript identity API
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
19bdf1196ec771a00777a16fd1e9c3662b8fd788a81034e705c41a74ee79c7ec plugin-sdk-api-baseline.json
|
||||
43feff80c90adad0f821d1f1e184a9bff1e93d81e6d53a26a26fd9e2972be759 plugin-sdk-api-baseline.jsonl
|
||||
15ceed8879fabeecd3e7c87726f121b64ee490b5c92135ecb3915490dc71934a plugin-sdk-api-baseline.json
|
||||
d2be9469fa4952b4861c1a98dc0dee0327057afe98e2ac6b35a90ce5b0091f57 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -166,6 +166,8 @@ two-party event loops that do not go through the shared inbound reply runner.
|
||||
|
||||
Prefer `getSessionEntry(...)`, `listSessionEntries(...)`, `patchSessionEntry(...)`, or `upsertSessionEntry(...)` for session workflows. These helpers address sessions by agent/session identity so plugins do not depend on the legacy `sessions.json` storage shape. Use `preserveActivity: true` for metadata-only patches that should not refresh session activity, and `replaceEntry: true` only when the callback returns a complete entry and deleted fields must stay deleted. `loadSessionStore(...)` remains as a deprecated compatibility escape hatch for callers that intentionally need a mutable whole-store clone.
|
||||
|
||||
For transcript reads, import `openclaw/plugin-sdk/session-transcript-runtime` and use `resolveSessionTranscriptIdentity(...)` or `readSessionTranscriptEvents(...)` with `{ agentId, sessionKey, sessionId }`. That API returns stable transcript identity and storage-neutral memory hit keys without exposing `sessionFile` as the identity. File-path helpers such as `resolveSessionFilePath(...)`, `resolveAndPersistSessionFile(...)`, and `readLatestAssistantTextFromSessionTranscript(...)` are legacy compatibility exports for plugins that still own file-backed behavior.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="api.runtime.agent.defaults">
|
||||
Default model and provider constants:
|
||||
|
||||
@@ -245,6 +245,7 @@ usage endpoint failed or returned no usable usage data.
|
||||
| `plugin-sdk/reply-reference` | `createReplyReferencePlanner` |
|
||||
| `plugin-sdk/reply-chunking` | Narrow text/markdown chunking helpers |
|
||||
| `plugin-sdk/session-store-runtime` | Session workflow helpers (`getSessionEntry`, `listSessionEntries`, `patchSessionEntry`, `upsertSessionEntry`), legacy session store path/session-key helpers, updated-at reads, and deprecated whole-store mutation helpers |
|
||||
| `plugin-sdk/session-transcript-runtime` | Transcript identity, read-events, and storage-neutral memory hit key helpers that do not expose `sessionFile` as identity |
|
||||
| `plugin-sdk/cron-store-runtime` | Cron store path/load/save helpers |
|
||||
| `plugin-sdk/state-paths` | State/OAuth dir path helpers |
|
||||
| `plugin-sdk/plugin-state-runtime` | Plugin sidecar SQLite keyed-state types |
|
||||
|
||||
@@ -949,6 +949,10 @@
|
||||
"types": "./dist/plugin-sdk/session-transcript-hit.d.ts",
|
||||
"default": "./dist/plugin-sdk/session-transcript-hit.js"
|
||||
},
|
||||
"./plugin-sdk/session-transcript-runtime": {
|
||||
"types": "./dist/plugin-sdk/session-transcript-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/session-transcript-runtime.js"
|
||||
},
|
||||
"./plugin-sdk/session-visibility": {
|
||||
"types": "./dist/plugin-sdk/session-visibility.d.ts",
|
||||
"default": "./dist/plugin-sdk/session-visibility.js"
|
||||
|
||||
@@ -104,6 +104,9 @@ export const pluginSdkDocMetadata = {
|
||||
"runtime-store": {
|
||||
category: "runtime",
|
||||
},
|
||||
"session-transcript-runtime": {
|
||||
category: "runtime",
|
||||
},
|
||||
"qa-live-transport-scenarios": {
|
||||
category: "utilities",
|
||||
},
|
||||
|
||||
@@ -216,6 +216,7 @@
|
||||
"session-key-runtime",
|
||||
"session-store-runtime",
|
||||
"session-transcript-hit",
|
||||
"session-transcript-runtime",
|
||||
"session-visibility",
|
||||
"ssrf-dispatcher",
|
||||
"string-coerce-runtime",
|
||||
|
||||
@@ -3,6 +3,9 @@ import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import {
|
||||
extractTranscriptIdentityFromSessionsMemoryHit,
|
||||
extractTranscriptStemFromSessionsMemoryHit,
|
||||
formatSessionTranscriptMemoryHitKey,
|
||||
parseSessionTranscriptMemoryHitKey,
|
||||
resolveSessionTranscriptMemoryHitKeyToSessionKeys,
|
||||
resolveTranscriptStemToSessionKeys,
|
||||
} from "./session-transcript-hit.js";
|
||||
|
||||
@@ -272,3 +275,28 @@ describe("resolveTranscriptStemToSessionKeys", () => {
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("session transcript memory hit key compatibility exports", () => {
|
||||
it("exports storage-neutral memory hit key helpers from the legacy hit subpath", () => {
|
||||
const key = formatSessionTranscriptMemoryHitKey({
|
||||
agentId: "main",
|
||||
sessionId: "session:legacy",
|
||||
});
|
||||
const store: Record<string, SessionEntry> = {
|
||||
"agent:main:discord:direct:42": {
|
||||
sessionFile: "/tmp/not-the-identity.jsonl",
|
||||
sessionId: "session:legacy",
|
||||
updatedAt: 10,
|
||||
},
|
||||
};
|
||||
|
||||
expect(key).toBe("transcript:main:session%3Alegacy");
|
||||
expect(parseSessionTranscriptMemoryHitKey(key)).toMatchObject({
|
||||
agentId: "main",
|
||||
sessionId: "session:legacy",
|
||||
});
|
||||
expect(resolveSessionTranscriptMemoryHitKeyToSessionKeys({ key, store })).toEqual([
|
||||
"agent:main:discord:direct:42",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,19 @@ import { uniqueStrings } from "../../packages/normalization-core/src/string-norm
|
||||
import { parseUsageCountedSessionIdFromFileName } from "../config/sessions/artifacts.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import { normalizeAgentId } from "../routing/session-key.js";
|
||||
export {
|
||||
formatSessionTranscriptMemoryHitKey,
|
||||
parseSessionTranscriptMemoryHitKey,
|
||||
resolveSessionTranscriptMemoryHitKeyToSessionKeys,
|
||||
} from "./session-transcript-runtime.js";
|
||||
export type {
|
||||
ResolveSessionTranscriptMemoryHitKeyParams,
|
||||
SessionTranscriptIdentity,
|
||||
SessionTranscriptMemoryHitIdentity,
|
||||
SessionTranscriptMemoryHitKey,
|
||||
SessionTranscriptMemoryHitKeyParams,
|
||||
SessionTranscriptReadParams,
|
||||
} from "./session-transcript-runtime.js";
|
||||
|
||||
export { loadCombinedSessionStoreForGateway } from "../config/sessions/combined-store-gateway.js";
|
||||
|
||||
|
||||
104
src/plugin-sdk/session-transcript-runtime.test.ts
Normal file
104
src/plugin-sdk/session-transcript-runtime.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { appendTranscriptEvent, upsertSessionEntry } from "../config/sessions/session-accessor.js";
|
||||
import { loadSessionStore } from "../config/sessions/store.js";
|
||||
import {
|
||||
formatSessionTranscriptMemoryHitKey,
|
||||
parseSessionTranscriptMemoryHitKey,
|
||||
readSessionTranscriptEvents,
|
||||
resolveSessionTranscriptIdentity,
|
||||
resolveSessionTranscriptMemoryHitKeyToSessionKeys,
|
||||
} from "./session-transcript-runtime.js";
|
||||
|
||||
describe("session transcript runtime SDK", () => {
|
||||
let tempDir: string;
|
||||
let storePath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-transcript-"));
|
||||
storePath = path.join(tempDir, "sessions.json");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
it("resolves transcript identity and reads events without returning sessionFile", async () => {
|
||||
const scope = {
|
||||
agentId: "Main",
|
||||
sessionId: "session-with-colon",
|
||||
sessionKey: "agent:main:main",
|
||||
storePath,
|
||||
};
|
||||
const event = { id: "event-1", type: "message" };
|
||||
|
||||
await upsertSessionEntry(scope, { sessionId: scope.sessionId, updatedAt: 10 });
|
||||
await appendTranscriptEvent(scope, event);
|
||||
|
||||
const identity = await resolveSessionTranscriptIdentity(scope);
|
||||
|
||||
expect(identity).toEqual({
|
||||
agentId: "main",
|
||||
memoryKey: "transcript:main:session-with-colon",
|
||||
sessionId: scope.sessionId,
|
||||
sessionKey: "agent:main:main",
|
||||
});
|
||||
expect(identity).not.toHaveProperty("sessionFile");
|
||||
await expect(readSessionTranscriptEvents(scope)).resolves.toEqual([event]);
|
||||
});
|
||||
|
||||
it("round-trips encoded memory hit keys with opaque session ids", () => {
|
||||
const key = formatSessionTranscriptMemoryHitKey({
|
||||
agentId: "SECONDARY",
|
||||
sessionId: "my-plugin:task/1",
|
||||
});
|
||||
|
||||
expect(key).toBe("transcript:secondary:my-plugin%3Atask%2F1");
|
||||
expect(parseSessionTranscriptMemoryHitKey(key)).toEqual({
|
||||
agentId: "secondary",
|
||||
key,
|
||||
sessionId: "my-plugin:task/1",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves memory hit keys by agent and session id instead of transcript basename", async () => {
|
||||
const scope = {
|
||||
agentId: "main",
|
||||
sessionId: "session-id",
|
||||
sessionKey: "agent:main:telegram:direct:123",
|
||||
storePath,
|
||||
};
|
||||
await upsertSessionEntry(scope, {
|
||||
sessionFile: path.join(tempDir, "legacy-file-name.jsonl"),
|
||||
sessionId: scope.sessionId,
|
||||
updatedAt: 10,
|
||||
});
|
||||
|
||||
const keys = resolveSessionTranscriptMemoryHitKeyToSessionKeys({
|
||||
key: formatSessionTranscriptMemoryHitKey(scope),
|
||||
store: loadSessionStore(storePath),
|
||||
});
|
||||
|
||||
expect(keys).toEqual(["agent:main:telegram:direct:123"]);
|
||||
});
|
||||
|
||||
it("can avoid synthetic fallback keys for strict live-store checks", () => {
|
||||
const key = formatSessionTranscriptMemoryHitKey({
|
||||
agentId: "main",
|
||||
sessionId: "deleted-session",
|
||||
});
|
||||
|
||||
expect(resolveSessionTranscriptMemoryHitKeyToSessionKeys({ key, store: {} })).toEqual([
|
||||
"agent:main:deleted-session",
|
||||
]);
|
||||
expect(
|
||||
resolveSessionTranscriptMemoryHitKeyToSessionKeys({
|
||||
includeSyntheticFallback: false,
|
||||
key,
|
||||
store: {},
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
151
src/plugin-sdk/session-transcript-runtime.ts
Normal file
151
src/plugin-sdk/session-transcript-runtime.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { normalizeOptionalString } from "../../packages/normalization-core/src/string-coerce.js";
|
||||
import { uniqueStrings } from "../../packages/normalization-core/src/string-normalization.js";
|
||||
import {
|
||||
loadTranscriptEvents,
|
||||
resolveSessionTranscriptRuntimeTarget,
|
||||
} from "../config/sessions/session-accessor.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import { normalizeAgentId, resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
||||
|
||||
const SESSION_TRANSCRIPT_MEMORY_HIT_PREFIX = "transcript";
|
||||
|
||||
export type SessionTranscriptEvent = unknown;
|
||||
|
||||
export type SessionTranscriptIdentity = {
|
||||
agentId: string;
|
||||
memoryKey: SessionTranscriptMemoryHitKey;
|
||||
sessionId: string;
|
||||
sessionKey: string;
|
||||
};
|
||||
|
||||
export type SessionTranscriptMemoryHitIdentity = {
|
||||
agentId: string;
|
||||
key: SessionTranscriptMemoryHitKey;
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
export type SessionTranscriptMemoryHitKey = `transcript:${string}:${string}`;
|
||||
|
||||
export type SessionTranscriptReadParams = {
|
||||
agentId?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
hydrateSkillPromptRefs?: boolean;
|
||||
sessionId: string;
|
||||
sessionKey: string;
|
||||
storePath?: string;
|
||||
threadId?: string | number;
|
||||
};
|
||||
|
||||
export type SessionTranscriptMemoryHitKeyParams = {
|
||||
agentId: string;
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
export type ResolveSessionTranscriptMemoryHitKeyParams = {
|
||||
includeSyntheticFallback?: boolean;
|
||||
key: string;
|
||||
store: Record<string, SessionEntry>;
|
||||
};
|
||||
|
||||
function requireMemoryKeySegment(value: string, label: string): string {
|
||||
const normalized = normalizeOptionalString(value);
|
||||
if (!normalized) {
|
||||
throw new Error(`Cannot build session transcript memory hit key without ${label}.`);
|
||||
}
|
||||
return encodeURIComponent(normalized);
|
||||
}
|
||||
|
||||
function decodeMemoryKeySegment(value: string): string | null {
|
||||
try {
|
||||
return normalizeOptionalString(decodeURIComponent(value)) ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function syntheticSessionKey(identity: SessionTranscriptMemoryHitIdentity): string {
|
||||
return `agent:${identity.agentId}:${identity.sessionId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the storage-neutral memory hit key for one session transcript.
|
||||
*/
|
||||
export function formatSessionTranscriptMemoryHitKey(
|
||||
params: SessionTranscriptMemoryHitKeyParams,
|
||||
): SessionTranscriptMemoryHitKey {
|
||||
const agentId = requireMemoryKeySegment(normalizeAgentId(params.agentId), "agentId");
|
||||
const sessionId = requireMemoryKeySegment(params.sessionId, "sessionId");
|
||||
return `${SESSION_TRANSCRIPT_MEMORY_HIT_PREFIX}:${agentId}:${sessionId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a storage-neutral session transcript memory hit key.
|
||||
*/
|
||||
export function parseSessionTranscriptMemoryHitKey(
|
||||
key: string,
|
||||
): SessionTranscriptMemoryHitIdentity | null {
|
||||
const parts = key.split(":");
|
||||
if (parts.length !== 3 || parts[0] !== SESSION_TRANSCRIPT_MEMORY_HIT_PREFIX) {
|
||||
return null;
|
||||
}
|
||||
const agentId = decodeMemoryKeySegment(parts[1] ?? "");
|
||||
const sessionId = decodeMemoryKeySegment(parts[2] ?? "");
|
||||
if (!agentId || !sessionId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
agentId: normalizeAgentId(agentId),
|
||||
key: formatSessionTranscriptMemoryHitKey({ agentId, sessionId }),
|
||||
sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the public identity for a transcript without returning its file path.
|
||||
*/
|
||||
export async function resolveSessionTranscriptIdentity(
|
||||
params: SessionTranscriptReadParams,
|
||||
): Promise<SessionTranscriptIdentity> {
|
||||
const target = await resolveSessionTranscriptRuntimeTarget(params);
|
||||
const agentId = normalizeAgentId(target.agentId);
|
||||
return {
|
||||
agentId,
|
||||
memoryKey: formatSessionTranscriptMemoryHitKey({ agentId, sessionId: target.sessionId }),
|
||||
sessionId: target.sessionId,
|
||||
sessionKey: target.sessionKey,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads transcript events by public session identity instead of file path.
|
||||
*/
|
||||
export async function readSessionTranscriptEvents(
|
||||
params: SessionTranscriptReadParams,
|
||||
): Promise<SessionTranscriptEvent[]> {
|
||||
return await loadTranscriptEvents(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a storage-neutral memory hit key back to visible session store keys.
|
||||
*/
|
||||
export function resolveSessionTranscriptMemoryHitKeyToSessionKeys(
|
||||
params: ResolveSessionTranscriptMemoryHitKeyParams,
|
||||
): string[] {
|
||||
const identity = parseSessionTranscriptMemoryHitKey(params.key);
|
||||
if (!identity) {
|
||||
return [];
|
||||
}
|
||||
const matches = Object.entries(params.store)
|
||||
.filter(([sessionKey, entry]) => {
|
||||
return (
|
||||
entry.sessionId === identity.sessionId &&
|
||||
normalizeAgentId(resolveAgentIdFromSessionKey(sessionKey)) === identity.agentId
|
||||
);
|
||||
})
|
||||
.map(([sessionKey]) => sessionKey);
|
||||
const deduped = uniqueStrings(matches);
|
||||
if (deduped.length > 0) {
|
||||
return deduped;
|
||||
}
|
||||
return params.includeSyntheticFallback === false ? [] : [syntheticSessionKey(identity)];
|
||||
}
|
||||
Reference in New Issue
Block a user