Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions hindsight-integrations/openclaw/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
truncateRecallQuery,
buildRetainRequest,
meetsMinimumVersion,
getAppendCapabilityFromVersionPayload,
parseSessionKey,
extractTelegramDirectSenderId,
resolveSessionIdentity,
Expand Down Expand Up @@ -1537,6 +1538,55 @@ describe("meetsMinimumVersion", () => {
});
});

describe("getAppendCapabilityFromVersionPayload", () => {
it("supports append for APIs at the minimum version when feature metadata is absent", () => {
expect(getAppendCapabilityFromVersionPayload({ api_version: "0.5.0" })).toEqual({
version: "0.5.0",
supported: true,
reason: "supported",
});
});

it("supports append when stored document text is explicitly enabled", () => {
expect(
getAppendCapabilityFromVersionPayload({
api_version: "0.8.4",
features: { store_document_text: true },
})
).toEqual({
version: "0.8.4",
supported: true,
reason: "supported",
});
});

it("disables append when stored document text is explicitly disabled", () => {
expect(
getAppendCapabilityFromVersionPayload({
api_version: "0.8.4",
features: { store_document_text: false },
})
).toEqual({
version: "0.8.4",
supported: false,
reason: "store_document_text_disabled",
});
});

it("disables append for legacy or malformed version payloads", () => {
expect(getAppendCapabilityFromVersionPayload({ api_version: "0.4.99" })).toEqual({
version: "0.4.99",
supported: false,
reason: "legacy",
});
expect(getAppendCapabilityFromVersionPayload({ api_version: 804 })).toEqual({
version: null,
supported: false,
reason: "legacy",
});
});
});

// ---------------------------------------------------------------------------
// getPluginConfig — whitelist normalisation
// ---------------------------------------------------------------------------
Expand Down
76 changes: 60 additions & 16 deletions hindsight-integrations/openclaw/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ let supportsUpdateModeAppend = false;
let appendCapabilityProbed = false;
const MIN_VERSION_FOR_UPDATE_MODE_APPEND = "0.5.0";

interface HindsightVersionPayload {
api_version?: unknown;
features?: {
store_document_text?: unknown;
};
}

interface AppendCapabilityResult {
version: string | null;
supported: boolean;
reason: "supported" | "legacy" | "store_document_text_disabled";
}

// Store the current plugin config for bank ID derivation
let currentPluginConfig: PluginConfig | null = null;

Expand Down Expand Up @@ -1383,15 +1396,33 @@ export function meetsMinimumVersion(actual: string, minimum: string): boolean {
}

/**
* Probe `<apiUrl>/version` once at service.start to learn the running
* Hindsight API version. Returns `null` (treated as "no append support") if
* the endpoint is unreachable or returns malformed payload — conservative
* Interpret the `/version` payload as an `update_mode: 'append'` capability.
* `features.store_document_text` was added after append support, so missing
* feature metadata keeps the historical version-only behavior for older APIs.
*/
export function getAppendCapabilityFromVersionPayload(
data: HindsightVersionPayload
): AppendCapabilityResult {
const version = typeof data.api_version === "string" ? data.api_version : null;
if (!version || !meetsMinimumVersion(version, MIN_VERSION_FOR_UPDATE_MODE_APPEND)) {
return { version, supported: false, reason: "legacy" };
}
if (data.features?.store_document_text === false) {
return { version, supported: false, reason: "store_document_text_disabled" };
}
return { version, supported: true, reason: "supported" };
}

/**
* Probe `<apiUrl>/version` once at service.start to learn whether the running
* Hindsight API supports `update_mode: 'append'`. Returns an unsupported result
* if the endpoint is unreachable or returns malformed payload — conservative
* fallback path is the right call when we can't be sure.
*/
async function fetchHindsightApiVersion(
async function fetchHindsightAppendCapability(
apiUrl: string,
apiToken?: string | null
): Promise<string | null> {
): Promise<AppendCapabilityResult> {
const versionUrl = `${apiUrl.replace(/\/$/, "")}/version`;
try {
const headers: Record<string, string> = { "User-Agent": USER_AGENT };
Expand All @@ -1402,17 +1433,18 @@ async function fetchHindsightApiVersion(
});
if (!response.ok) {
debug(`[Hindsight] /version returned HTTP ${response.status}; assuming legacy`);
return null;
return { version: null, supported: false, reason: "legacy" };
}
const data = (await response.json()) as { api_version?: unknown };
const v = typeof data.api_version === "string" ? data.api_version : null;
if (!v) {
const capability = getAppendCapabilityFromVersionPayload(
(await response.json()) as HindsightVersionPayload
);
if (!capability.version) {
debug(`[Hindsight] /version payload missing api_version; assuming legacy`);
}
return v;
return capability;
} catch (error) {
debug(`[Hindsight] /version probe failed: ${String(error)}; assuming legacy`);
return null;
return { version: null, supported: false, reason: "legacy" };
}
}

Expand All @@ -1426,9 +1458,8 @@ async function fetchHindsightApiVersion(
* always re-evaluated when the plugin (re)connects to the API.
*/
async function detectAppendCapability(apiUrl: string, apiToken?: string | null): Promise<void> {
const version = await fetchHindsightApiVersion(apiUrl, apiToken);
const supported =
version !== null && meetsMinimumVersion(version, MIN_VERSION_FOR_UPDATE_MODE_APPEND);
const capability = await fetchHindsightAppendCapability(apiUrl, apiToken);
const { version, supported } = capability;
const transitionedToUnsupported = supportsUpdateModeAppend && !supported;
const firstProbe = !appendCapabilityProbed;
appendCapabilityProbed = true;
Expand All @@ -1437,14 +1468,27 @@ async function detectAppendCapability(apiUrl: string, apiToken?: string | null):
debug(`[Hindsight] API version ${version} supports update_mode=append`);
return;
}
if (capability.reason === "store_document_text_disabled") {
debug(
`[Hindsight] API version ${version} disables stored document text; update_mode=append is unavailable`
);
}
// Warn on the first probe when unsupported, and on any transition from
// supported -> unsupported. Stay silent on subsequent re-probes that
// confirm the same unsupported state.
if (!firstProbe && !transitionedToUnsupported) return;
const reason =
capability.reason === "store_document_text_disabled"
? "because stored document text is disabled"
: `which is older than ${MIN_VERSION_FOR_UPDATE_MODE_APPEND}`;
const remediation =
capability.reason === "store_document_text_disabled"
? "Enable stored document text on the Hindsight API to use session-scoped retention with update_mode=append."
: `Upgrade Hindsight to ${MIN_VERSION_FOR_UPDATE_MODE_APPEND} or newer to enable session-scoped retention with update_mode=append.`;
log.warn(
`[Hindsight] ⚠️ API at ${apiUrl} reports version "${version ?? "unknown"}", which is older than ${MIN_VERSION_FOR_UPDATE_MODE_APPEND}. ` +
`[Hindsight] ⚠️ API at ${apiUrl} reports version "${version ?? "unknown"}", ${reason}. ` +
`Falling back to per-turn document ids — each retain becomes its own document instead of accumulating into one per-session document. ` +
`Upgrade Hindsight to ${MIN_VERSION_FOR_UPDATE_MODE_APPEND} or newer to enable session-scoped retention with update_mode=append.`
remediation
);
}

Expand Down