fix: preserve sqlite fallback routing

This commit is contained in:
Peter Steinberger
2026-05-16 15:52:42 +01:00
parent 8d3143d258
commit c8a21b6efb
4 changed files with 84 additions and 8 deletions

View File

@@ -57,7 +57,11 @@ public enum DeviceAuthStore {
self.removeLegacyToken(deviceId: deviceId, role: normalizedRole)
}
} catch {
self.writeLegacyStore(DeviceAuthStoreFile(version: 1, deviceId: deviceId, tokens: [normalizedRole: entry]))
var fallback =
self.readLegacyStore().flatMap { $0.deviceId == deviceId ? $0 : nil }
?? DeviceAuthStoreFile(version: 1, deviceId: deviceId, tokens: [:])
fallback.tokens[normalizedRole] = entry
self.writeLegacyStore(fallback)
}
return entry
}

View File

@@ -185,6 +185,33 @@ struct DeviceIdentityStoreTests {
}
}
@Test("merges legacy device auth sidecar when SQLite write fails")
func mergesLegacyDeviceAuthSidecarWhenSQLiteWriteFails() throws {
try Self.withTempStateDir { stateDir in
let legacyURL = Self.legacyAuthURL(stateDir: stateDir)
try Self.writeLegacyAuthSidecar(
legacyURL,
deviceId: "device-1",
token: "gateway-token",
scopes: ["read"])
try FileManager.default.createDirectory(
at: Self.databaseURL(stateDir: stateDir),
withIntermediateDirectories: true)
_ = DeviceAuthStore.storeToken(
deviceId: "device-1",
role: "operator",
token: "operator-token",
scopes: ["write"])
let data = try Data(contentsOf: legacyURL)
let root = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
let tokens = try #require(root["tokens"] as? [String: Any])
#expect((tokens["gateway"] as? [String: Any])?["token"] as? String == "gateway-token")
#expect((tokens["operator"] as? [String: Any])?["token"] as? String == "operator-token")
}
}
@Test("drops stale legacy device auth sidecar when storing a different device")
func dropsStaleLegacyDeviceAuthSidecarWhenReplacingDevice() throws {
try Self.withTempStateDir { stateDir in

View File

@@ -157,6 +157,41 @@ describe("SQLite session row backend", () => {
});
});
it("preserves channel-only delivery context without a conversation peer", () => {
const stateDir = createTempDir();
const env = { OPENCLAW_STATE_DIR: stateDir };
upsertSessionEntry({
agentId: "ops",
env,
sessionKey: "webchat:main",
entry: {
sessionId: "webchat-session",
updatedAt: 200,
channel: "webchat",
deliveryContext: {
channel: "webchat",
},
lastChannel: "webchat",
},
});
const loaded = getSessionEntry({
agentId: "ops",
env,
sessionKey: "webchat:main",
});
expect(loaded).toMatchObject({
sessionId: "webchat-session",
channel: "webchat",
deliveryContext: {
channel: "webchat",
},
lastChannel: "webchat",
});
expect(loaded?.lastTo).toBeUndefined();
});
it("stores hot session metadata in canonical session roots", () => {
const stateDir = createTempDir();
const env = { OPENCLAW_STATE_DIR: stateDir };

View File

@@ -13,7 +13,10 @@ import {
runOpenClawAgentWriteTransaction,
} from "../../state/openclaw-agent-db.js";
import { type OpenClawStateDatabaseOptions } from "../../state/openclaw-state-db.js";
import { normalizeDeliveryContext } from "../../utils/delivery-context.shared.js";
import {
normalizeDeliveryContext,
normalizeSessionDeliveryFields,
} from "../../utils/delivery-context.shared.js";
import {
conversationIdentityFromSessionEntry,
type ConversationIdentity,
@@ -142,19 +145,23 @@ function clearCompatibilityRoutingShadow(
}
function projectCompatibilityRoutingShadow(entry: SessionEntry): void {
const deliveryContext = normalizeDeliveryContext(entry.deliveryContext);
if (!deliveryContext?.channel || !deliveryContext.to) {
const normalized = normalizeSessionDeliveryFields(entry);
const deliveryContext = normalized.deliveryContext;
if (!deliveryContext?.channel) {
return;
}
entry.deliveryContext = deliveryContext;
entry.lastChannel = deliveryContext.channel;
entry.lastTo = deliveryContext.to;
entry.lastAccountId = deliveryContext.accountId;
entry.lastThreadId = deliveryContext.threadId;
entry.lastChannel = normalized.lastChannel;
entry.lastTo = normalized.lastTo;
entry.lastAccountId = normalized.lastAccountId;
entry.lastThreadId = normalized.lastThreadId;
}
function projectTypedSessionColumns(row: SessionEntryRow): SessionEntry | null {
const parsed = parseSessionEntry(row);
const parsedDeliveryContext = parsed
? normalizeSessionDeliveryFields(parsed).deliveryContext
: undefined;
const sessionId = optionalString(row.typed_session_id) ?? parsed?.sessionId;
const updatedAt =
typeof row.typed_updated_at === "number" && Number.isFinite(row.typed_updated_at)
@@ -222,6 +229,9 @@ function projectTypedSessionColumns(row: SessionEntryRow): SessionEntry | null {
if (nativeDirectUserId) {
next.nativeDirectUserId = nativeDirectUserId;
}
if (!next.deliveryContext && parsedDeliveryContext?.channel && !parsedDeliveryContext.to) {
next.deliveryContext = parsedDeliveryContext;
}
const modelProvider = optionalString(row.typed_model_provider);
if (modelProvider) {
next.modelProvider = modelProvider;