awb-agent-manager 1.0.0
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/LICENSE +21 -0
- package/README.md +246 -0
- package/dist/lib/agent-lockfile.d.ts +37 -0
- package/dist/lib/agent-lockfile.js +205 -0
- package/dist/lib/agent-lockfile.js.map +1 -0
- package/dist/lib/agent-manager-commands.d.ts +34 -0
- package/dist/lib/agent-manager-commands.js +531 -0
- package/dist/lib/agent-manager-commands.js.map +1 -0
- package/dist/lib/base-session-manager.d.ts +127 -0
- package/dist/lib/base-session-manager.js +667 -0
- package/dist/lib/base-session-manager.js.map +1 -0
- package/dist/lib/chat-attachment-prep.d.ts +35 -0
- package/dist/lib/chat-attachment-prep.js +180 -0
- package/dist/lib/chat-attachment-prep.js.map +1 -0
- package/dist/lib/chat-session-manager.d.ts +29 -0
- package/dist/lib/chat-session-manager.js +400 -0
- package/dist/lib/chat-session-manager.js.map +1 -0
- package/dist/lib/circuit-breaker.d.ts +34 -0
- package/dist/lib/circuit-breaker.js +146 -0
- package/dist/lib/circuit-breaker.js.map +1 -0
- package/dist/lib/cli-adapters/antigravity.d.ts +15 -0
- package/dist/lib/cli-adapters/antigravity.js +114 -0
- package/dist/lib/cli-adapters/antigravity.js.map +1 -0
- package/dist/lib/cli-adapters/base.d.ts +115 -0
- package/dist/lib/cli-adapters/base.js +111 -0
- package/dist/lib/cli-adapters/base.js.map +1 -0
- package/dist/lib/cli-adapters/claude.d.ts +19 -0
- package/dist/lib/cli-adapters/claude.js +233 -0
- package/dist/lib/cli-adapters/claude.js.map +1 -0
- package/dist/lib/cli-adapters/codex.d.ts +14 -0
- package/dist/lib/cli-adapters/codex.js +152 -0
- package/dist/lib/cli-adapters/codex.js.map +1 -0
- package/dist/lib/cli-adapters/deepseek.d.ts +15 -0
- package/dist/lib/cli-adapters/deepseek.js +70 -0
- package/dist/lib/cli-adapters/deepseek.js.map +1 -0
- package/dist/lib/cli-adapters/index.d.ts +8 -0
- package/dist/lib/cli-adapters/index.js +27 -0
- package/dist/lib/cli-adapters/index.js.map +1 -0
- package/dist/lib/cli-adapters/model-introspect.d.ts +3 -0
- package/dist/lib/cli-adapters/model-introspect.js +65 -0
- package/dist/lib/cli-adapters/model-introspect.js.map +1 -0
- package/dist/lib/cli-error-signatures.d.ts +9 -0
- package/dist/lib/cli-error-signatures.js +22 -0
- package/dist/lib/cli-error-signatures.js.map +1 -0
- package/dist/lib/cli-output-summary.d.ts +2 -0
- package/dist/lib/cli-output-summary.js +104 -0
- package/dist/lib/cli-output-summary.js.map +1 -0
- package/dist/lib/cli-resolver.d.ts +2 -0
- package/dist/lib/cli-resolver.js +177 -0
- package/dist/lib/cli-resolver.js.map +1 -0
- package/dist/lib/config.d.ts +13 -0
- package/dist/lib/config.js +124 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/constants.d.ts +36 -0
- package/dist/lib/constants.js +51 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/environment-provisioner.d.ts +31 -0
- package/dist/lib/environment-provisioner.js +182 -0
- package/dist/lib/environment-provisioner.js.map +1 -0
- package/dist/lib/error-log-uploader.d.ts +19 -0
- package/dist/lib/error-log-uploader.js +138 -0
- package/dist/lib/error-log-uploader.js.map +1 -0
- package/dist/lib/event-dispatcher.d.ts +149 -0
- package/dist/lib/event-dispatcher.js +799 -0
- package/dist/lib/event-dispatcher.js.map +1 -0
- package/dist/lib/event-log-recorder.d.ts +11 -0
- package/dist/lib/event-log-recorder.js +77 -0
- package/dist/lib/event-log-recorder.js.map +1 -0
- package/dist/lib/event-stream.d.ts +26 -0
- package/dist/lib/event-stream.js +177 -0
- package/dist/lib/event-stream.js.map +1 -0
- package/dist/lib/fs-browser.d.ts +30 -0
- package/dist/lib/fs-browser.js +287 -0
- package/dist/lib/fs-browser.js.map +1 -0
- package/dist/lib/host-mcp/logs.d.ts +19 -0
- package/dist/lib/host-mcp/logs.js +102 -0
- package/dist/lib/host-mcp/logs.js.map +1 -0
- package/dist/lib/host-mcp/platform.d.ts +24 -0
- package/dist/lib/host-mcp/platform.js +133 -0
- package/dist/lib/host-mcp/platform.js.map +1 -0
- package/dist/lib/host-mcp/screens.d.ts +22 -0
- package/dist/lib/host-mcp/screens.js +172 -0
- package/dist/lib/host-mcp/screens.js.map +1 -0
- package/dist/lib/host-mcp/server.d.ts +3 -0
- package/dist/lib/host-mcp/server.js +233 -0
- package/dist/lib/host-mcp/server.js.map +1 -0
- package/dist/lib/host-mcp/tools.d.ts +172 -0
- package/dist/lib/host-mcp/tools.js +1241 -0
- package/dist/lib/host-mcp/tools.js.map +1 -0
- package/dist/lib/instance-heartbeat.d.ts +55 -0
- package/dist/lib/instance-heartbeat.js +118 -0
- package/dist/lib/instance-heartbeat.js.map +1 -0
- package/dist/lib/legacy-import.d.ts +17 -0
- package/dist/lib/legacy-import.js +70 -0
- package/dist/lib/legacy-import.js.map +1 -0
- package/dist/lib/logging.d.ts +3 -0
- package/dist/lib/logging.js +65 -0
- package/dist/lib/logging.js.map +1 -0
- package/dist/lib/managed-agent-context.d.ts +25 -0
- package/dist/lib/managed-agent-context.js +33 -0
- package/dist/lib/managed-agent-context.js.map +1 -0
- package/dist/lib/managed-agent-store.d.ts +34 -0
- package/dist/lib/managed-agent-store.js +160 -0
- package/dist/lib/managed-agent-store.js.map +1 -0
- package/dist/lib/managed-agents.d.ts +24 -0
- package/dist/lib/managed-agents.js +87 -0
- package/dist/lib/managed-agents.js.map +1 -0
- package/dist/lib/mcp-client.d.ts +8 -0
- package/dist/lib/mcp-client.js +122 -0
- package/dist/lib/mcp-client.js.map +1 -0
- package/dist/lib/orphan-cleanup.d.ts +6 -0
- package/dist/lib/orphan-cleanup.js +108 -0
- package/dist/lib/orphan-cleanup.js.map +1 -0
- package/dist/lib/presence-heartbeat.d.ts +8 -0
- package/dist/lib/presence-heartbeat.js +61 -0
- package/dist/lib/presence-heartbeat.js.map +1 -0
- package/dist/lib/prompts.d.ts +29 -0
- package/dist/lib/prompts.js +208 -0
- package/dist/lib/prompts.js.map +1 -0
- package/dist/lib/rest.d.ts +61 -0
- package/dist/lib/rest.js +307 -0
- package/dist/lib/rest.js.map +1 -0
- package/dist/lib/run-provisioner.d.ts +21 -0
- package/dist/lib/run-provisioner.js +115 -0
- package/dist/lib/run-provisioner.js.map +1 -0
- package/dist/lib/self-path.d.ts +7 -0
- package/dist/lib/self-path.js +29 -0
- package/dist/lib/self-path.js.map +1 -0
- package/dist/lib/self-update.d.ts +46 -0
- package/dist/lib/self-update.js +482 -0
- package/dist/lib/self-update.js.map +1 -0
- package/dist/lib/service-install.d.ts +23 -0
- package/dist/lib/service-install.js +673 -0
- package/dist/lib/service-install.js.map +1 -0
- package/dist/lib/setup.d.ts +16 -0
- package/dist/lib/setup.js +157 -0
- package/dist/lib/setup.js.map +1 -0
- package/dist/lib/subagent-manager.d.ts +84 -0
- package/dist/lib/subagent-manager.js +793 -0
- package/dist/lib/subagent-manager.js.map +1 -0
- package/dist/lib/subagent-monitor.d.ts +43 -0
- package/dist/lib/subagent-monitor.js +229 -0
- package/dist/lib/subagent-monitor.js.map +1 -0
- package/dist/lib/ticket-session-manager.d.ts +23 -0
- package/dist/lib/ticket-session-manager.js +471 -0
- package/dist/lib/ticket-session-manager.js.map +1 -0
- package/dist/lib/worktree-manager.d.ts +39 -0
- package/dist/lib/worktree-manager.js +226 -0
- package/dist/lib/worktree-manager.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +670 -0
- package/dist/main.js.map +1 -0
- package/dist/package.json +57 -0
- package/package.json +57 -0
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { log } from './logging.js';
|
|
3
|
+
import { loadAgentInfo } from './config.js';
|
|
4
|
+
import { fetchTicketContext, fetchChatRoomHistory, postFsResponse, postChatRoomMessage, } from './rest.js';
|
|
5
|
+
import { recordEvent } from './event-log-recorder.js';
|
|
6
|
+
import { MANAGED_AGENTS_DIR } from './constants.js';
|
|
7
|
+
import { prepareChatAttachments } from './chat-attachment-prep.js';
|
|
8
|
+
import { createAdapter, ADAPTER_CAPABILITIES } from './cli-adapters/index.js';
|
|
9
|
+
import { parseRunProvision, provisionRunWorkspace } from './run-provisioner.js';
|
|
10
|
+
import { fireAndForgetTool } from './mcp-client.js';
|
|
11
|
+
import { mentionTriggerId } from './subagent-manager.js';
|
|
12
|
+
export function parseHarnessConfig(raw) {
|
|
13
|
+
let obj = raw;
|
|
14
|
+
if (typeof obj === 'string') {
|
|
15
|
+
if (!obj.trim())
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
obj = JSON.parse(obj);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
|
25
|
+
return null;
|
|
26
|
+
const out = {};
|
|
27
|
+
if (typeof obj.system_prompt_append === 'string' && obj.system_prompt_append.trim()) {
|
|
28
|
+
out.system_prompt_append = obj.system_prompt_append;
|
|
29
|
+
}
|
|
30
|
+
for (const key of ['allowed_tools', 'disallowed_tools']) {
|
|
31
|
+
if (Array.isArray(obj[key])) {
|
|
32
|
+
const list = obj[key].filter((t) => typeof t === 'string' && t.trim());
|
|
33
|
+
if (list.length > 0)
|
|
34
|
+
out[key] = list;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const key of ['model', 'permission_mode']) {
|
|
38
|
+
if (typeof obj[key] === 'string' && obj[key].trim())
|
|
39
|
+
out[key] = obj[key].trim();
|
|
40
|
+
}
|
|
41
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
42
|
+
}
|
|
43
|
+
const EFFORT_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
44
|
+
export function parseEffortPreset(raw) {
|
|
45
|
+
let obj = raw;
|
|
46
|
+
if (typeof obj === 'string') {
|
|
47
|
+
if (!obj.trim())
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
obj = JSON.parse(obj);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
|
57
|
+
return null;
|
|
58
|
+
if (typeof obj.id !== 'string' || !obj.id.trim())
|
|
59
|
+
return null;
|
|
60
|
+
const out = { id: obj.id.trim() };
|
|
61
|
+
if (typeof obj.label === 'string' && obj.label.trim())
|
|
62
|
+
out.label = obj.label;
|
|
63
|
+
if (obj.claude && typeof obj.claude === 'object' && !Array.isArray(obj.claude)) {
|
|
64
|
+
const c = {};
|
|
65
|
+
if (typeof obj.claude.model === 'string' && obj.claude.model.trim())
|
|
66
|
+
c.model = obj.claude.model.trim();
|
|
67
|
+
if (typeof obj.claude.effort === 'string' && EFFORT_LEVELS.has(obj.claude.effort)) {
|
|
68
|
+
c.effort = obj.claude.effort;
|
|
69
|
+
}
|
|
70
|
+
if (typeof obj.claude.ultracode === 'boolean')
|
|
71
|
+
c.ultracode = obj.claude.ultracode;
|
|
72
|
+
if (Object.keys(c).length > 0)
|
|
73
|
+
out.claude = c;
|
|
74
|
+
}
|
|
75
|
+
for (const key of ['codex', 'antigravity']) {
|
|
76
|
+
const slice = obj[key];
|
|
77
|
+
if (slice && typeof slice === 'object' && !Array.isArray(slice)) {
|
|
78
|
+
if (typeof slice.model === 'string' && slice.model.trim()) {
|
|
79
|
+
out[key] = { model: slice.model.trim() };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
export function parseEnvironmentConfig(raw) {
|
|
86
|
+
let obj = raw;
|
|
87
|
+
if (typeof obj === 'string') {
|
|
88
|
+
if (!obj.trim())
|
|
89
|
+
return null;
|
|
90
|
+
try {
|
|
91
|
+
obj = JSON.parse(obj);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
|
98
|
+
return null;
|
|
99
|
+
const repositories = [];
|
|
100
|
+
if (Array.isArray(obj.repositories)) {
|
|
101
|
+
for (const r of obj.repositories) {
|
|
102
|
+
if (!r || typeof r !== 'object')
|
|
103
|
+
continue;
|
|
104
|
+
const url = typeof r.url === 'string' ? r.url.trim() : '';
|
|
105
|
+
if (!url)
|
|
106
|
+
continue;
|
|
107
|
+
const target_dir = typeof r.target_dir === 'string' && r.target_dir.trim() ? r.target_dir.trim() : '';
|
|
108
|
+
if (!target_dir)
|
|
109
|
+
continue;
|
|
110
|
+
repositories.push({
|
|
111
|
+
url,
|
|
112
|
+
target_dir,
|
|
113
|
+
branch: typeof r.branch === 'string' ? r.branch.trim() : '',
|
|
114
|
+
post_clone_commands: Array.isArray(r.post_clone_commands)
|
|
115
|
+
? r.post_clone_commands.filter((c) => typeof c === 'string' && c.trim())
|
|
116
|
+
: [],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const env_vars = {};
|
|
121
|
+
if (obj.env_vars && typeof obj.env_vars === 'object' && !Array.isArray(obj.env_vars)) {
|
|
122
|
+
for (const [k, v] of Object.entries(obj.env_vars)) {
|
|
123
|
+
if (typeof k === 'string' && k.trim() && typeof v === 'string')
|
|
124
|
+
env_vars[k] = v;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const setup_commands = Array.isArray(obj.setup_commands)
|
|
128
|
+
? obj.setup_commands.filter((c) => typeof c === 'string' && c.trim())
|
|
129
|
+
: [];
|
|
130
|
+
if (repositories.length === 0 && Object.keys(env_vars).length === 0 && setup_commands.length === 0) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const timeout = Number(obj.setup_timeout_seconds);
|
|
134
|
+
return {
|
|
135
|
+
repositories,
|
|
136
|
+
env_vars,
|
|
137
|
+
setup_commands,
|
|
138
|
+
setup_timeout_seconds: Number.isFinite(timeout) && timeout > 0 ? Math.floor(timeout) : 600,
|
|
139
|
+
version: Number.isFinite(Number(obj.version)) ? Math.floor(Number(obj.version)) : 0,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const AGENT_CHAIN_DEPTH_CAP = 3;
|
|
143
|
+
export class EventDispatcher {
|
|
144
|
+
#config;
|
|
145
|
+
#subagentManager;
|
|
146
|
+
#chatSessionManager;
|
|
147
|
+
#ticketSessionManager;
|
|
148
|
+
#fsBrowser;
|
|
149
|
+
#prompts;
|
|
150
|
+
#agentManagerCommandHandler;
|
|
151
|
+
#managedAgentContexts;
|
|
152
|
+
#worktreeManager;
|
|
153
|
+
#environmentProvisioner;
|
|
154
|
+
constructor(config, deps = {}) {
|
|
155
|
+
this.#config = config;
|
|
156
|
+
this.#subagentManager = deps.subagentManager ?? null;
|
|
157
|
+
this.#chatSessionManager = deps.chatSessionManager ?? null;
|
|
158
|
+
this.#ticketSessionManager = deps.ticketSessionManager ?? null;
|
|
159
|
+
this.#fsBrowser = deps.fsBrowser ?? null;
|
|
160
|
+
this.#prompts = deps.prompts ?? null;
|
|
161
|
+
this.#agentManagerCommandHandler = deps.agentManagerCommandHandler ?? null;
|
|
162
|
+
this.#managedAgentContexts = deps.managedAgentContexts ?? null;
|
|
163
|
+
this.#worktreeManager = deps.worktreeManager ?? null;
|
|
164
|
+
this.#environmentProvisioner = deps.environmentProvisioner ?? null;
|
|
165
|
+
}
|
|
166
|
+
async #applyWorktreeCwd(agentContext, ticketId, role) {
|
|
167
|
+
if (!agentContext || !this.#worktreeManager || !ticketId || !role)
|
|
168
|
+
return;
|
|
169
|
+
if (this.#config?.delegation?.worktreeIsolation === false)
|
|
170
|
+
return;
|
|
171
|
+
try {
|
|
172
|
+
const res = await this.#worktreeManager.resolveCwd({
|
|
173
|
+
baseWorkingDir: agentContext.cwd,
|
|
174
|
+
worktreesRoot: join(MANAGED_AGENTS_DIR, agentContext.agent_id, 'worktrees'),
|
|
175
|
+
ticketId,
|
|
176
|
+
role,
|
|
177
|
+
});
|
|
178
|
+
if (res.isWorktree) {
|
|
179
|
+
log(`[worktree] ticket=${ticketId.slice(0, 8)} role=${role} agent=${agentContext.agent_id.slice(0, 8)} cwd=${res.cwd}${res.reused ? ' (reused)' : ' (new)'}`);
|
|
180
|
+
agentContext.cwd = res.cwd;
|
|
181
|
+
}
|
|
182
|
+
else if (res.reason && res.reason !== 'disabled') {
|
|
183
|
+
log(`[worktree] isolation skipped for ticket=${ticketId.slice(0, 8)} role=${role}: ${res.reason} — using shared cwd ${agentContext.cwd}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
log(`[worktree] resolveCwd failed (${err?.message ?? err}); using shared cwd`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async #cleanupTerminalTicketWorktrees(ticketId) {
|
|
191
|
+
if (!this.#worktreeManager || !this.#worktreeManager.enabled)
|
|
192
|
+
return;
|
|
193
|
+
if (this.#config?.delegation?.worktreeIsolation === false)
|
|
194
|
+
return;
|
|
195
|
+
if (!this.#managedAgentContexts)
|
|
196
|
+
return;
|
|
197
|
+
try {
|
|
198
|
+
const ticket = await fetchTicketContext(this.#config, ticketId);
|
|
199
|
+
if (!ticket || !ticket.terminal_entered_at)
|
|
200
|
+
return;
|
|
201
|
+
let total = 0;
|
|
202
|
+
const seenRoots = new Set();
|
|
203
|
+
for (const ctx of this.#managedAgentContexts.list()) {
|
|
204
|
+
if (!ctx.working_dir)
|
|
205
|
+
continue;
|
|
206
|
+
const worktreesRoot = join(MANAGED_AGENTS_DIR, ctx.agent_id, 'worktrees');
|
|
207
|
+
const dedupeKey = `${ctx.working_dir} ${worktreesRoot}`;
|
|
208
|
+
if (seenRoots.has(dedupeKey))
|
|
209
|
+
continue;
|
|
210
|
+
seenRoots.add(dedupeKey);
|
|
211
|
+
total += await this.#worktreeManager.removeTicketWorktrees({
|
|
212
|
+
baseWorkingDir: ctx.working_dir,
|
|
213
|
+
worktreesRoot,
|
|
214
|
+
ticketId,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (total > 0) {
|
|
218
|
+
log(`[worktree] terminal ticket=${ticketId.slice(0, 8)} reclaimed ${total} worktree(s)`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
log(`[worktree] terminal cleanup failed for ticket=${ticketId.slice(0, 8)}: ${err?.message ?? err}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
#resolveAgentContext(eventAgentId) {
|
|
226
|
+
if (!eventAgentId || !this.#managedAgentContexts)
|
|
227
|
+
return undefined;
|
|
228
|
+
const ctx = this.#managedAgentContexts.get(eventAgentId);
|
|
229
|
+
if (!ctx)
|
|
230
|
+
return undefined;
|
|
231
|
+
if (!ctx.api_key || !ctx.working_dir || !ctx.mcp_config_path)
|
|
232
|
+
return undefined;
|
|
233
|
+
return {
|
|
234
|
+
agent_id: ctx.agent_id,
|
|
235
|
+
api_key: ctx.api_key,
|
|
236
|
+
cwd: ctx.working_dir,
|
|
237
|
+
mcp_config_path: ctx.mcp_config_path,
|
|
238
|
+
cli: ctx.cli || 'claude',
|
|
239
|
+
cli_home_dir: ctx.cli_home_dir,
|
|
240
|
+
extra_env: ctx.extra_env,
|
|
241
|
+
credential_provider: ctx.credential_provider ?? null,
|
|
242
|
+
model: ctx.model ?? null,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
#resolveAgentContextFromMembers(memberIds) {
|
|
246
|
+
if (!memberIds.length || !this.#managedAgentContexts)
|
|
247
|
+
return undefined;
|
|
248
|
+
for (const id of memberIds) {
|
|
249
|
+
const ctx = this.#resolveAgentContext(id);
|
|
250
|
+
if (ctx)
|
|
251
|
+
return ctx;
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
#senderIsSelf(senderId) {
|
|
256
|
+
if (!senderId)
|
|
257
|
+
return false;
|
|
258
|
+
const selfAgentId = loadAgentInfo()?.agent_id || '';
|
|
259
|
+
if (selfAgentId && senderId === selfAgentId)
|
|
260
|
+
return true;
|
|
261
|
+
return !!this.#managedAgentContexts?.has(senderId);
|
|
262
|
+
}
|
|
263
|
+
dispatch(eventType, raw) {
|
|
264
|
+
if (!raw)
|
|
265
|
+
return;
|
|
266
|
+
switch (eventType) {
|
|
267
|
+
case 'agent_trigger':
|
|
268
|
+
case 'board_update':
|
|
269
|
+
case 'chat_request':
|
|
270
|
+
case 'chat_room_message':
|
|
271
|
+
case 'comment_mention':
|
|
272
|
+
case 'fs_request':
|
|
273
|
+
case 'agent_manager_command':
|
|
274
|
+
recordEvent(eventType, raw);
|
|
275
|
+
break;
|
|
276
|
+
default:
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
switch (eventType) {
|
|
280
|
+
case 'agent_trigger':
|
|
281
|
+
return this.handleTrigger(raw);
|
|
282
|
+
case 'board_update':
|
|
283
|
+
return this.handleBoardUpdate(raw);
|
|
284
|
+
case 'chat_request':
|
|
285
|
+
return this.handleChatRequest(raw);
|
|
286
|
+
case 'chat_room_message':
|
|
287
|
+
return this.handleChatRoomMessage(raw);
|
|
288
|
+
case 'comment_mention':
|
|
289
|
+
return this.handleCommentMention(raw);
|
|
290
|
+
case 'fs_request':
|
|
291
|
+
return this.handleFsRequest(raw);
|
|
292
|
+
case 'agent_manager_command':
|
|
293
|
+
return this.#agentManagerCommandHandler
|
|
294
|
+
? this.#agentManagerCommandHandler.handle(raw)
|
|
295
|
+
: undefined;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async handleFsRequest(raw) {
|
|
299
|
+
let ev;
|
|
300
|
+
try {
|
|
301
|
+
ev = JSON.parse(raw);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
log(`Failed to parse fs_request: ${err?.message ?? err}`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const requestId = ev.request_id;
|
|
308
|
+
if (!requestId) {
|
|
309
|
+
log('fs_request missing request_id — dropped');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (!this.#fsBrowser) {
|
|
313
|
+
log('handleFsRequest: no FsBrowser wired — lazy-constructing a default. Fix main.ts wiring.');
|
|
314
|
+
const { FsBrowser } = await import('./fs-browser.js');
|
|
315
|
+
this.#fsBrowser = new FsBrowser(this.#config, null);
|
|
316
|
+
}
|
|
317
|
+
const result = await this.#fsBrowser.handle({
|
|
318
|
+
op: ev.op,
|
|
319
|
+
path: ev.path,
|
|
320
|
+
offset: ev.offset,
|
|
321
|
+
limit: ev.limit,
|
|
322
|
+
name: ev.name,
|
|
323
|
+
});
|
|
324
|
+
await postFsResponse(this.#config, requestId, result);
|
|
325
|
+
log(`fs_request ${ev.op} ${ev.path} → ${result.ok ? 'ok' : `err:${result.code || 'FS_ERROR'}`}`);
|
|
326
|
+
}
|
|
327
|
+
async handleTrigger(raw) {
|
|
328
|
+
let ev;
|
|
329
|
+
try {
|
|
330
|
+
ev = JSON.parse(raw);
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
log(`Failed to parse trigger: ${err?.message ?? err}`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const selfAgentId = loadAgentInfo()?.agent_id || '';
|
|
337
|
+
const eventAgentId = ev.actor_name || ev.agent_id || '';
|
|
338
|
+
const agentContext = this.#resolveAgentContext(eventAgentId);
|
|
339
|
+
if (selfAgentId &&
|
|
340
|
+
eventAgentId &&
|
|
341
|
+
selfAgentId !== eventAgentId &&
|
|
342
|
+
!agentContext) {
|
|
343
|
+
log(`Trigger dropped (not for this agent): target=${eventAgentId} self=${selfAgentId}`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
await this.#applyWorktreeCwd(agentContext, ev.ticket_id, ev.action);
|
|
347
|
+
const harness = parseHarnessConfig(ev.harness_config);
|
|
348
|
+
if (harness) {
|
|
349
|
+
log(`Trigger carries harness_config: ticket=${ev.ticket_id} keys=${Object.keys(harness).join(',')}`);
|
|
350
|
+
}
|
|
351
|
+
const effortPreset = parseEffortPreset(ev.effort_preset);
|
|
352
|
+
if (effortPreset) {
|
|
353
|
+
log(`Trigger carries effort_preset: id=${effortPreset.id}${effortPreset.label ? ` (${effortPreset.label})` : ''} ticket=${ev.ticket_id}`);
|
|
354
|
+
}
|
|
355
|
+
const envConfig = parseEnvironmentConfig(ev.environment_config);
|
|
356
|
+
const envVars = envConfig?.env_vars && Object.keys(envConfig.env_vars).length > 0
|
|
357
|
+
? envConfig.env_vars
|
|
358
|
+
: undefined;
|
|
359
|
+
if (envConfig && this.#environmentProvisioner) {
|
|
360
|
+
const provisionAgentId = agentContext?.agent_id || selfAgentId;
|
|
361
|
+
if (!provisionAgentId) {
|
|
362
|
+
log(`[env-provision] no agent id resolvable for ticket=${ev.ticket_id} — skipping provisioning`);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
log(`Trigger carries environment_config: ticket=${ev.ticket_id} repos=${envConfig.repositories.length} setup=${envConfig.setup_commands.length} env_vars=${Object.keys(envConfig.env_vars).length}`);
|
|
366
|
+
const result = await this.#environmentProvisioner.provision({
|
|
367
|
+
agentId: provisionAgentId,
|
|
368
|
+
config: envConfig,
|
|
369
|
+
ticketId: ev.ticket_id,
|
|
370
|
+
});
|
|
371
|
+
if (!result.ok) {
|
|
372
|
+
if (!result.reported && ev.ticket_id) {
|
|
373
|
+
const detail = (result.steps.length > 0 ? `\n\n실행 단계:\n${result.steps.map((s) => `- ${s}`).join('\n')}` : '');
|
|
374
|
+
await fireAndForgetTool(this.#config, 'add_comment', {
|
|
375
|
+
ticket_id: ev.ticket_id,
|
|
376
|
+
content: `⚠️ **환경 프로비저닝 실패** — 작업을 시작하지 않고 디스패치를 중단했습니다.\n\n` +
|
|
377
|
+
`\`\`\`\n${result.error || 'unknown error'}\n\`\`\`${detail}\n\n` +
|
|
378
|
+
`Board 환경설정(repositories / setup_commands)을 확인한 뒤 다시 트리거하세요. ` +
|
|
379
|
+
`(fingerprint=\`${result.fingerprint.slice(0, 12)}\`, 약 5분 동안 재시도/재알림을 억제합니다.)`,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
log(`Trigger aborted — environment provisioning failed: ticket=${ev.ticket_id} fp=${result.fingerprint.slice(0, 8)} reported=${result.reported === true}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (!result.skipped) {
|
|
386
|
+
log(`Environment provisioned for ticket=${ev.ticket_id} fp=${result.fingerprint.slice(0, 8)}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const delegation = this.#config?.delegation ?? {};
|
|
391
|
+
const delegationEnabled = delegation.enabled !== false;
|
|
392
|
+
const persistentTicket = delegation.persistentTicketSessions !== false;
|
|
393
|
+
if (delegationEnabled && persistentTicket && this.#ticketSessionManager) {
|
|
394
|
+
try {
|
|
395
|
+
const ticket = await fetchTicketContext(this.#config, ev.ticket_id);
|
|
396
|
+
const rolePrompt = ev.role_prompt || '';
|
|
397
|
+
const ticketPrompt = ev.ticket_prompt || '';
|
|
398
|
+
const columnPrompt = ev.column_prompt || null;
|
|
399
|
+
const result = await this.#ticketSessionManager.dispatchTrigger({
|
|
400
|
+
ticketId: ev.ticket_id || '',
|
|
401
|
+
role: ev.action || '',
|
|
402
|
+
triggerId: ev.field_changed || '',
|
|
403
|
+
agentId: ev.actor_name || '',
|
|
404
|
+
rolePrompt,
|
|
405
|
+
ticketPrompt,
|
|
406
|
+
columnPrompt,
|
|
407
|
+
ticket,
|
|
408
|
+
forceRespawn: ev.force_respawn === true,
|
|
409
|
+
triggerSource: ev.trigger_source || '',
|
|
410
|
+
agentContext,
|
|
411
|
+
harness,
|
|
412
|
+
effortPreset,
|
|
413
|
+
envVars,
|
|
414
|
+
maxConcurrentTicketsPerAgent: typeof ev.max_concurrent_tickets_per_agent === 'number'
|
|
415
|
+
? ev.max_concurrent_tickets_per_agent
|
|
416
|
+
: undefined,
|
|
417
|
+
});
|
|
418
|
+
if (result.dispatched) {
|
|
419
|
+
log(`Trigger dispatched to ticket session: ticket=${ev.ticket_id} pid=${result.pid}${result.firstTurn ? ' (new session)' : ''}`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (result.reason === 'duplicate_trigger') {
|
|
423
|
+
log(`Trigger deduped: ticket=${ev.ticket_id} trigger=${ev.field_changed || ''}`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (result.reason === 'circuit_breaker_open') {
|
|
427
|
+
log(`Trigger blocked by circuit-breaker: ticket=${ev.ticket_id} — not falling back to one-shot`);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
log(`Ticket session dispatch declined (${result.reason}), falling back to one-shot subagent`);
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
log(`Ticket session path failed: ${err?.message ?? err}, falling back to one-shot subagent`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const canDelegate = delegationEnabled && this.#subagentManager && this.#subagentManager.canSpawn();
|
|
437
|
+
if (canDelegate && this.#subagentManager) {
|
|
438
|
+
try {
|
|
439
|
+
const ticket = await fetchTicketContext(this.#config, ev.ticket_id);
|
|
440
|
+
const rolePrompt = ev.role_prompt || '';
|
|
441
|
+
const ticketPrompt = ev.ticket_prompt || '';
|
|
442
|
+
const columnPrompt = ev.column_prompt || null;
|
|
443
|
+
const taskText = this.#prompts?.composeTriggerPrompt(ticket, rolePrompt, ticketPrompt, ev.ticket_id, columnPrompt) ?? `[trigger] ${ev.ticket_id}`;
|
|
444
|
+
const result = await this.#subagentManager.spawn({
|
|
445
|
+
kind: 'trigger',
|
|
446
|
+
taskText,
|
|
447
|
+
rolePrompt,
|
|
448
|
+
triggerId: ev.field_changed || '',
|
|
449
|
+
ticketId: ev.ticket_id || '',
|
|
450
|
+
agentId: ev.actor_name || '',
|
|
451
|
+
role: ev.action || '',
|
|
452
|
+
triggerSource: ev.trigger_source || '',
|
|
453
|
+
agentContext,
|
|
454
|
+
harness,
|
|
455
|
+
effortPreset,
|
|
456
|
+
envVars,
|
|
457
|
+
});
|
|
458
|
+
if (result.spawned) {
|
|
459
|
+
log(`Trigger dispatched to subagent: ticket=${ev.ticket_id} pid=${result.pid}${agentContext ? ` agent=${agentContext.agent_id.slice(0, 8)}` : ''}`);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
log(`Subagent spawn declined (${result.reason}); no further fallback in standalone mode`);
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
log(`Delegation path failed: ${err?.message ?? err}; dropping`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
log(`Trigger processed (no delegation path spawned): ticket=${ev.ticket_id} role=${ev.action}`);
|
|
469
|
+
}
|
|
470
|
+
async handleChatRequest(raw) {
|
|
471
|
+
let ev;
|
|
472
|
+
try {
|
|
473
|
+
ev = JSON.parse(raw);
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
log(`Failed to parse chat_request: ${err?.message ?? err}`);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const payload = ev.payload || {};
|
|
480
|
+
const agentContext = this.#resolveAgentContext(payload.agent_id || '');
|
|
481
|
+
const delegation = this.#config?.delegation ?? {};
|
|
482
|
+
const delegationEnabled = delegation.enabled !== false;
|
|
483
|
+
const persistentChat = delegation.persistentChatSessions !== false;
|
|
484
|
+
if (delegationEnabled &&
|
|
485
|
+
persistentChat &&
|
|
486
|
+
this.#chatSessionManager &&
|
|
487
|
+
payload.room_id) {
|
|
488
|
+
const onProgress = (stage) => {
|
|
489
|
+
const status = stage === 'thinking' ? 'thinking' : 'composing reply';
|
|
490
|
+
this.#setChatRoomTyping(payload.room_id, true, status).catch(() => { });
|
|
491
|
+
};
|
|
492
|
+
try {
|
|
493
|
+
const result = await this.#chatSessionManager.dispatch({
|
|
494
|
+
roomId: payload.room_id,
|
|
495
|
+
agentId: payload.agent_id || agentContext?.agent_id || loadAgentInfo()?.agent_id || '',
|
|
496
|
+
senderId: payload.user_id || '',
|
|
497
|
+
senderName: '',
|
|
498
|
+
createdAt: ev.timestamp || '',
|
|
499
|
+
content: payload.new_message || '',
|
|
500
|
+
rolePrompt: payload.role_prompt || '',
|
|
501
|
+
onProgress,
|
|
502
|
+
agentContext,
|
|
503
|
+
});
|
|
504
|
+
if (result.dispatched) {
|
|
505
|
+
log(`Chat request dispatched to session: room=${payload.room_id} pid=${result.pid}${result.firstTurn ? ' (new session)' : ''}`);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (result.reason === 'duplicate_chat') {
|
|
509
|
+
log(`Chat request deduped: room=${payload.room_id} user=${payload.user_id} ts=${ev.timestamp || ''}`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
log(`Chat session dispatch declined (${result.reason}), falling back to legacy path`);
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
log(`Chat session path failed: ${err?.message ?? err}, falling back to legacy path`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const canDelegate = delegationEnabled && this.#subagentManager && this.#subagentManager.canSpawn();
|
|
519
|
+
if (canDelegate && this.#subagentManager) {
|
|
520
|
+
const rolePrompt = payload.role_prompt || '';
|
|
521
|
+
const history = Array.isArray(payload.history) ? payload.history : [];
|
|
522
|
+
const newMessage = payload.new_message || '';
|
|
523
|
+
const usesNativeMcp = createAdapter(agentContext?.cli).has(ADAPTER_CAPABILITIES.NATIVE_MCP);
|
|
524
|
+
const taskText = this.#prompts?.composeChatPrompt(rolePrompt, history, newMessage, payload.room_id || '', usesNativeMcp) ?? `[chat] ${newMessage}`;
|
|
525
|
+
try {
|
|
526
|
+
const result = await this.#subagentManager.spawn({
|
|
527
|
+
kind: 'chat',
|
|
528
|
+
taskText,
|
|
529
|
+
rolePrompt,
|
|
530
|
+
chatRequestId: payload.user_id
|
|
531
|
+
? `msg:${payload.user_id}:${ev.timestamp || ''}`
|
|
532
|
+
: undefined,
|
|
533
|
+
ticketId: payload.ticket_id || '',
|
|
534
|
+
agentId: payload.agent_id || '',
|
|
535
|
+
roomId: payload.room_id || '',
|
|
536
|
+
agentContext,
|
|
537
|
+
});
|
|
538
|
+
if (result.spawned) {
|
|
539
|
+
log(`Chat request dispatched to subagent: agent=${payload.agent_id} pid=${result.pid}`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
log(`Chat subagent spawn declined (${result.reason}); no further fallback in standalone mode`);
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
log(`Chat delegation path failed: ${err?.message ?? err}; dropping`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
log(`Chat request dropped (no delegation path): agent=${payload.agent_id} user=${payload.user_id}`);
|
|
549
|
+
}
|
|
550
|
+
async handleCommentMention(raw) {
|
|
551
|
+
let ev;
|
|
552
|
+
try {
|
|
553
|
+
ev = JSON.parse(raw);
|
|
554
|
+
}
|
|
555
|
+
catch (err) {
|
|
556
|
+
log(`Failed to parse comment_mention: ${err?.message ?? err}`);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const ticketId = ev.ticket_id || '';
|
|
560
|
+
const commentId = ev.comment_id || ev.field_changed || '';
|
|
561
|
+
const agentId = ev.agent_id || ev.actor_name || '';
|
|
562
|
+
const agentContext = this.#resolveAgentContext(agentId);
|
|
563
|
+
const mention = {
|
|
564
|
+
ticket_id: ticketId,
|
|
565
|
+
comment_id: commentId,
|
|
566
|
+
actor_name: ev.actor_name || '',
|
|
567
|
+
actor_id: ev.actor_id || '',
|
|
568
|
+
content: ev.content || '',
|
|
569
|
+
mention_source: ev.mention_source || 'direct',
|
|
570
|
+
role_shortcut: ev.role_shortcut || '',
|
|
571
|
+
};
|
|
572
|
+
const delegation = this.#config?.delegation ?? {};
|
|
573
|
+
const delegationEnabled = delegation.enabled !== false;
|
|
574
|
+
const persistentTicket = delegation.persistentTicketSessions !== false;
|
|
575
|
+
if (delegationEnabled && persistentTicket && this.#ticketSessionManager && ticketId) {
|
|
576
|
+
try {
|
|
577
|
+
const forwarded = this.#ticketSessionManager.forwardCommentMention(ticketId, mention, ev.agent_id || '');
|
|
578
|
+
if (forwarded) {
|
|
579
|
+
log(`Comment mention forwarded to ticket session: ticket=${ticketId} comment=${commentId}`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch (err) {
|
|
584
|
+
log(`Ticket session forward failed for comment_mention: ${err?.message ?? err}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const canDelegate = delegationEnabled && this.#subagentManager && this.#subagentManager.canSpawn();
|
|
588
|
+
if (canDelegate && this.#subagentManager) {
|
|
589
|
+
try {
|
|
590
|
+
const ticket = ticketId ? await fetchTicketContext(this.#config, ticketId) : null;
|
|
591
|
+
const rolePrompt = ev.role_prompt || '';
|
|
592
|
+
const taskText = this.#prompts?.composeCommentMentionPrompt(ticket, rolePrompt, mention, ticketId) ?? `[mention] ${ticketId} ${commentId}`;
|
|
593
|
+
const result = await this.#subagentManager.spawn({
|
|
594
|
+
kind: 'trigger',
|
|
595
|
+
taskText,
|
|
596
|
+
rolePrompt,
|
|
597
|
+
triggerId: mentionTriggerId(commentId, agentId),
|
|
598
|
+
ticketId,
|
|
599
|
+
agentId,
|
|
600
|
+
role: mention.mention_source === 'role' ? mention.role_shortcut || '' : '',
|
|
601
|
+
agentContext,
|
|
602
|
+
});
|
|
603
|
+
if (result.spawned) {
|
|
604
|
+
log(`Comment mention dispatched to subagent: ticket=${ticketId} comment=${commentId} pid=${result.pid}`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
log(`Comment mention subagent spawn declined (${result.reason}); no further fallback in standalone mode`);
|
|
608
|
+
}
|
|
609
|
+
catch (err) {
|
|
610
|
+
log(`Comment mention delegation failed: ${err?.message ?? err}; dropping`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
log(`Comment mention dropped (no delegation path): ticket=${ticketId} comment=${commentId}`);
|
|
614
|
+
}
|
|
615
|
+
handleBoardUpdate(raw) {
|
|
616
|
+
try {
|
|
617
|
+
const ev = JSON.parse(raw);
|
|
618
|
+
if (ev.entity_type === 'ticket' && ev.action === 'moved' && ev.ticket_id) {
|
|
619
|
+
void this.#cleanupTerminalTicketWorktrees(ev.ticket_id);
|
|
620
|
+
}
|
|
621
|
+
if (this.#ticketSessionManager && ev.ticket_id) {
|
|
622
|
+
const forwarded = this.#ticketSessionManager.forwardBoardUpdate(ev.ticket_id, ev);
|
|
623
|
+
if (forwarded) {
|
|
624
|
+
log(`Board update forwarded to ticket session: ticket=${ev.ticket_id} ${ev.entity_type}.${ev.action}`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
log(`Board update dropped (no live ticket session): ticket=${ev.ticket_id} ${ev.entity_type}.${ev.action}`);
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
log(`Failed to parse board_update: ${err?.message ?? err}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async #setChatRoomTyping(roomId, isTyping, status = null) {
|
|
635
|
+
try {
|
|
636
|
+
const agentInfo = loadAgentInfo();
|
|
637
|
+
const url = `${this.#config.url.replace(/\/$/, '')}/api/agent/chat-rooms/${encodeURIComponent(roomId)}/typing`;
|
|
638
|
+
await fetch(url, {
|
|
639
|
+
method: 'POST',
|
|
640
|
+
headers: {
|
|
641
|
+
'X-Agent-Key': this.#config.apiKey,
|
|
642
|
+
'Content-Type': 'application/json',
|
|
643
|
+
},
|
|
644
|
+
body: JSON.stringify({
|
|
645
|
+
agent_id: agentInfo?.agent_id || '',
|
|
646
|
+
agent_name: agentInfo?.agent_name || agentInfo?.name || 'Agent',
|
|
647
|
+
is_typing: isTyping,
|
|
648
|
+
status,
|
|
649
|
+
}),
|
|
650
|
+
signal: AbortSignal.timeout(5000),
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
catch (err) {
|
|
654
|
+
log(`setChatRoomTyping failed: ${err?.message ?? err}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async handleChatRoomMessage(raw) {
|
|
658
|
+
let ev;
|
|
659
|
+
try {
|
|
660
|
+
ev = JSON.parse(raw);
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
log(`Failed to parse chat_room_message: ${err?.message ?? err}`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const p = ev.payload || ev;
|
|
667
|
+
if (p.type === 'progress') {
|
|
668
|
+
log(`Chat room message is progress heartbeat (room=${p.room_id} sender=${p.sender_name || p.sender_id}) — skipping delegation`);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const memberIds = Array.isArray(p.agent_member_ids) ? p.agent_member_ids : [];
|
|
672
|
+
const agentContext = this.#resolveAgentContextFromMembers(memberIds);
|
|
673
|
+
if (p.sender_type === 'agent') {
|
|
674
|
+
if (this.#senderIsSelf(p.sender_id)) {
|
|
675
|
+
this.#chatSessionManager?.recordRoomMessage(p);
|
|
676
|
+
log(`Chat room message from self (${p.sender_name || p.sender_id}) — skipping delegation`);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const depth = typeof p.agent_chain_depth === 'number' ? p.agent_chain_depth : 0;
|
|
680
|
+
if (depth >= AGENT_CHAIN_DEPTH_CAP) {
|
|
681
|
+
this.#chatSessionManager?.recordRoomMessage(p);
|
|
682
|
+
log(`Chat room message from agent (${p.sender_name || p.sender_id}) — agent_chain_depth=${depth} ` +
|
|
683
|
+
`>= cap ${AGENT_CHAIN_DEPTH_CAP}, skipping delegation to break loop`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
let runContext = agentContext;
|
|
688
|
+
const runProvision = parseRunProvision(p.run_provision);
|
|
689
|
+
if (runProvision) {
|
|
690
|
+
log(`Chat room run dispatch: kind=${runProvision.kind} run=${runProvision.run_id.slice(0, 8)} ` +
|
|
691
|
+
`folder=${runProvision.workspace_folder} checkout=${runProvision.checkout_mode} ` +
|
|
692
|
+
`repo=${runProvision.repo ? runProvision.repo.url : 'none'}`);
|
|
693
|
+
const result = await provisionRunWorkspace(runProvision);
|
|
694
|
+
if (!result.ok) {
|
|
695
|
+
const responder = agentContext?.agent_id || loadAgentInfo()?.agent_id || '';
|
|
696
|
+
if (p.room_id && responder) {
|
|
697
|
+
const detail = result.steps.length > 0 ? `\n\n실행 단계:\n${result.steps.map((s) => `- ${s}`).join('\n')}` : '';
|
|
698
|
+
await postChatRoomMessage(this.#config, p.room_id, responder, `⚠️ **런 작업폴더 프로비저닝 실패** — 작업을 시작하지 않고 디스패치를 중단했습니다.\n\n` +
|
|
699
|
+
`\`\`\`\n${result.error || 'unknown error'}\n\`\`\`${detail}\n\n` +
|
|
700
|
+
`시나리오의 repo / branch / checkout 설정을 확인한 뒤 다시 실행하세요.`).catch(() => { });
|
|
701
|
+
}
|
|
702
|
+
const completeTool = runProvision.kind === 'qa' ? 'complete_qa_run' : 'complete_security_run';
|
|
703
|
+
await fireAndForgetTool(this.#config, completeTool, {
|
|
704
|
+
run_id: runProvision.run_id,
|
|
705
|
+
workspace_id: runProvision.workspace_id,
|
|
706
|
+
status: 'error',
|
|
707
|
+
summary: `작업폴더 프로비저닝 실패: ${result.error || 'unknown error'}`,
|
|
708
|
+
});
|
|
709
|
+
if (p.room_id)
|
|
710
|
+
await this.#setChatRoomTyping(p.room_id, false, '').catch(() => { });
|
|
711
|
+
log(`Chat room run dispatch aborted — provisioning failed: run=${runProvision.run_id.slice(0, 8)} dir=${result.dir}`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (agentContext)
|
|
715
|
+
runContext = { ...agentContext, cwd: result.dir };
|
|
716
|
+
log(`Run workspace ready: run=${runProvision.run_id.slice(0, 8)} dir=${result.dir}`);
|
|
717
|
+
}
|
|
718
|
+
if (p.room_id) {
|
|
719
|
+
await this.#setChatRoomTyping(p.room_id, true, '👀 reading context');
|
|
720
|
+
}
|
|
721
|
+
const onProgress = p.room_id
|
|
722
|
+
? (stage) => {
|
|
723
|
+
const status = stage === 'thinking' ? 'thinking' : 'composing reply';
|
|
724
|
+
this.#setChatRoomTyping(p.room_id, true, status).catch(() => { });
|
|
725
|
+
}
|
|
726
|
+
: undefined;
|
|
727
|
+
const delegation = this.#config?.delegation ?? {};
|
|
728
|
+
const delegationEnabled = delegation.enabled !== false;
|
|
729
|
+
const persistentChat = delegation.persistentChatSessions !== false;
|
|
730
|
+
if (delegationEnabled && persistentChat && this.#chatSessionManager && p.room_id) {
|
|
731
|
+
const responderAgentId = agentContext?.agent_id || loadAgentInfo()?.agent_id || '';
|
|
732
|
+
try {
|
|
733
|
+
const result = await this.#chatSessionManager.dispatch({
|
|
734
|
+
roomId: p.room_id,
|
|
735
|
+
agentId: responderAgentId,
|
|
736
|
+
senderId: p.sender_id || '',
|
|
737
|
+
senderName: p.sender_name || '',
|
|
738
|
+
createdAt: p.created_at || '',
|
|
739
|
+
content: p.content || '',
|
|
740
|
+
rolePrompt: p.role_prompt || '',
|
|
741
|
+
onProgress,
|
|
742
|
+
agentContext: runContext,
|
|
743
|
+
attachments: Array.isArray(p.attachments) ? p.attachments : [],
|
|
744
|
+
});
|
|
745
|
+
this.#chatSessionManager?.recordRoomMessage(p);
|
|
746
|
+
if (result.dispatched) {
|
|
747
|
+
log(`Chat room message dispatched to session: room=${p.room_id} ` +
|
|
748
|
+
`agent=${responderAgentId.slice(0, 8)} pid=${result.pid}` +
|
|
749
|
+
`${result.firstTurn ? ' (new session)' : ''}`);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (result.reason === 'duplicate_chat') {
|
|
753
|
+
log(`Chat room message deduped: room=${p.room_id} sender=${p.sender_id} ts=${p.created_at || ''}`);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
log(`Chat room session dispatch declined (${result.reason}), falling back to legacy path`);
|
|
757
|
+
}
|
|
758
|
+
catch (err) {
|
|
759
|
+
log(`Chat room session path failed: ${err?.message ?? err}, falling back to legacy path`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const canDelegate = delegationEnabled && this.#subagentManager && this.#subagentManager.canSpawn();
|
|
763
|
+
if (canDelegate && this.#subagentManager) {
|
|
764
|
+
try {
|
|
765
|
+
await this.#setChatRoomTyping(p.room_id, true, 'thinking');
|
|
766
|
+
const history = await fetchChatRoomHistory(this.#config, p.room_id);
|
|
767
|
+
const rolePrompt = p.role_prompt || '';
|
|
768
|
+
const prepared = await prepareChatAttachments(this.#config, p.room_id, Array.isArray(p.attachments) ? p.attachments : [], { fetchImages: false });
|
|
769
|
+
const usesNativeMcp = createAdapter(agentContext?.cli).has(ADAPTER_CAPABILITIES.NATIVE_MCP);
|
|
770
|
+
const taskText = this.#prompts?.composeChatRoomPrompt(p.room_id, history, {
|
|
771
|
+
content: p.content || '',
|
|
772
|
+
sender_name: p.sender_name || '',
|
|
773
|
+
sender_id: p.sender_id || '',
|
|
774
|
+
}, prepared, usesNativeMcp) ?? `[chat_room] ${p.content || ''}`;
|
|
775
|
+
const result = await this.#subagentManager.spawn({
|
|
776
|
+
kind: 'chat',
|
|
777
|
+
taskText,
|
|
778
|
+
rolePrompt,
|
|
779
|
+
chatRequestId: `msg:${p.sender_id}:${p.created_at || ''}`,
|
|
780
|
+
ticketId: '',
|
|
781
|
+
agentId: agentContext?.agent_id || '',
|
|
782
|
+
roomId: p.room_id || '',
|
|
783
|
+
agentContext: runContext,
|
|
784
|
+
});
|
|
785
|
+
if (result.spawned) {
|
|
786
|
+
await this.#setChatRoomTyping(p.room_id, true, 'composing reply');
|
|
787
|
+
log(`Chat room message dispatched to subagent: room=${p.room_id} pid=${result.pid}`);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
log(`Chat room subagent spawn declined (${result.reason}); no further fallback in standalone mode`);
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
log(`Chat room delegation path failed: ${err?.message ?? err}; dropping`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
log(`Chat room message dropped (no delegation path): room=${p.room_id} sender=${p.sender_name || p.sender_id}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
//# sourceMappingURL=event-dispatcher.js.map
|