Skip to content

Commit be0cd4f

Browse files
authored
Merge 55f4652 into c20a055
2 parents c20a055 + 55f4652 commit be0cd4f

15 files changed

Lines changed: 18280 additions & 151 deletions

File tree

config/knip.config.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,7 @@ const config = {
157157
],
158158
},
159159
ui: {
160-
entry: [
161-
"index.html!",
162-
"src/main.ts!",
163-
"vite.config.ts!",
164-
"vitest*.ts!",
165-
],
160+
entry: ["index.html!", "src/main.ts!", "vite.config.ts!", "vitest*.ts!"],
166161
project: ["src/**/*.{ts,tsx}!"],
167162
},
168163
"packages/sdk": {

extensions/diffs/assets/viewer-runtime.js

Lines changed: 17189 additions & 52 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/matrix/src/session-route.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ describe("resolveMatrixOutboundSessionRoute", () => {
278278
const route = resolveMatrixOutboundSessionRoute({
279279
cfg: {},
280280
agentId: "main",
281-
target: "room:!Ops:Example.Org",
281+
target: "room:!ops:example.org",
282282
currentSessionKey: "agent:main:matrix:channel:!ops:example.org:thread:$RootEvent:Example.Org",
283283
});
284284

scripts/crabbox-wrapper.mjs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,7 +1491,11 @@ function remoteAwsMacosJsBootstrap({ packageManager = false } = {}) {
14911491
}
14921492

14931493
function scopedAwsMacosEnvCommand(commandArgs) {
1494-
if (commandArgs.length <= 1 || shellWordBasename(commandArgs[0]) !== "env" || commandArgs[0].includes("/")) {
1494+
if (
1495+
commandArgs.length <= 1 ||
1496+
shellWordBasename(commandArgs[0]) !== "env" ||
1497+
commandArgs[0].includes("/")
1498+
) {
14951499
return null;
14961500
}
14971501

@@ -1501,7 +1505,10 @@ function scopedAwsMacosEnvCommand(commandArgs) {
15011505
}
15021506

15031507
const targetEntrypoint = shellWordBasename(targetWords[0]);
1504-
if (!jsRuntimeEntrypoints.has(targetEntrypoint) && !awsMacosCorepackEntrypoints.has(targetEntrypoint)) {
1508+
if (
1509+
!jsRuntimeEntrypoints.has(targetEntrypoint) &&
1510+
!awsMacosCorepackEntrypoints.has(targetEntrypoint)
1511+
) {
15051512
return null;
15061513
}
15071514

@@ -1517,7 +1524,8 @@ function injectRemoteAwsMacosJsBootstrap(commandArgs, providerName) {
15171524
const directScopedEnvCommand = hasOption(commandArgs, "--shell")
15181525
? null
15191526
: scopedAwsMacosEnvCommand(runArgs);
1520-
const runtimeEntrypoint = directScopedEnvCommand?.runtimeEntrypoint || commandRuntimeEntrypoint(runArgs);
1527+
const runtimeEntrypoint =
1528+
directScopedEnvCommand?.runtimeEntrypoint || commandRuntimeEntrypoint(runArgs);
15211529
if (!isAwsMacosRemoteTarget(commandArgs, providerName) || !runtimeEntrypoint) {
15221530
return commandArgs;
15231531
}
@@ -1535,7 +1543,8 @@ function injectRemoteAwsMacosJsBootstrap(commandArgs, providerName) {
15351543
? remoteCommand[0]
15361544
: shellJoin(remoteCommand));
15371545
const shellCommand = `${remoteAwsMacosJsBootstrap({
1538-
packageManager: directScopedEnvCommand?.packageManager || commandNeedsAwsMacosPackageManager(runArgs),
1546+
packageManager:
1547+
directScopedEnvCommand?.packageManager || commandNeedsAwsMacosPackageManager(runArgs),
15391548
})} && { ${originalShellCommand}\n}`;
15401549

15411550
if (!hasOption(normalizedArgs, "--shell")) {

src/commands/doctor-state-migrations.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,47 @@ describe("doctor legacy state migrations", () => {
874874
expect(store["agent:main:slack:channel:C123"]).toBeUndefined();
875875
});
876876

877+
it("preserves Matrix room and thread casing during canonicalization", async () => {
878+
const root = await makeTempRoot();
879+
const cfg: OpenClawConfig = {};
880+
const targetDir = path.join(root, "agents", "main", "sessions");
881+
writeJson5(path.join(targetDir, "sessions.json"), {
882+
"agent:main:Matrix:Channel:!Mixed:Example.Org:Thread:$EventABC": {
883+
sessionId: "matrix",
884+
updatedAt: 10,
885+
},
886+
});
887+
888+
const store = await runAndReadSessionsStore({
889+
root,
890+
cfg,
891+
targetDir,
892+
now: () => 123,
893+
});
894+
expect(store["agent:main:matrix:channel:!Mixed:Example.Org:thread:$EventABC"]?.sessionId).toBe(
895+
"matrix",
896+
);
897+
expect(store["agent:main:matrix:channel:!mixed:example.org:thread:$eventabc"]).toBeUndefined();
898+
});
899+
900+
it("preserves unscoped legacy Matrix room casing when scoping to an agent", async () => {
901+
const root = await makeTempRoot();
902+
const cfg: OpenClawConfig = {};
903+
const targetDir = path.join(root, "agents", "main", "sessions");
904+
writeJson5(path.join(targetDir, "sessions.json"), {
905+
"Matrix:Channel:!Mixed:Example.Org": { sessionId: "matrix", updatedAt: 10 },
906+
});
907+
908+
const store = await runAndReadSessionsStore({
909+
root,
910+
cfg,
911+
targetDir,
912+
now: () => 123,
913+
});
914+
expect(store["agent:main:matrix:channel:!Mixed:Example.Org"]?.sessionId).toBe("matrix");
915+
expect(store["agent:main:matrix:channel:!mixed:example.org"]).toBeUndefined();
916+
});
917+
877918
it("auto-migrates when only target sessions contain legacy keys", async () => {
878919
const { root, cfg } = await makeRootWithEmptyCfg();
879920
const targetDir = path.join(root, "agents", "main", "sessions");

src/config/sessions/delivery-info.test.ts

Lines changed: 200 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,9 @@ describe("extractDeliveryInfo", () => {
277277
});
278278

279279
it("derives delivery info from stored last route metadata when deliveryContext is missing", () => {
280-
const sessionKey = "agent:main:matrix:channel:!lowercased:example.org";
281-
storeState.store[sessionKey] = {
280+
const sessionKey = "agent:main:matrix:channel:!MixedCase:example.org";
281+
const legacyKey = "agent:main:matrix:channel:!mixedcase:example.org";
282+
storeState.store[legacyKey] = {
282283
sessionId: "session-1",
283284
updatedAt: Date.now(),
284285
origin: {
@@ -361,34 +362,69 @@ describe("extractDeliveryInfo", () => {
361362
});
362363
});
363364

364-
it("prefers an older routable normalized alias over a fresher non-routable alias", () => {
365-
const queriedKey = "agent:main:matrix:channel:!MiXeDCase:Example.Org";
366-
const routableAlias = "agent:main:matrix:channel:!MixedCase:Example.Org";
367-
const canonicalKey = "agent:main:matrix:channel:!mixedcase:example.org";
365+
it("prefers an older routable normalized alias over a fresher non-routable alias for non-opaque keys", () => {
366+
const queriedKey = "agent:main:telegram:group:MiXeDCase";
367+
const routableAlias = "agent:main:telegram:group:MixedCase";
368+
const canonicalKey = "agent:main:telegram:group:mixedcase";
368369
storeState.store[canonicalKey] = {
369370
sessionId: "fresh-normalized-session",
370371
updatedAt: Date.now(),
371372
origin: {
372-
provider: "matrix",
373+
provider: "telegram",
373374
},
374375
};
375376
storeState.store[routableAlias] = {
376377
sessionId: "older-routable-session",
377378
updatedAt: Date.now() - 1_000,
378379
deliveryContext: {
379-
channel: "matrix",
380-
to: "room:!MixedCase:Example.Org",
381-
accountId: "matrix-account",
380+
channel: "telegram",
381+
to: "telegram:MixedCase",
382+
accountId: "telegram-account",
382383
},
383384
};
384385

385386
const result = extractDeliveryInfo(queriedKey);
386387

387388
expect(result).toEqual({
388389
deliveryContext: {
389-
channel: "matrix",
390-
to: "room:!MixedCase:Example.Org",
391-
accountId: "matrix-account",
390+
channel: "telegram",
391+
to: "telegram:MixedCase",
392+
accountId: "telegram-account",
393+
},
394+
threadId: undefined,
395+
});
396+
});
397+
398+
it("keeps freshest routable alias ordering for non-opaque keys", () => {
399+
const queriedKey = "agent:main:telegram:group:MiXeDCase";
400+
const canonicalKey = "agent:main:telegram:group:mixedcase";
401+
const routableAlias = "agent:main:telegram:group:MixedCase";
402+
storeState.store[canonicalKey] = {
403+
sessionId: "older-canonical-session",
404+
updatedAt: Date.now() - 1_000,
405+
deliveryContext: {
406+
channel: "telegram",
407+
to: "telegram:old-route",
408+
accountId: "telegram-account",
409+
},
410+
};
411+
storeState.store[routableAlias] = {
412+
sessionId: "fresh-routable-session",
413+
updatedAt: Date.now(),
414+
deliveryContext: {
415+
channel: "telegram",
416+
to: "telegram:fresh-route",
417+
accountId: "telegram-account",
418+
},
419+
};
420+
421+
const result = extractDeliveryInfo(queriedKey);
422+
423+
expect(result).toEqual({
424+
deliveryContext: {
425+
channel: "telegram",
426+
to: "telegram:fresh-route",
427+
accountId: "telegram-account",
392428
},
393429
threadId: undefined,
394430
});
@@ -416,26 +452,57 @@ describe("extractDeliveryInfo", () => {
416452
});
417453
});
418454

419-
it("prefers the freshest routable alias even when the normalized key is already routable", () => {
420-
const queriedKey = "agent:main:matrix:channel:!MiXeDCase:Example.Org";
421-
const canonicalKey = "agent:main:matrix:channel:!mixedcase:example.org";
422-
const fresherAlias = "agent:main:matrix:channel:!MixedCase:Example.Org";
423-
storeState.store[canonicalKey] = {
424-
sessionId: "older-canonical-session",
455+
it("prefers the exact mixed-case Matrix entry over a fresher folded legacy alias", () => {
456+
// Matrix room IDs are case-sensitive (openclaw#75670): the exact mixed-case
457+
// session is canonical and must win over a stale lowercased legacy alias even
458+
// when the alias is fresher. (Previously these collapsed to one lowercased key
459+
// and freshest won — that collapse was the bug.)
460+
const queriedKey = "agent:main:matrix:channel:!MixedCase:Example.Org";
461+
const legacyFoldedKey = "agent:main:matrix:channel:!mixedcase:example.org";
462+
storeState.store[queriedKey] = {
463+
sessionId: "exact-mixedcase-session",
425464
updatedAt: Date.now() - 1_000,
465+
deliveryContext: {
466+
channel: "matrix",
467+
to: "room:!MixedCase:Example.Org",
468+
accountId: "matrix-account",
469+
},
470+
};
471+
storeState.store[legacyFoldedKey] = {
472+
sessionId: "fresher-legacy-folded-session",
473+
updatedAt: Date.now(),
426474
deliveryContext: {
427475
channel: "matrix",
428476
to: "room:!mixedcase:example.org",
429477
accountId: "matrix-account",
430478
},
431479
};
432-
storeState.store[fresherAlias] = {
433-
sessionId: "fresh-alias-session",
480+
481+
const result = extractDeliveryInfo(queriedKey);
482+
483+
expect(result).toEqual({
484+
deliveryContext: {
485+
channel: "matrix",
486+
to: "room:!MixedCase:Example.Org",
487+
accountId: "matrix-account",
488+
},
489+
threadId: undefined,
490+
});
491+
});
492+
493+
it("finds Matrix thread entries with a legacy lowercased room and preserved event id", () => {
494+
const queriedKey =
495+
"agent:main:matrix:channel:!MixedCase:Example.Org:thread:$RootEvent:Example.Org";
496+
const legacyThreadKey =
497+
"agent:main:matrix:channel:!mixedcase:example.org:thread:$RootEvent:Example.Org";
498+
storeState.store[legacyThreadKey] = {
499+
sessionId: "legacy-thread-session",
434500
updatedAt: Date.now(),
435501
deliveryContext: {
436502
channel: "matrix",
437503
to: "room:!MixedCase:Example.Org",
438504
accountId: "matrix-account",
505+
threadId: "$RootEvent:Example.Org",
439506
},
440507
};
441508

@@ -446,11 +513,123 @@ describe("extractDeliveryInfo", () => {
446513
channel: "matrix",
447514
to: "room:!MixedCase:Example.Org",
448515
accountId: "matrix-account",
516+
threadId: "$RootEvent:Example.Org",
449517
},
518+
threadId: "$RootEvent:Example.Org",
519+
});
520+
});
521+
522+
it("does not return a case-distinct lowercase Matrix sibling when the mixed-case key has no exact entry", () => {
523+
const queriedKey = "agent:main:matrix:channel:!MixedCase:Example.Org";
524+
const lowercaseSiblingKey = "agent:main:matrix:channel:!mixedcase:example.org";
525+
storeState.store[lowercaseSiblingKey] = buildEntry({
526+
channel: "matrix",
527+
to: "room:!mixedcase:example.org",
528+
accountId: "matrix-account",
529+
});
530+
531+
const result = extractDeliveryInfo(queriedKey);
532+
533+
expect(result).toEqual({
534+
deliveryContext: undefined,
535+
threadId: undefined,
536+
});
537+
});
538+
539+
it("does not return a mixed-case Matrix sibling for a lowercase room query", () => {
540+
const queriedKey = "agent:main:matrix:channel:!mixedcase:example.org";
541+
const mixedSiblingKey = "agent:main:matrix:channel:!MixedCase:Example.Org";
542+
storeState.store[mixedSiblingKey] = buildEntry({
543+
channel: "matrix",
544+
to: "room:!MixedCase:Example.Org",
545+
accountId: "matrix-account",
546+
});
547+
548+
const result = extractDeliveryInfo(queriedKey);
549+
550+
expect(result).toEqual({
551+
deliveryContext: undefined,
450552
threadId: undefined,
451553
});
452554
});
453555

556+
it("does not return an exact lowercase Matrix key with mixed-case delivery metadata", () => {
557+
const queriedKey = "agent:main:matrix:channel:!mixedcase:example.org";
558+
storeState.store[queriedKey] = buildEntry({
559+
channel: "matrix",
560+
to: "room:!MixedCase:Example.Org",
561+
accountId: "matrix-account",
562+
});
563+
564+
const result = extractDeliveryInfo(queriedKey);
565+
566+
expect(result).toEqual({
567+
deliveryContext: undefined,
568+
threadId: undefined,
569+
});
570+
});
571+
572+
it("returns a confirmed lowercased Matrix legacy artifact for a mixed-case key", () => {
573+
const queriedKey = "agent:main:matrix:channel:!MixedCase:Example.Org";
574+
const legacyArtifactKey = "agent:main:matrix:channel:!mixedcase:example.org";
575+
storeState.store[legacyArtifactKey] = buildEntry({
576+
channel: "matrix",
577+
to: "room:!MixedCase:Example.Org",
578+
accountId: "matrix-account",
579+
});
580+
581+
const result = extractDeliveryInfo(queriedKey);
582+
583+
expect(result).toEqual({
584+
deliveryContext: {
585+
channel: "matrix",
586+
to: "room:!MixedCase:Example.Org",
587+
accountId: "matrix-account",
588+
},
589+
threadId: undefined,
590+
});
591+
});
592+
593+
it("returns a confirmed lowercased Matrix room-alias artifact", () => {
594+
const queriedKey = "agent:main:matrix:channel:#MixedAlias:Example.Org";
595+
const legacyArtifactKey = "agent:main:matrix:channel:#mixedalias:example.org";
596+
storeState.store[legacyArtifactKey] = buildEntry({
597+
channel: "matrix",
598+
to: "room:#MixedAlias:Example.Org",
599+
accountId: "matrix-account",
600+
});
601+
602+
const result = extractDeliveryInfo(queriedKey);
603+
604+
expect(result).toEqual({
605+
deliveryContext: {
606+
channel: "matrix",
607+
to: "room:#MixedAlias:Example.Org",
608+
accountId: "matrix-account",
609+
},
610+
threadId: undefined,
611+
});
612+
});
613+
614+
it("does not return a folded Matrix thread artifact when the stored thread id differs by case", () => {
615+
const queriedKey = "agent:main:matrix:channel:!MixedCase:Example.Org:thread:$ThreadRootAbC";
616+
const foldedThreadKey =
617+
"agent:main:matrix:channel:!mixedcase:example.org:thread:$threadrootabc";
618+
storeState.store[foldedThreadKey] = buildEntry({
619+
channel: "matrix",
620+
to: "room:!MixedCase:Example.Org",
621+
accountId: "matrix-account",
622+
threadId: "$threadrootabc",
623+
});
624+
625+
const result = extractDeliveryInfo(queriedKey);
626+
627+
expect(result).toEqual({
628+
deliveryContext: undefined,
629+
threadId: "$ThreadRootAbC",
630+
});
631+
});
632+
454633
it("falls back to the base session when a thread entry only has partial route metadata", () => {
455634
const baseKey = "agent:main:matrix:channel:!MixedCase:example.org";
456635
const threadKey = `${baseKey}:thread:$thread-event`;

0 commit comments

Comments
 (0)