ework-daemon 0.1.2 → 0.1.3
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/opencode.ts +85 -25
package/package.json
CHANGED
package/src/opencode.ts
CHANGED
|
@@ -14,6 +14,51 @@ interface TrackerRegistry {
|
|
|
14
14
|
get(type: string): IssueTracker | undefined;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Extract the target name of an @mention from comment text.
|
|
19
|
+
*
|
|
20
|
+
* Strips fenced + inline code first (terminal pastes with "user@host" / "git@repo"),
|
|
21
|
+
* then matches `@name`. Rejects two phantom-mention shapes that previously spawned
|
|
22
|
+
* stray agent sessions (ework-daemon#2):
|
|
23
|
+
* - scoped package refs (`@types/node`, `@babel/core` — the trailing `/` means an
|
|
24
|
+
* npm path, not a person);
|
|
25
|
+
* - version-like `@<digits>` (`@123`).
|
|
26
|
+
*
|
|
27
|
+
* Exported so tests can pin the exact accept/reject behavior (regression coverage).
|
|
28
|
+
*/
|
|
29
|
+
export function detectMention(text: string): string | null {
|
|
30
|
+
const stripped = text
|
|
31
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
32
|
+
.replace(/`[^`\n]*`/g, "");
|
|
33
|
+
const re = /(?:^|\s)@([\w\u4e00-\u9fff]+)/g;
|
|
34
|
+
let m: RegExpExecArray | null;
|
|
35
|
+
while ((m = re.exec(stripped)) !== null) {
|
|
36
|
+
const name = m[1];
|
|
37
|
+
if (!name) continue;
|
|
38
|
+
if (/^\d+$/.test(name)) continue; // @<digits> → version ref, skip
|
|
39
|
+
if (stripped[re.lastIndex] === "/") continue; // @scope/pkg → scoped package, skip
|
|
40
|
+
return name;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pick the most recently active session from a list: the one whose process last
|
|
47
|
+
* started (`startedAt`), falling back to creation time when a session has never
|
|
48
|
+
* run yet. Returns `undefined` for an empty list.
|
|
49
|
+
*
|
|
50
|
+
* Used by the no-mention dispatch path to route a comment to a single session
|
|
51
|
+
* (the "last AI") instead of broadcasting to all of them.
|
|
52
|
+
*/
|
|
53
|
+
export function pickLastActive(sessions: OpSession[]): OpSession | undefined {
|
|
54
|
+
if (sessions.length === 0) return undefined;
|
|
55
|
+
return sessions.reduce((a, b) => {
|
|
56
|
+
const aT = a.startedAt ?? a.createdAt.getTime();
|
|
57
|
+
const bT = b.startedAt ?? b.createdAt.getTime();
|
|
58
|
+
return bT > aT ? b : a;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
17
62
|
// ─── Engine ───
|
|
18
63
|
|
|
19
64
|
export class Engine {
|
|
@@ -107,13 +152,21 @@ export class Engine {
|
|
|
107
152
|
}
|
|
108
153
|
|
|
109
154
|
private extractMentionName(text: string): string | null {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
155
|
+
return detectMention(text);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Whitelist gate: default only the bot itself is a valid @mention target.
|
|
160
|
+
* Unknown names (misread from plain text — `@types/node`, etc.) don't spawn a
|
|
161
|
+
* new session; they fall back to broadcast. To run multiple named agents, set
|
|
162
|
+
* `DAEMON_ALLOWED_AGENTS=ework,tester,...`.
|
|
163
|
+
*/
|
|
164
|
+
private isAllowedAgent(name: string): boolean {
|
|
165
|
+
const env = process.env.DAEMON_ALLOWED_AGENTS;
|
|
166
|
+
const allowed = env && env.trim()
|
|
167
|
+
? env.split(",").map(s => s.trim()).filter(Boolean)
|
|
168
|
+
: [this.cfg.bot.username];
|
|
169
|
+
return allowed.includes(name);
|
|
117
170
|
}
|
|
118
171
|
|
|
119
172
|
private parseDirCommand(text: string): string | null {
|
|
@@ -261,7 +314,14 @@ export class Engine {
|
|
|
261
314
|
}
|
|
262
315
|
|
|
263
316
|
const dirPath = this.parseDirCommand(comment.body);
|
|
264
|
-
const
|
|
317
|
+
const rawMention = this.extractMentionName(comment.body);
|
|
318
|
+
// Gate extracted @mentions through the agent whitelist. An unknown name
|
|
319
|
+
// (e.g. `@types` misread from `@types/node`) is treated as no mention and
|
|
320
|
+
// falls through to broadcast instead of spawning a phantom session (ework-daemon#2).
|
|
321
|
+
const mentionName = rawMention && this.isAllowedAgent(rawMention) ? rawMention : null;
|
|
322
|
+
if (rawMention && !mentionName) {
|
|
323
|
+
log.info(`engine: @${rawMention} is not an allowed agent — routing to last session instead of spawning`);
|
|
324
|
+
}
|
|
265
325
|
|
|
266
326
|
if (mentionName) {
|
|
267
327
|
// @mention → targeted delivery
|
|
@@ -304,27 +364,27 @@ export class Engine {
|
|
|
304
364
|
this.enqueueOrRun(session, issue, prompt, comment.id, model);
|
|
305
365
|
}
|
|
306
366
|
} else {
|
|
307
|
-
// No @mention →
|
|
367
|
+
// No valid @mention → forward to the most recently active session only.
|
|
368
|
+
// Broadcasting to all sessions makes multiple AIs race on the same request;
|
|
369
|
+
// routing to the last-active lets the user continue without re-@mentioning,
|
|
370
|
+
// while @mention switches to a different AI.
|
|
308
371
|
const sessions = this.store.getSessionsForIssue(issue.id);
|
|
309
372
|
if (sessions.length === 0) return;
|
|
310
373
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const workdir = this.resolveWorkdir(session, issue);
|
|
317
|
-
const instructions = tracker.getTrackerInstructions(ref);
|
|
318
|
-
const prompt = this.buildForwardPrompt(
|
|
319
|
-
session.name, this.handleLargeContent(workdir, comment.body, `comment-${comment.id}.txt`),
|
|
320
|
-
comment.author, issueData.title, workdir, instructions
|
|
321
|
-
);
|
|
322
|
-
this.enqueueOrRun(session, issue, prompt, comment.id, model);
|
|
374
|
+
const session = pickLastActive(sessions);
|
|
375
|
+
if (!session) return;
|
|
376
|
+
if (dirPath) {
|
|
377
|
+
this.store.updateSession(session.id, { workdir: dirPath });
|
|
378
|
+
session.workdir = dirPath;
|
|
323
379
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
380
|
+
const workdir = this.resolveWorkdir(session, issue);
|
|
381
|
+
const instructions = tracker.getTrackerInstructions(ref);
|
|
382
|
+
const prompt = this.buildForwardPrompt(
|
|
383
|
+
session.name, this.handleLargeContent(workdir, comment.body, `comment-${comment.id}.txt`),
|
|
384
|
+
comment.author, issueData.title, workdir, instructions
|
|
385
|
+
);
|
|
386
|
+
await tracker.createComment(ref, `[system] ✓ Message forwarded to **${session.name}**${this.running.has(this.sessionKey(session, issue)) ? " (running)" : ""}.\n> session: \`${session.id}\` | workdir: \`${workdir}\``);
|
|
387
|
+
this.enqueueOrRun(session, issue, prompt, comment.id, model);
|
|
328
388
|
}
|
|
329
389
|
} finally {
|
|
330
390
|
if (comment.id) this.processingComments.delete(comment.id);
|