botmux 2.51.0 → 2.51.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.
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Phase 0 keystone — `botmux dispatch` pure core.
3
+ *
4
+ * The orchestrator (主 bot) splits a big project into sub-projects and assigns
5
+ * each to a small group of bots (often a coder + a reviewer). To open a
6
+ * sub-project it seeds a fresh Lark thread and @-mentions the assigned bots so
7
+ * each spawns its own thread-scoped session (botmux's existing one-thread-one-
8
+ * session routing; bot→bot @ inside a thread is ungated — see
9
+ * event-dispatcher.ts decideRouting + the chat-scope-only foreign-bot gate).
10
+ *
11
+ * This module is the pure, I/O-free core: parse the `--bot` specs and build the
12
+ * two messages (a top-level seed = the thread root, and the threaded kickoff
13
+ * that @-mentions the bots with their roles + the brief). The CLI shell
14
+ * (cli.ts) performs the actual sendMessage + replyMessage.
15
+ */
16
+ export interface DispatchBot {
17
+ /** open_id as seen by the orchestrator's app (from <available_bots>). */
18
+ openId: string;
19
+ /** Display name, for readable @ rendering / division-of-labor lines. */
20
+ name?: string;
21
+ /** Short role label, e.g. "coder" / "reviewer". */
22
+ role?: string;
23
+ }
24
+ export type PostNode = {
25
+ tag: 'text';
26
+ text: string;
27
+ } | {
28
+ tag: 'at';
29
+ user_id: string;
30
+ };
31
+ export type PostParagraph = PostNode[];
32
+ export interface DispatchMessages {
33
+ /** Plain-text seed (the thread root) — the human-visible "this sub-project exists" header. */
34
+ seedText: string;
35
+ /** Lark 'post' content (paragraphs of nodes) for the threaded kickoff. */
36
+ threadContent: PostParagraph[];
37
+ /** open_ids @-mentioned in the kickoff — the bots that will be triggered. */
38
+ mentionedOpenIds: string[];
39
+ }
40
+ /**
41
+ * Parse a `--bot` spec `openId[:name[:role]]` into a {@link DispatchBot}.
42
+ * Mirrors the `--mention "open_id:Display Name"` convention, with an optional
43
+ * trailing role segment.
44
+ */
45
+ export declare function parseDispatchBotSpec(raw: string): DispatchBot;
46
+ /**
47
+ * Build the seed + threaded-kickoff messages for one sub-project dispatch.
48
+ * Throws when there is no title or no bot to dispatch to.
49
+ */
50
+ export declare function buildDispatchMessages(input: {
51
+ title: string;
52
+ brief: string;
53
+ bots: DispatchBot[];
54
+ }): DispatchMessages;
55
+ /**
56
+ * Build the "repo prime" message: a `/repo <path>` command @-mentioning the
57
+ * target bots, sent as a **plain text message** — exactly like a human typing
58
+ * "@bot /repo <path>". Sent as the first message into a freshly-seeded thread,
59
+ * it makes each sub-bot's daemon resolve the working dir and spawn its CLI
60
+ * **idle** (no repo-selection card, no manual "直接开始" click) — i.e. standby.
61
+ *
62
+ * Why text (not a structured `post`): the receiving daemon parses a text
63
+ * message's @ via `resolveMentions` (the same clean path a human @ goes
64
+ * through), whereas a `post`'s at/text nodes go through `renderPostNode`, which
65
+ * drops the `/repo` argument in the live event — see the dispatch debugging
66
+ * notes. `/repo` is an existing botmux command, so this needs no receiving-side
67
+ * change. The `<at>` tags come first so that, once the receiving daemon strips
68
+ * leading mentions, it sees `/repo <path>` as the command.
69
+ */
70
+ export declare function buildRepoPrimeText(input: {
71
+ path: string;
72
+ bots: DispatchBot[];
73
+ }): {
74
+ text: string;
75
+ mentionedOpenIds: string[];
76
+ };
77
+ /**
78
+ * Build the report-back message a dispatched sub-bot sends to its orchestrator.
79
+ *
80
+ * In 多话题协作模式 a sub-bot must NOT @ the orchestrator in its own sub-topic —
81
+ * that thread has no orchestrator session, so the orchestrator's daemon would
82
+ * spawn a fresh, context-less one. Instead `botmux report` sends this content
83
+ * **into the orchestrator's own thread** (recorded by `botmux dispatch`),
84
+ * @-mentioning the orchestrator so its existing, context-rich session is the one
85
+ * that wakes up. This is the pure content builder; cli.ts resolves the coords
86
+ * and performs the reply.
87
+ *
88
+ * The @ stays on the first line so the mention renders next to the headline;
89
+ * any further lines become their own paragraphs (Lark 'post' shape).
90
+ */
91
+ export declare function buildReportContent(input: {
92
+ orchOpenId: string;
93
+ content: string;
94
+ }): PostParagraph[];
95
+ /**
96
+ * Footgun guard for the orchestrator→sub-bot direction. A dispatched sub-bot's
97
+ * session lives **inside its sub-topic**, so @-mentioning it from the main chat
98
+ * (e.g. `botmux send --mention <sub-bot>`) doesn't reach that session — it
99
+ * spawns a fresh, context-less one in the chat (the mirror of the report-back
100
+ * problem). To talk to a sub-bot the orchestrator must send INTO its sub-topic
101
+ * (`botmux dispatch --into <seed> --bot <sub-bot>`).
102
+ *
103
+ * Given the dispatch registry (seed → {orchChatId, bots}) and the set of seeds
104
+ * whose sub-topic is still active, return the sub-topic seed to redirect to when
105
+ * `mentionOpenId` is a sub-bot dispatched into an active topic of `chatId`;
106
+ * otherwise null. Only fires for live topics so stale entries don't block sends.
107
+ */
108
+ export declare function findSubBotTopic(input: {
109
+ mentionOpenId: string;
110
+ chatId: string;
111
+ registry: Record<string, {
112
+ orchChatId?: string;
113
+ bots?: string[];
114
+ }>;
115
+ activeSeeds: Set<string>;
116
+ }): string | null;
117
+ /**
118
+ * The footgun check shared by `botmux send`'s explicit-mention guard AND its
119
+ * prose `@Name` auto-injection: returns the sub-topic seed if `mentionOpenId` is
120
+ * a dispatched sub-bot in an active topic that is NOT reachable in the current
121
+ * conversation (so @-ing it here would spawn a context-less session), else null.
122
+ *
123
+ * The bot I'm replying to (`quoteTargetSenderOpenId`) is reachable right here, so
124
+ * it's never treated as off-topic — that's the boundary that stops the guard from
125
+ * blocking a normal reply to a bot conversing with me. Callers block (explicit
126
+ * --mention) or drop (prose injection) on a non-null result, and skip the whole
127
+ * check under `--anyway`.
128
+ */
129
+ export declare function offTopicSubBotTopic(input: {
130
+ mentionOpenId: string;
131
+ quoteTargetSenderOpenId?: string;
132
+ chatId: string;
133
+ registry: Record<string, {
134
+ orchChatId?: string;
135
+ bots?: string[];
136
+ }>;
137
+ activeSeeds: Set<string>;
138
+ }): string | null;
139
+ /**
140
+ * Decide which names of a candidate bot are eligible for prose `@Name`
141
+ * auto-mention injection in `botmux send`.
142
+ *
143
+ * The fan-out bug: a bot writes "@Codex review" in its message; the injector
144
+ * matches each bot by **botName OR cliId**, and the cliId ("codex") is a shared
145
+ * *type* alias — so "@Codex" matches every codex-type bot (Codex分身, Codex二号分身,
146
+ * ttadk(codex), aiden x codex…) and pulls them ALL into the topic, each spawning
147
+ * a session and replying.
148
+ *
149
+ * Fix: the unique `botName` is always eligible (so first-time @-invites still
150
+ * work), but the type-generic `cliId` alias is eligible **only when this bot is
151
+ * actually in the current conversation** (`convoBotAppIds` = bots with an active
152
+ * session in this thread / chat). So "@Codex" resolves to the one codex bot
153
+ * collaborating here, not every same-type bot. `selfAliases` (the sender's own
154
+ * name/cliId) are always excluded.
155
+ */
156
+ export declare function eligibleAutoMentionAliases(input: {
157
+ botName?: string;
158
+ cliId?: string;
159
+ larkAppId?: string;
160
+ selfAliases: Set<string>;
161
+ convoBotAppIds: Set<string>;
162
+ }): string[];
163
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../src/core/dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,QAAQ,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,GAAG,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AACtF,MAAM,MAAM,aAAa,GAAG,QAAQ,EAAE,CAAC;AAEvC,MAAM,WAAW,gBAAgB;IAC/B,8FAA8F;IAC9F,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,aAAa,EAAE,aAAa,EAAE,CAAC;IAC/B,6EAA6E;IAC7E,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAY7D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,WAAW,EAAE,CAAC;CACrB,GAAG,gBAAgB,CAwCnB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,WAAW,EAAE,CAAC;CACrB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAAE,CAO/C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,aAAa,EAAE,CAclB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACnE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC1B,GAAG,MAAM,GAAG,IAAI,CAShB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,aAAa,EAAE,MAAM,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACnE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC1B,GAAG,MAAM,GAAG,IAAI,CAQhB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC7B,GAAG,MAAM,EAAE,CAaX"}
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Phase 0 keystone — `botmux dispatch` pure core.
3
+ *
4
+ * The orchestrator (主 bot) splits a big project into sub-projects and assigns
5
+ * each to a small group of bots (often a coder + a reviewer). To open a
6
+ * sub-project it seeds a fresh Lark thread and @-mentions the assigned bots so
7
+ * each spawns its own thread-scoped session (botmux's existing one-thread-one-
8
+ * session routing; bot→bot @ inside a thread is ungated — see
9
+ * event-dispatcher.ts decideRouting + the chat-scope-only foreign-bot gate).
10
+ *
11
+ * This module is the pure, I/O-free core: parse the `--bot` specs and build the
12
+ * two messages (a top-level seed = the thread root, and the threaded kickoff
13
+ * that @-mentions the bots with their roles + the brief). The CLI shell
14
+ * (cli.ts) performs the actual sendMessage + replyMessage.
15
+ */
16
+ /**
17
+ * Parse a `--bot` spec `openId[:name[:role]]` into a {@link DispatchBot}.
18
+ * Mirrors the `--mention "open_id:Display Name"` convention, with an optional
19
+ * trailing role segment.
20
+ */
21
+ export function parseDispatchBotSpec(raw) {
22
+ const trimmed = raw.trim();
23
+ if (!trimmed)
24
+ throw new Error('empty --bot spec');
25
+ const parts = trimmed.split(':');
26
+ const openId = parts[0]?.trim();
27
+ if (!openId)
28
+ throw new Error(`invalid --bot spec: ${JSON.stringify(raw)}`);
29
+ const bot = { openId };
30
+ const name = parts[1]?.trim();
31
+ const role = parts[2]?.trim();
32
+ if (name)
33
+ bot.name = name;
34
+ if (role)
35
+ bot.role = role;
36
+ return bot;
37
+ }
38
+ /**
39
+ * Build the seed + threaded-kickoff messages for one sub-project dispatch.
40
+ * Throws when there is no title or no bot to dispatch to.
41
+ */
42
+ export function buildDispatchMessages(input) {
43
+ const title = input.title.trim();
44
+ if (!title)
45
+ throw new Error('dispatch requires a title');
46
+ if (input.bots.length === 0)
47
+ throw new Error('dispatch requires at least one bot');
48
+ const seedText = `📋 子项目:${title}`;
49
+ const content = [];
50
+ // Line 1: @ every assigned bot (role suffix inline) so each gets triggered.
51
+ const atLine = [];
52
+ input.bots.forEach((b, i) => {
53
+ if (i > 0)
54
+ atLine.push({ tag: 'text', text: ' ' });
55
+ atLine.push({ tag: 'at', user_id: b.openId });
56
+ if (b.role)
57
+ atLine.push({ tag: 'text', text: `(${b.role})` });
58
+ });
59
+ content.push(atLine);
60
+ content.push([{ tag: 'text', text: '' }]);
61
+ // The brief, one paragraph per line.
62
+ for (const line of input.brief.split('\n')) {
63
+ content.push([{ tag: 'text', text: line }]);
64
+ }
65
+ // Division of labour, when any role was given.
66
+ if (input.bots.some(b => b.role)) {
67
+ content.push([{ tag: 'text', text: '' }]);
68
+ content.push([{ tag: 'text', text: '分工:' }]);
69
+ for (const b of input.bots) {
70
+ const label = b.name || b.openId;
71
+ content.push([{ tag: 'text', text: `· ${label}:${b.role ?? '执行'}` }]);
72
+ }
73
+ }
74
+ return {
75
+ seedText,
76
+ threadContent: content,
77
+ mentionedOpenIds: input.bots.map(b => b.openId),
78
+ };
79
+ }
80
+ /**
81
+ * Build the "repo prime" message: a `/repo <path>` command @-mentioning the
82
+ * target bots, sent as a **plain text message** — exactly like a human typing
83
+ * "@bot /repo <path>". Sent as the first message into a freshly-seeded thread,
84
+ * it makes each sub-bot's daemon resolve the working dir and spawn its CLI
85
+ * **idle** (no repo-selection card, no manual "直接开始" click) — i.e. standby.
86
+ *
87
+ * Why text (not a structured `post`): the receiving daemon parses a text
88
+ * message's @ via `resolveMentions` (the same clean path a human @ goes
89
+ * through), whereas a `post`'s at/text nodes go through `renderPostNode`, which
90
+ * drops the `/repo` argument in the live event — see the dispatch debugging
91
+ * notes. `/repo` is an existing botmux command, so this needs no receiving-side
92
+ * change. The `<at>` tags come first so that, once the receiving daemon strips
93
+ * leading mentions, it sees `/repo <path>` as the command.
94
+ */
95
+ export function buildRepoPrimeText(input) {
96
+ const path = input.path.trim();
97
+ if (!path)
98
+ throw new Error('repo prime requires a path');
99
+ if (input.bots.length === 0)
100
+ throw new Error('repo prime requires at least one bot');
101
+ const ats = input.bots.map(b => `<at user_id="${b.openId}"></at>`).join(' ');
102
+ return { text: `${ats} /repo ${path}`, mentionedOpenIds: input.bots.map(b => b.openId) };
103
+ }
104
+ /**
105
+ * Build the report-back message a dispatched sub-bot sends to its orchestrator.
106
+ *
107
+ * In 多话题协作模式 a sub-bot must NOT @ the orchestrator in its own sub-topic —
108
+ * that thread has no orchestrator session, so the orchestrator's daemon would
109
+ * spawn a fresh, context-less one. Instead `botmux report` sends this content
110
+ * **into the orchestrator's own thread** (recorded by `botmux dispatch`),
111
+ * @-mentioning the orchestrator so its existing, context-rich session is the one
112
+ * that wakes up. This is the pure content builder; cli.ts resolves the coords
113
+ * and performs the reply.
114
+ *
115
+ * The @ stays on the first line so the mention renders next to the headline;
116
+ * any further lines become their own paragraphs (Lark 'post' shape).
117
+ */
118
+ export function buildReportContent(input) {
119
+ const openId = input.orchOpenId.trim();
120
+ if (!openId)
121
+ throw new Error('report requires the orchestrator open_id');
122
+ const text = input.content.trim();
123
+ if (!text)
124
+ throw new Error('report requires content');
125
+ const lines = text.split('\n');
126
+ const paras = [
127
+ [{ tag: 'at', user_id: openId }, { tag: 'text', text: ' ' }, { tag: 'text', text: lines[0] }],
128
+ ];
129
+ for (let i = 1; i < lines.length; i++) {
130
+ paras.push([{ tag: 'text', text: lines[i] }]);
131
+ }
132
+ return paras;
133
+ }
134
+ /**
135
+ * Footgun guard for the orchestrator→sub-bot direction. A dispatched sub-bot's
136
+ * session lives **inside its sub-topic**, so @-mentioning it from the main chat
137
+ * (e.g. `botmux send --mention <sub-bot>`) doesn't reach that session — it
138
+ * spawns a fresh, context-less one in the chat (the mirror of the report-back
139
+ * problem). To talk to a sub-bot the orchestrator must send INTO its sub-topic
140
+ * (`botmux dispatch --into <seed> --bot <sub-bot>`).
141
+ *
142
+ * Given the dispatch registry (seed → {orchChatId, bots}) and the set of seeds
143
+ * whose sub-topic is still active, return the sub-topic seed to redirect to when
144
+ * `mentionOpenId` is a sub-bot dispatched into an active topic of `chatId`;
145
+ * otherwise null. Only fires for live topics so stale entries don't block sends.
146
+ */
147
+ export function findSubBotTopic(input) {
148
+ // Newest-first: a bot dispatched into several topics over time is, right now,
149
+ // working in the most-recent one — point there, not at a stale earlier topic.
150
+ for (const [seed, entry] of Object.entries(input.registry).reverse()) {
151
+ if (entry.orchChatId && entry.orchChatId !== input.chatId)
152
+ continue;
153
+ if (!input.activeSeeds.has(seed))
154
+ continue;
155
+ if ((entry.bots ?? []).includes(input.mentionOpenId))
156
+ return seed;
157
+ }
158
+ return null;
159
+ }
160
+ /**
161
+ * The footgun check shared by `botmux send`'s explicit-mention guard AND its
162
+ * prose `@Name` auto-injection: returns the sub-topic seed if `mentionOpenId` is
163
+ * a dispatched sub-bot in an active topic that is NOT reachable in the current
164
+ * conversation (so @-ing it here would spawn a context-less session), else null.
165
+ *
166
+ * The bot I'm replying to (`quoteTargetSenderOpenId`) is reachable right here, so
167
+ * it's never treated as off-topic — that's the boundary that stops the guard from
168
+ * blocking a normal reply to a bot conversing with me. Callers block (explicit
169
+ * --mention) or drop (prose injection) on a non-null result, and skip the whole
170
+ * check under `--anyway`.
171
+ */
172
+ export function offTopicSubBotTopic(input) {
173
+ if (!input.mentionOpenId || input.mentionOpenId === input.quoteTargetSenderOpenId)
174
+ return null;
175
+ return findSubBotTopic({
176
+ mentionOpenId: input.mentionOpenId,
177
+ chatId: input.chatId,
178
+ registry: input.registry,
179
+ activeSeeds: input.activeSeeds,
180
+ });
181
+ }
182
+ /**
183
+ * Decide which names of a candidate bot are eligible for prose `@Name`
184
+ * auto-mention injection in `botmux send`.
185
+ *
186
+ * The fan-out bug: a bot writes "@Codex review" in its message; the injector
187
+ * matches each bot by **botName OR cliId**, and the cliId ("codex") is a shared
188
+ * *type* alias — so "@Codex" matches every codex-type bot (Codex分身, Codex二号分身,
189
+ * ttadk(codex), aiden x codex…) and pulls them ALL into the topic, each spawning
190
+ * a session and replying.
191
+ *
192
+ * Fix: the unique `botName` is always eligible (so first-time @-invites still
193
+ * work), but the type-generic `cliId` alias is eligible **only when this bot is
194
+ * actually in the current conversation** (`convoBotAppIds` = bots with an active
195
+ * session in this thread / chat). So "@Codex" resolves to the one codex bot
196
+ * collaborating here, not every same-type bot. `selfAliases` (the sender's own
197
+ * name/cliId) are always excluded.
198
+ */
199
+ export function eligibleAutoMentionAliases(input) {
200
+ const out = [];
201
+ const { botName, cliId, larkAppId, selfAliases, convoBotAppIds } = input;
202
+ if (botName && !selfAliases.has(botName.toLowerCase()))
203
+ out.push(botName);
204
+ if (cliId &&
205
+ !selfAliases.has(cliId.toLowerCase()) &&
206
+ !!larkAppId &&
207
+ convoBotAppIds.has(larkAppId)) {
208
+ out.push(cliId);
209
+ }
210
+ return out;
211
+ }
212
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../src/core/dispatch.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AA0BA,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAmErD,OAAO,EAAE,oBAAoB,EAA6B,MAAM,uBAAuB,CAAC;AACxF,OAAO,KAAK,EAAE,sBAAsB,EAAiB,MAAM,wBAAwB,CAAC;AAyWpF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,sBAAsB,GAAG,oBAAoB,CA0C5G;AAijED,wBAAsB,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyTlE"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AA0BA,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAmErD,OAAO,EAAE,oBAAoB,EAA6B,MAAM,uBAAuB,CAAC;AACxF,OAAO,KAAK,EAAE,sBAAsB,EAAiB,MAAM,wBAAwB,CAAC;AAyWpF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,sBAAsB,GAAG,oBAAoB,CA0C5G;AA6lED,wBAAsB,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyTlE"}
package/dist/daemon.js CHANGED
@@ -1925,9 +1925,50 @@ async function handleThreadReply(data, ctx) {
1925
1925
  sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
1926
1926
  return;
1927
1927
  }
