tmux-watch 2026.2.4 → 2026.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/manager.ts +139 -50
package/package.json
CHANGED
package/src/manager.ts
CHANGED
|
@@ -53,22 +53,23 @@ type WatchEntry = {
|
|
|
53
53
|
runtime: WatchRuntime;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
type ResolvedTarget = {
|
|
56
|
+
export type ResolvedTarget = {
|
|
57
57
|
channel: string;
|
|
58
58
|
target: string;
|
|
59
59
|
accountId?: string;
|
|
60
60
|
threadId?: string | number;
|
|
61
61
|
label?: string;
|
|
62
|
-
source: "targets" | "last";
|
|
62
|
+
source: "targets" | "last" | "last-fallback";
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
type SessionEntryLike = {
|
|
65
|
+
export type SessionEntryLike = {
|
|
66
66
|
deliveryContext?: {
|
|
67
67
|
channel?: string;
|
|
68
68
|
to?: string;
|
|
69
69
|
accountId?: string;
|
|
70
70
|
threadId?: string | number;
|
|
71
71
|
};
|
|
72
|
+
updatedAt?: number;
|
|
72
73
|
lastChannel?: string;
|
|
73
74
|
lastTo?: string;
|
|
74
75
|
lastAccountId?: string;
|
|
@@ -83,6 +84,7 @@ type MinimalConfig = {
|
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
const STATE_VERSION = 1;
|
|
87
|
+
const INTERNAL_LAST_CHANNELS = new Set(["webchat", "tui"]);
|
|
86
88
|
|
|
87
89
|
export class TmuxWatchManager {
|
|
88
90
|
private readonly api: OpenClawPluginApi;
|
|
@@ -455,60 +457,26 @@ export class TmuxWatchManager {
|
|
|
455
457
|
}
|
|
456
458
|
|
|
457
459
|
if (includeLast) {
|
|
458
|
-
const
|
|
459
|
-
if (
|
|
460
|
-
targets.push(
|
|
461
|
-
...last,
|
|
462
|
-
source: "last",
|
|
463
|
-
});
|
|
460
|
+
const lastTargets = await this.resolveLastTargets(sessionKey);
|
|
461
|
+
if (lastTargets.length > 0) {
|
|
462
|
+
targets.push(...lastTargets);
|
|
464
463
|
}
|
|
465
464
|
}
|
|
466
465
|
|
|
467
466
|
return dedupeTargets(targets);
|
|
468
467
|
}
|
|
469
468
|
|
|
470
|
-
private async
|
|
471
|
-
const
|
|
472
|
-
if (!
|
|
473
|
-
return
|
|
474
|
-
}
|
|
475
|
-
const delivery = entry.deliveryContext ?? {};
|
|
476
|
-
const channel =
|
|
477
|
-
typeof delivery.channel === "string"
|
|
478
|
-
? delivery.channel.trim()
|
|
479
|
-
: typeof entry.lastChannel === "string"
|
|
480
|
-
? entry.lastChannel.trim()
|
|
481
|
-
: typeof entry.channel === "string"
|
|
482
|
-
? entry.channel.trim()
|
|
483
|
-
: undefined;
|
|
484
|
-
const target =
|
|
485
|
-
typeof delivery.to === "string"
|
|
486
|
-
? delivery.to.trim()
|
|
487
|
-
: typeof entry.lastTo === "string"
|
|
488
|
-
? entry.lastTo.trim()
|
|
489
|
-
: undefined;
|
|
490
|
-
if (!channel || !target) {
|
|
491
|
-
return null;
|
|
469
|
+
private async resolveLastTargets(sessionKey: string): Promise<ResolvedTarget[]> {
|
|
470
|
+
const store = await this.readSessionStore(sessionKey);
|
|
471
|
+
if (!store) {
|
|
472
|
+
return [];
|
|
492
473
|
}
|
|
493
|
-
|
|
494
|
-
typeof delivery.accountId === "string"
|
|
495
|
-
? delivery.accountId.trim()
|
|
496
|
-
: typeof entry.lastAccountId === "string"
|
|
497
|
-
? entry.lastAccountId.trim()
|
|
498
|
-
: undefined;
|
|
499
|
-
const threadId =
|
|
500
|
-
delivery.threadId ?? entry.lastThreadId ?? entry.origin?.threadId ?? undefined;
|
|
501
|
-
return {
|
|
502
|
-
channel,
|
|
503
|
-
target,
|
|
504
|
-
accountId: accountId || undefined,
|
|
505
|
-
threadId: parseThreadId(threadId),
|
|
506
|
-
label: undefined,
|
|
507
|
-
source: "last",
|
|
508
|
-
};
|
|
474
|
+
return resolveLastTargetsFromStore({ store, sessionKey });
|
|
509
475
|
}
|
|
510
476
|
|
|
511
|
-
private async
|
|
477
|
+
private async readSessionStore(
|
|
478
|
+
sessionKey: string,
|
|
479
|
+
): Promise<Record<string, SessionEntryLike> | null> {
|
|
512
480
|
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
|
513
481
|
const storePath = this.api.runtime.channel.session.resolveStorePath(
|
|
514
482
|
this.api.config.session?.store,
|
|
@@ -520,8 +488,10 @@ export class TmuxWatchManager {
|
|
|
520
488
|
if (!store || typeof store !== "object") {
|
|
521
489
|
return null;
|
|
522
490
|
}
|
|
523
|
-
return store
|
|
524
|
-
} catch {
|
|
491
|
+
return store;
|
|
492
|
+
} catch (err) {
|
|
493
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
494
|
+
this.api.logger.warn(`[tmux-watch] session store read failed: ${message}`);
|
|
525
495
|
return null;
|
|
526
496
|
}
|
|
527
497
|
}
|
|
@@ -769,6 +739,125 @@ function normalizeSessionKey(input: string | undefined, cfg?: MinimalConfig) {
|
|
|
769
739
|
return `agent:${normalizeAgentId(agentId)}:${lowered}`;
|
|
770
740
|
}
|
|
771
741
|
|
|
742
|
+
type TargetSnapshot = {
|
|
743
|
+
channel: string;
|
|
744
|
+
target: string;
|
|
745
|
+
accountId?: string;
|
|
746
|
+
threadId?: string | number;
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
function isInternalLastChannel(channel: string | undefined): boolean {
|
|
750
|
+
if (!channel) {
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
return INTERNAL_LAST_CHANNELS.has(channel.trim().toLowerCase());
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function extractTargetSnapshot(entry: SessionEntryLike): TargetSnapshot | null {
|
|
757
|
+
const delivery = entry.deliveryContext ?? {};
|
|
758
|
+
const channel =
|
|
759
|
+
typeof delivery.channel === "string"
|
|
760
|
+
? delivery.channel.trim()
|
|
761
|
+
: typeof entry.lastChannel === "string"
|
|
762
|
+
? entry.lastChannel.trim()
|
|
763
|
+
: typeof entry.channel === "string"
|
|
764
|
+
? entry.channel.trim()
|
|
765
|
+
: undefined;
|
|
766
|
+
const target =
|
|
767
|
+
typeof delivery.to === "string"
|
|
768
|
+
? delivery.to.trim()
|
|
769
|
+
: typeof entry.lastTo === "string"
|
|
770
|
+
? entry.lastTo.trim()
|
|
771
|
+
: undefined;
|
|
772
|
+
if (!channel || !target) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
const accountId =
|
|
776
|
+
typeof delivery.accountId === "string"
|
|
777
|
+
? delivery.accountId.trim()
|
|
778
|
+
: typeof entry.lastAccountId === "string"
|
|
779
|
+
? entry.lastAccountId.trim()
|
|
780
|
+
: undefined;
|
|
781
|
+
const threadId =
|
|
782
|
+
delivery.threadId ?? entry.lastThreadId ?? entry.origin?.threadId ?? undefined;
|
|
783
|
+
return {
|
|
784
|
+
channel,
|
|
785
|
+
target,
|
|
786
|
+
accountId: accountId || undefined,
|
|
787
|
+
threadId,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function snapshotKey(snapshot: TargetSnapshot): string {
|
|
792
|
+
return [snapshot.channel, snapshot.target, snapshot.accountId ?? "", snapshot.threadId ?? ""].join(
|
|
793
|
+
"|",
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function findLatestExternalTarget(
|
|
798
|
+
store: Record<string, SessionEntryLike>,
|
|
799
|
+
exclude: TargetSnapshot,
|
|
800
|
+
): TargetSnapshot | null {
|
|
801
|
+
const excludeKey = snapshotKey(exclude);
|
|
802
|
+
let best: { updatedAt: number; target: TargetSnapshot } | null = null;
|
|
803
|
+
for (const entry of Object.values(store)) {
|
|
804
|
+
const snapshot = extractTargetSnapshot(entry);
|
|
805
|
+
if (!snapshot) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (isInternalLastChannel(snapshot.channel)) {
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (snapshotKey(snapshot) === excludeKey) {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const updatedAt = typeof entry.updatedAt === "number" ? entry.updatedAt : 0;
|
|
815
|
+
if (!best || updatedAt > best.updatedAt) {
|
|
816
|
+
best = { updatedAt, target: snapshot };
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return best?.target ?? null;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function toResolvedTarget(
|
|
823
|
+
snapshot: TargetSnapshot,
|
|
824
|
+
source: ResolvedTarget["source"],
|
|
825
|
+
): ResolvedTarget {
|
|
826
|
+
return {
|
|
827
|
+
channel: snapshot.channel,
|
|
828
|
+
target: snapshot.target,
|
|
829
|
+
accountId: snapshot.accountId,
|
|
830
|
+
threadId: parseThreadId(snapshot.threadId),
|
|
831
|
+
label: undefined,
|
|
832
|
+
source,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export function resolveLastTargetsFromStore(params: {
|
|
837
|
+
store: Record<string, SessionEntryLike>;
|
|
838
|
+
sessionKey: string;
|
|
839
|
+
}): ResolvedTarget[] {
|
|
840
|
+
const entry =
|
|
841
|
+
params.store[params.sessionKey] ??
|
|
842
|
+
params.store[params.sessionKey.toLowerCase()] ??
|
|
843
|
+
null;
|
|
844
|
+
if (!entry) {
|
|
845
|
+
return [];
|
|
846
|
+
}
|
|
847
|
+
const primary = extractTargetSnapshot(entry);
|
|
848
|
+
if (!primary) {
|
|
849
|
+
return [];
|
|
850
|
+
}
|
|
851
|
+
const targets: ResolvedTarget[] = [toResolvedTarget(primary, "last")];
|
|
852
|
+
if (isInternalLastChannel(primary.channel)) {
|
|
853
|
+
const fallback = findLatestExternalTarget(params.store, primary);
|
|
854
|
+
if (fallback) {
|
|
855
|
+
targets.push(toResolvedTarget(fallback, "last-fallback"));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return targets;
|
|
859
|
+
}
|
|
860
|
+
|
|
772
861
|
function hashOutput(output: string): string {
|
|
773
862
|
return createHash("sha256").update(output).digest("hex");
|
|
774
863
|
}
|