opencode-subagent-magazine 1.6.1 → 1.7.1

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.
@@ -1,6 +1,23 @@
1
1
  import type { SubEntry } from "../core/types";
2
2
  export type SubEntryPatch = Omit<SubEntry, "startedAt" | "endedAt"> & {
3
3
  startedAt?: number;
4
+ endedAt?: number;
4
5
  };
5
6
  export declare function findSubEntryKey(entries: ReadonlyMap<string, SubEntry>, id: string, sessionId?: string): string | undefined;
6
7
  export declare function upsertSubEntry(entries: ReadonlyMap<string, SubEntry>, partial: SubEntryPatch, now?: number): Map<string, SubEntry>;
8
+ /**
9
+ * 合并同一子代理条目的两个视图,绝不回退状态。
10
+ *
11
+ * 持久化存储由多个 TUI 实例写入,每个写入者都可能
12
+ * 持有过期快照。终态始终优先于 running /
13
+ * cancel_requested,进度计数只增不减,描述性字段
14
+ * 则回退到仍保有它的一侧。
15
+ */
16
+ export declare function mergeSubEntry(base: SubEntry, incoming: SubEntry): SubEntry;
17
+ /**
18
+ * 合并两个条目集合,去重方式与 `upsertSubEntry` 相同
19
+ * (先子会话、后条目 id),冲突用 `mergeSubEntry` 合并。
20
+ * 所有持久化路径都走这里,避免并发实例以整表写入
21
+ * 互相丢弃对方的条目。
22
+ */
23
+ export declare function mergeSubEntries(base: Iterable<SubEntry>, incoming: Iterable<SubEntry>): Map<string, SubEntry>;
@@ -19,7 +19,81 @@ export function upsertSubEntry(entries, partial, now = Date.now()) {
19
19
  ...partial,
20
20
  id: existing?.id ?? partial.id,
21
21
  startedAt: existing?.startedAt || partial.startedAt || now,
22
- endedAt: ended ? (existing?.endedAt || now) : undefined,
22
+ endedAt: ended ? (existing?.endedAt ?? partial.endedAt ?? now) : undefined,
23
23
  });
24
24
  return next;
25
25
  }
26
+ /** running < cancel_requested < 终态(done/error/cancelled)。 */
27
+ function statusRank(status) {
28
+ if (status === "running")
29
+ return 0;
30
+ if (status === "cancel_requested")
31
+ return 1;
32
+ return 2;
33
+ }
34
+ const isTerminalStatus = (status) => statusRank(status) === 2;
35
+ function minDefined(a, b) {
36
+ if (a === undefined)
37
+ return b;
38
+ if (b === undefined)
39
+ return a;
40
+ return Math.min(a, b);
41
+ }
42
+ function maxDefined(a, b) {
43
+ if (a === undefined)
44
+ return b;
45
+ if (b === undefined)
46
+ return a;
47
+ return Math.max(a, b);
48
+ }
49
+ /**
50
+ * 合并同一子代理条目的两个视图,绝不回退状态。
51
+ *
52
+ * 持久化存储由多个 TUI 实例写入,每个写入者都可能
53
+ * 持有过期快照。终态始终优先于 running /
54
+ * cancel_requested,进度计数只增不减,描述性字段
55
+ * 则回退到仍保有它的一侧。
56
+ */
57
+ export function mergeSubEntry(base, incoming) {
58
+ const successor = statusRank(incoming.status) > statusRank(base.status) ? incoming : base;
59
+ const status = successor.status;
60
+ const ended = isTerminalStatus(status);
61
+ return {
62
+ ...base,
63
+ ...incoming,
64
+ id: base.id,
65
+ status,
66
+ sessionId: incoming.sessionId ?? base.sessionId,
67
+ startedAt: minDefined(base.startedAt, incoming.startedAt) ?? base.startedAt,
68
+ endedAt: ended ? (base.endedAt ?? incoming.endedAt ?? Date.now()) : undefined,
69
+ tokens: maxDefined(base.tokens, incoming.tokens),
70
+ cost: maxDefined(base.cost, incoming.cost),
71
+ model: incoming.model ?? base.model,
72
+ todoTotal: maxDefined(base.todoTotal, incoming.todoTotal),
73
+ todoDone: maxDefined(base.todoDone, incoming.todoDone),
74
+ error: incoming.error ?? base.error,
75
+ cancelRequestedAt: minDefined(base.cancelRequestedAt, incoming.cancelRequestedAt),
76
+ abortAccepted: base.abortAccepted || incoming.abortAccepted || undefined,
77
+ cancelReason: incoming.cancelReason ?? base.cancelReason,
78
+ };
79
+ }
80
+ /**
81
+ * 合并两个条目集合,去重方式与 `upsertSubEntry` 相同
82
+ * (先子会话、后条目 id),冲突用 `mergeSubEntry` 合并。
83
+ * 所有持久化路径都走这里,避免并发实例以整表写入
84
+ * 互相丢弃对方的条目。
85
+ */
86
+ export function mergeSubEntries(base, incoming) {
87
+ const out = new Map();
88
+ for (const entry of base)
89
+ out.set(entry.id, entry);
90
+ for (const entry of incoming) {
91
+ const key = findSubEntryKey(out, entry.id, entry.sessionId);
92
+ if (!key) {
93
+ out.set(entry.id, entry);
94
+ continue;
95
+ }
96
+ out.set(key, mergeSubEntry(out.get(key), entry));
97
+ }
98
+ return out;
99
+ }
@@ -8,6 +8,20 @@ export interface SessionLike {
8
8
  agent?: string;
9
9
  cost?: number;
10
10
  }
11
+ /** Spawned child session summary (V2 reads it from the local database). */
12
+ export interface ChildSessionLike {
13
+ id: string;
14
+ agent?: string;
15
+ title?: string;
16
+ model?: string;
17
+ cost?: number;
18
+ tokens?: number;
19
+ timeCreated?: number;
20
+ timeIdle?: number;
21
+ idleOutcome?: string;
22
+ /** Still running: last message is newer than time_idle (resume keeps the old value). */
23
+ active?: boolean;
24
+ }
11
25
  export interface SessionStatusLike {
12
26
  type: string;
13
27
  }
@@ -45,6 +59,16 @@ export interface PanelApi {
45
59
  messages(sid: string): unknown[] | undefined;
46
60
  /** Raw parts of a message (scan). */
47
61
  part(messageID: string): unknown[] | undefined;
62
+ /** Optional host-side fallback: map a subagent tool call to its child session
63
+ * when live events/history lack the link (V2 reads the local database). */
64
+ resolveChild?(input: {
65
+ parentId: string;
66
+ callId?: string;
67
+ agent?: string;
68
+ startedAt?: number;
69
+ }): string | undefined;
70
+ /** Optional host-side listing of spawned child sessions. */
71
+ listChildren?(parentId: string): ChildSessionLike[] | undefined;
48
72
  };
49
73
  event: {
50
74
  on(type: PanelEventType, cb: (e: PanelEvent) => void): () => void;