1928
+ // First message of a fresh thread carrying a session-needing daemon command
1929
+ // — e.g. another bot dispatched `/repo <path>` into a brand-new thread.
1930
+ // Without a session, handleCommand gets ds=undefined and `/repo` (and other
1931
+ // session commands) fall through to the repo-select card. Create the session
1932
+ // first, mirroring handleNewTopic's first-message `/repo` pendingRepo setup.
1933
+ // Session-less commands (/group /g) don't need one.
1934
+ if (!existingDs && threadChatId && !SESSIONLESS_DAEMON_COMMANDS.has(cmd)) {
1935
+ const session = sessionStore.createSession(threadChatId, anchor, cmdContent.substring(0, 50), ctxChatType);
1936
+ const now = Date.now();
1937
+ session.larkAppId = larkAppId;
1938
+ session.ownerOpenId = threadSenderOpenId;
1939
+ session.creatorOpenId = threadSenderOpenId; // stable creator (= dispatch orchestrator for /repo prime) — see Session.creatorOpenId
1940
+ session.ownerUnionId = data?.sender?.sender_id?.union_id;
1941
+ session.lastCallerOpenId = threadSenderOpenId;
1942
+ session.lastMessageAt = new Date(now).toISOString();
1943
+ session.scope = scope;
1944
+ let cmdPending;
1945
+ if (cmd === '/repo') {
1946
+ const { pinnedWorkingDir } = await resolvePinnedWorkingDir({ scope, anchor, chatId: threadChatId, chatType: ctxChatType, larkAppId });
1947
+ if (pinnedWorkingDir)
1948
+ session.workingDir = pinnedWorkingDir;
1949
+ cmdPending = { pendingRepo: true, pendingPrompt: '', workingDir: pinnedWorkingDir };
1950
+ }
1951
+ sessionStore.updateSession(session);
1952
+ activeSessions.set(sessionKey(anchor, larkAppId), {
1953
+ session,
1954
+ worker: null,
1955
+ workerPort: null,
1956
+ workerToken: null,
1957
+ larkAppId,
1958
+ chatId: threadChatId,
1959
+ chatType: ctxChatType,
1960
+ scope,
1961
+ spawnedAt: Date.parse(session.createdAt) || now,
1962
+ cliVersion: cliVersionCache.get(getBot(larkAppId).config.cliId)?.version ?? 'unknown',
1963
+ lastMessageAt: now,
1964
+ hasHistory: false,
1965
+ ownerOpenId: threadSenderOpenId,
1966
+ ...cmdPending,
1967
+ });
1968
+ }
1928
1969
  // Pass mention-stripped content so /command argument parsing works.
1929
1970
  // chatId lets session-less handlers (e.g. /group) reach the chat roster.
1930
- handleCommand(cmd, anchor, { ...parsed, content: commandContent, chatId: threadChatId }, commandDeps, larkAppId);
1971
+ await handleCommand(cmd, anchor, { ...parsed, content: commandContent, chatId: threadChatId }, commandDeps, larkAppId);
1931
1972
  return;
1932
1973
  }
1933
1974
  }
@@ -2039,6 +2080,10 @@ async function handleThreadReply(data, ctx) {
2039
2080
  const ownerUnionId = isForeignBot ? undefined : senderUId;
2040
2081
  session.larkAppId = larkAppId;
2041
2082
  session.ownerOpenId = ownerOpenId;
2083
+ // creatorOpenId is the raw creating sender — set even for foreign-bot
2084
+ // sessions (unlike ownerOpenId, nulled above) so `botmux report` can find the
2085
+ // dispatch orchestrator on a no-`/repo` kickoff auto-create. See Session.creatorOpenId.
2086
+ session.creatorOpenId = senderOId;
2042
2087
  session.ownerUnionId = ownerUnionId;
2043
2088
  session.lastCallerOpenId = senderOId;
2044
2089
  session.quoteTargetId = parsed.messageId;