@calltelemetry/openclaw-linear 0.9.8 → 0.9.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calltelemetry/openclaw-linear",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "description": "Linear Agent plugin for OpenClaw — webhook-driven AI pipeline with OAuth, multi-agent routing, and issue triage",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -2,7 +2,7 @@
2
2
  * Recorded API responses from sub-issue decomposition smoke test.
3
3
  * Auto-generated — do not edit manually.
4
4
  * Re-generate by running: npx vitest run src/__test__/smoke-linear-api.test.ts
5
- * Last recorded: 2026-02-22T02:12:13.118Z
5
+ * Last recorded: 2026-02-22T02:38:53.901Z
6
6
  */
7
7
 
8
8
  export const RECORDED = {
@@ -44,20 +44,20 @@ export const RECORDED = {
44
44
  }
45
45
  ],
46
46
  "createParent": {
47
- "id": "b2794dc5-82a1-4942-b999-36c7093b7477",
48
- "identifier": "UAT-285"
47
+ "id": "953324f8-878f-40ba-b773-8d8815ee7987",
48
+ "identifier": "UAT-320"
49
49
  },
50
50
  "createSubIssue1": {
51
- "id": "5a928cd6-7dd8-4e3d-9aa0-4699ee2f3990",
52
- "identifier": "UAT-286"
51
+ "id": "69ce9a01-3bdb-4f4a-a882-e28e23d1b628",
52
+ "identifier": "UAT-321"
53
53
  },
54
54
  "createSubIssue2": {
55
- "id": "b5d88918-6f3f-43d9-9e3b-e98608707efe",
56
- "identifier": "UAT-287"
55
+ "id": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
56
+ "identifier": "UAT-322"
57
57
  },
58
58
  "subIssue1Details": {
59
- "id": "5a928cd6-7dd8-4e3d-9aa0-4699ee2f3990",
60
- "identifier": "UAT-286",
59
+ "id": "69ce9a01-3bdb-4f4a-a882-e28e23d1b628",
60
+ "identifier": "UAT-321",
61
61
  "title": "[SMOKE TEST] Sub-Issue 1: Backend API",
62
62
  "description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
63
63
  "estimate": 2,
@@ -83,16 +83,16 @@ export const RECORDED = {
83
83
  },
84
84
  "project": null,
85
85
  "parent": {
86
- "id": "b2794dc5-82a1-4942-b999-36c7093b7477",
87
- "identifier": "UAT-285"
86
+ "id": "953324f8-878f-40ba-b773-8d8815ee7987",
87
+ "identifier": "UAT-320"
88
88
  },
89
89
  "relations": {
90
90
  "nodes": []
91
91
  }
92
92
  },
93
93
  "subIssue2Details": {
94
- "id": "b5d88918-6f3f-43d9-9e3b-e98608707efe",
95
- "identifier": "UAT-287",
94
+ "id": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
95
+ "identifier": "UAT-322",
96
96
  "title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
97
97
  "description": "Build the frontend search UI component.\n\nGiven the search page loads, when the user types a query, then results display in real-time.",
98
98
  "estimate": 3,
@@ -118,18 +118,18 @@ export const RECORDED = {
118
118
  },
119
119
  "project": null,
120
120
  "parent": {
121
- "id": "b2794dc5-82a1-4942-b999-36c7093b7477",
122
- "identifier": "UAT-285"
121
+ "id": "953324f8-878f-40ba-b773-8d8815ee7987",
122
+ "identifier": "UAT-320"
123
123
  },
124
124
  "relations": {
125
125
  "nodes": []
126
126
  }
127
127
  },
128
128
  "parentDetails": {
129
- "id": "b2794dc5-82a1-4942-b999-36c7093b7477",
130
- "identifier": "UAT-285",
129
+ "id": "953324f8-878f-40ba-b773-8d8815ee7987",
130
+ "identifier": "UAT-320",
131
131
  "title": "[SMOKE TEST] Sub-Issue Parent: Search Feature",
132
- "description": "Auto-generated by smoke test to verify sub-issue decomposition.\n\nThis parent issue should have two sub-issues created under it.\n\nCreated: 2026-02-22T02:12:11.695Z",
132
+ "description": "Auto-generated by smoke test to verify sub-issue decomposition.\n\nThis parent issue should have two sub-issues created under it.\n\nCreated: 2026-02-22T02:38:52.132Z",
133
133
  "estimate": null,
134
134
  "state": {
135
135
  "name": "Backlog",
@@ -158,11 +158,11 @@ export const RECORDED = {
158
158
  }
159
159
  },
160
160
  "createRelation": {
161
- "id": "6db881b7-5a05-449f-85f2-3c03dbb1e3f0"
161
+ "id": "9207d26d-d65d-45a2-ac76-b9233acea181"
162
162
  },
163
163
  "subIssue1WithRelation": {
164
- "id": "5a928cd6-7dd8-4e3d-9aa0-4699ee2f3990",
165
- "identifier": "UAT-286",
164
+ "id": "69ce9a01-3bdb-4f4a-a882-e28e23d1b628",
165
+ "identifier": "UAT-321",
166
166
  "title": "[SMOKE TEST] Sub-Issue 1: Backend API",
167
167
  "description": "Implement the backend search API endpoint.\n\nGiven a search query, when the API is called, then matching results are returned.",
168
168
  "estimate": 2,
@@ -188,16 +188,16 @@ export const RECORDED = {
188
188
  },
189
189
  "project": null,
190
190
  "parent": {
191
- "id": "b2794dc5-82a1-4942-b999-36c7093b7477",
192
- "identifier": "UAT-285"
191
+ "id": "953324f8-878f-40ba-b773-8d8815ee7987",
192
+ "identifier": "UAT-320"
193
193
  },
194
194
  "relations": {
195
195
  "nodes": [
196
196
  {
197
197
  "type": "blocks",
198
198
  "relatedIssue": {
199
- "id": "b5d88918-6f3f-43d9-9e3b-e98608707efe",
200
- "identifier": "UAT-287",
199
+ "id": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
200
+ "identifier": "UAT-322",
201
201
  "title": "[SMOKE TEST] Sub-Issue 2: Frontend UI"
202
202
  }
203
203
  }
@@ -205,8 +205,8 @@ export const RECORDED = {
205
205
  }
206
206
  },
207
207
  "subIssue2WithRelation": {
208
- "id": "b5d88918-6f3f-43d9-9e3b-e98608707efe",
209
- "identifier": "UAT-287",
208
+ "id": "b39cc5eb-ec05-44aa-917b-562a290ecab6",
209
+ "identifier": "UAT-322",
210
210
  "title": "[SMOKE TEST] Sub-Issue 2: Frontend UI",
211
211
  "description": "Build the frontend search UI component.\n\nGiven the search page loads, when the user types a query, then results display in real-time.",
212
212
  "estimate": 3,
@@ -232,14 +232,14 @@ export const RECORDED = {
232
232
  {
233
233
  "body": "This thread is for an agent session with ctclaw.",
234
234
  "user": null,
235
- "createdAt": "2026-02-22T02:12:12.989Z"
235
+ "createdAt": "2026-02-22T02:38:53.699Z"
236
236
  }
237
237
  ]
238
238
  },
239
239
  "project": null,
240
240
  "parent": {
241
- "id": "b2794dc5-82a1-4942-b999-36c7093b7477",
242
- "identifier": "UAT-285"
241
+ "id": "953324f8-878f-40ba-b773-8d8815ee7987",
242
+ "identifier": "UAT-320"
243
243
  },
244
244
  "relations": {
245
245
  "nodes": []
@@ -649,7 +649,7 @@ describe("webhook scenario tests — full handler flows", () => {
649
649
 
650
650
  expect(mockRunAgent).toHaveBeenCalledOnce();
651
651
  const runArgs = mockRunAgent.mock.calls[0][0];
652
- expect(runArgs.message).toContain("Additional Guidance");
652
+ expect(runArgs.message).toContain("Workspace Guidance");
653
653
  expect(runArgs.message).toContain("Always use the main branch");
654
654
  });
655
655
 
@@ -668,11 +668,12 @@ describe("webhook scenario tests — full handler flows", () => {
668
668
  expect(mockRunAgent).toHaveBeenCalledOnce();
669
669
  const msg = mockRunAgent.mock.calls[0][0].message;
670
670
 
671
- // Guidance text should appear in the appendix section, not as the user's comment
672
- const userMsgSection = msg.split("Additional Guidance")[0];
673
- expect(userMsgSection).toContain("Please fix the routing bug");
674
- // The guidance string itself should not appear before the appendix
675
- expect(userMsgSection).not.toContain("Always use the main branch");
671
+ // Guidance text should appear in the guidance section, not as the user's latest message
672
+ expect(msg).toContain("Please fix the routing bug");
673
+ // The guidance text should be within the guidance section, not in the "Latest message" block
674
+ const latestMsgSection = msg.split("**Latest message:**")[1] ?? "";
675
+ expect(latestMsgSection).toContain("Please fix the routing bug");
676
+ expect(latestMsgSection).not.toContain("Always use the main branch");
676
677
  });
677
678
 
678
679
  it("prompted: includes guidance from promptContext", async () => {
@@ -688,7 +689,7 @@ describe("webhook scenario tests — full handler flows", () => {
688
689
  expect(mockRunAgent).toHaveBeenCalledOnce();
689
690
  const msg = mockRunAgent.mock.calls[0][0].message;
690
691
  expect(msg).toContain("Can you also fix the tests?");
691
- expect(msg).toContain("Additional Guidance");
692
+ expect(msg).toContain("Workspace Guidance");
692
693
  expect(msg).toContain("Use TypeScript strict mode");
693
694
  });
694
695
 
@@ -704,7 +705,7 @@ describe("webhook scenario tests — full handler flows", () => {
704
705
 
705
706
  expect(mockRunAgent).toHaveBeenCalledOnce();
706
707
  const msg = mockRunAgent.mock.calls[0][0].message;
707
- expect(msg).not.toContain("Additional Guidance");
708
+ expect(msg).not.toContain("Workspace Guidance");
708
709
  expect(msg).not.toContain("Should not appear in prompt");
709
710
  });
710
711
 
@@ -724,7 +725,7 @@ describe("webhook scenario tests — full handler flows", () => {
724
725
 
725
726
  expect(mockRunAgent).toHaveBeenCalledOnce();
726
727
  const msg = mockRunAgent.mock.calls[0][0].message;
727
- expect(msg).not.toContain("Additional Guidance");
728
+ expect(msg).not.toContain("Workspace Guidance");
728
729
  expect(msg).not.toContain("Should be suppressed");
729
730
  });
730
731
 
@@ -782,7 +783,7 @@ describe("webhook scenario tests — full handler flows", () => {
782
783
 
783
784
  expect(mockRunAgent).toHaveBeenCalledOnce();
784
785
  const msg = mockRunAgent.mock.calls[0][0].message;
785
- expect(msg).toContain("Additional Guidance");
786
+ expect(msg).toContain("Workspace Guidance");
786
787
  expect(msg).toContain("Cached guidance from session event");
787
788
  });
788
789
  });
package/src/infra/cli.ts CHANGED
@@ -13,6 +13,7 @@ import { validateRepoPath } from "./multi-repo.js";
13
13
  import { LINEAR_OAUTH_AUTH_URL, LINEAR_OAUTH_TOKEN_URL, LINEAR_AGENT_SCOPES } from "../api/auth.js";
14
14
  import { listWorktrees } from "./codex-worktree.js";
15
15
  import { loadPrompts, clearPromptCache } from "../pipeline/pipeline.js";
16
+ import { PROFILES_PATH, createAgentProfilesFile, loadAgentProfiles } from "./shared-profiles.js";
16
17
  import {
17
18
  formatMessage,
18
19
  parseNotificationsConfig,
@@ -213,6 +214,265 @@ export function registerCli(program: Command, api: OpenClawPluginApi): void {
213
214
  console.log();
214
215
  });
215
216
 
217
+ // --- interactive profile creation helper ---
218
+ async function createProfileInteractive(): Promise<void> {
219
+ const name = await prompt(" Agent name (lowercase, no spaces — e.g. bobbin): ");
220
+ if (!name) {
221
+ console.log(" Skipped.\n");
222
+ return;
223
+ }
224
+
225
+ const agentId = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
226
+ if (!agentId) {
227
+ console.log(" Invalid name. Skipped.\n");
228
+ return;
229
+ }
230
+
231
+ const labelDefault = agentId.charAt(0).toUpperCase() + agentId.slice(1);
232
+ const labelInput = await prompt(` Display label [${labelDefault}]: `);
233
+ const label = labelInput || labelDefault;
234
+
235
+ const aliasInput = await prompt(` Additional @mention aliases (comma-separated, or blank): `);
236
+ const extraAliases = aliasInput
237
+ ? aliasInput.split(",").map(a => a.trim().toLowerCase()).filter(Boolean)
238
+ : [];
239
+ const mentionAliases = [agentId, ...extraAliases.filter(a => a !== agentId)];
240
+
241
+ try {
242
+ createAgentProfilesFile({ agentId, label, mentionAliases });
243
+ console.log(`\n ✓ Created ${PROFILES_PATH}`);
244
+ console.log(` Agent: ${agentId} (${label})`);
245
+ console.log(` Aliases: ${mentionAliases.map(a => "@" + a).join(", ")}`);
246
+ console.log(` Default: yes`);
247
+ } catch (err) {
248
+ console.log(`\n ✗ Failed to create agent-profiles.json: ${err instanceof Error ? err.message : String(err)}`);
249
+ }
250
+ }
251
+
252
+ // --- openclaw openclaw-linear setup ---
253
+ linear
254
+ .command("setup")
255
+ .description("Guided first-time setup — creates agent profile, checks auth, provisions webhook")
256
+ .action(async () => {
257
+ const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
258
+
259
+ console.log("\nLinear Plugin Setup");
260
+ console.log("═".repeat(50));
261
+
262
+ // ---------------------------------------------------------------
263
+ // Step 1: Agent Profile
264
+ // ---------------------------------------------------------------
265
+ console.log("\nStep 1: Agent Profile");
266
+ console.log("─".repeat(50));
267
+
268
+ if (existsSync(PROFILES_PATH)) {
269
+ const profiles = loadAgentProfiles();
270
+ const count = Object.keys(profiles).length;
271
+ if (count > 0) {
272
+ const names = Object.entries(profiles)
273
+ .map(([id, p]) => `${id}${p.isDefault ? " (default)" : ""}`)
274
+ .join(", ");
275
+ console.log(` ✓ agent-profiles.json found (${count} agent${count > 1 ? "s" : ""}: ${names})`);
276
+ } else {
277
+ console.log(" ⚠ agent-profiles.json exists but has no agents.");
278
+ const fix = await prompt(" Overwrite with a new profile? [Y/n]: ");
279
+ if (fix.toLowerCase() === "n") {
280
+ console.log(" Skipped. Fix the file manually and re-run setup.\n");
281
+ } else {
282
+ await createProfileInteractive();
283
+ }
284
+ }
285
+ } else {
286
+ console.log(" No agent-profiles.json found — let's create one!\n");
287
+ console.log(" Your agent needs a name so Linear users can @mention it.");
288
+ console.log(" Example: if your agent is called \"bobbin\", users type @bobbin in comments.\n");
289
+ await createProfileInteractive();
290
+ }
291
+
292
+ // ---------------------------------------------------------------
293
+ // Step 2: Authentication
294
+ // ---------------------------------------------------------------
295
+ console.log("\nStep 2: Authentication");
296
+ console.log("─".repeat(50));
297
+
298
+ const tokenInfo = resolveLinearToken(pluginConfig);
299
+ if (tokenInfo.accessToken) {
300
+ // Verify the token works
301
+ try {
302
+ const authHeader = tokenInfo.refreshToken
303
+ ? `Bearer ${tokenInfo.accessToken}`
304
+ : tokenInfo.accessToken;
305
+ const res = await fetch(LINEAR_GRAPHQL_URL, {
306
+ method: "POST",
307
+ headers: { "Content-Type": "application/json", Authorization: authHeader },
308
+ body: JSON.stringify({ query: `{ viewer { name } organization { name } }` }),
309
+ });
310
+ if (res.ok) {
311
+ const payload = await res.json() as any;
312
+ if (payload.data?.viewer) {
313
+ console.log(` ✓ Authenticated as ${payload.data.viewer.name} (${payload.data.organization.name})`);
314
+ } else {
315
+ console.log(` ⚠ Token found but API returned errors. Run: openclaw openclaw-linear auth`);
316
+ }
317
+ } else {
318
+ console.log(` ⚠ Token found but API returned ${res.status}. Run: openclaw openclaw-linear auth`);
319
+ }
320
+ } catch {
321
+ console.log(` ⚠ Token found but API unreachable. Check network and retry.`);
322
+ }
323
+ } else {
324
+ console.log(" No Linear token found.\n");
325
+ const doAuth = await prompt(" Run OAuth authorization now? [Y/n]: ");
326
+ if (doAuth.toLowerCase() !== "n") {
327
+ const clientId = (pluginConfig?.clientId as string) ?? process.env.LINEAR_CLIENT_ID;
328
+ const clientSecret = (pluginConfig?.clientSecret as string) ?? process.env.LINEAR_CLIENT_SECRET;
329
+
330
+ if (!clientId || !clientSecret) {
331
+ console.log("\n ✗ OAuth client ID and secret not configured.");
332
+ console.log(" Set LINEAR_CLIENT_ID and LINEAR_CLIENT_SECRET env vars,");
333
+ console.log(" or add clientId/clientSecret to the plugin config in openclaw.json.");
334
+ console.log(" Then re-run: openclaw openclaw-linear setup\n");
335
+ } else {
336
+ const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT ?? "18789";
337
+ const redirectUri = (pluginConfig?.redirectUri as string)
338
+ ?? process.env.LINEAR_REDIRECT_URI
339
+ ?? `http://localhost:${gatewayPort}/linear/oauth/callback`;
340
+
341
+ const state = Math.random().toString(36).substring(7);
342
+ const authUrl = `${LINEAR_OAUTH_AUTH_URL}?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(LINEAR_AGENT_SCOPES)}&state=${state}&actor=app`;
343
+
344
+ console.log("\n Opening Linear OAuth page...\n");
345
+ console.log(` ${authUrl}\n`);
346
+ openBrowser(authUrl);
347
+
348
+ const code = await prompt(" Paste the authorization code from Linear: ");
349
+ if (code) {
350
+ try {
351
+ const response = await fetch(LINEAR_OAUTH_TOKEN_URL, {
352
+ method: "POST",
353
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
354
+ body: new URLSearchParams({
355
+ grant_type: "authorization_code",
356
+ code,
357
+ client_id: clientId,
358
+ client_secret: clientSecret,
359
+ redirect_uri: redirectUri,
360
+ }),
361
+ });
362
+
363
+ if (response.ok) {
364
+ const tokens = await response.json() as any;
365
+ let store: any = { version: 1, profiles: {} };
366
+ try {
367
+ const raw = readFileSync(AUTH_PROFILES_PATH, "utf8");
368
+ store = JSON.parse(raw);
369
+ } catch { /* fresh store */ }
370
+
371
+ store.profiles = store.profiles ?? {};
372
+ store.profiles["linear:default"] = {
373
+ type: "oauth",
374
+ provider: "linear",
375
+ accessToken: tokens.access_token,
376
+ access: tokens.access_token,
377
+ refreshToken: tokens.refresh_token ?? null,
378
+ refresh: tokens.refresh_token ?? null,
379
+ expiresAt: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : null,
380
+ expires: tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : null,
381
+ scope: tokens.scope,
382
+ };
383
+
384
+ writeFileSync(AUTH_PROFILES_PATH, JSON.stringify(store, null, 2), "utf8");
385
+ console.log(" ✓ Token saved to auth-profiles.json");
386
+ } else {
387
+ const error = await response.text();
388
+ console.log(` ✗ Token exchange failed (${response.status}): ${error}`);
389
+ }
390
+ } catch (err) {
391
+ console.log(` ✗ Token exchange error: ${err instanceof Error ? err.message : String(err)}`);
392
+ }
393
+ } else {
394
+ console.log(" Skipped. Run: openclaw openclaw-linear auth later.");
395
+ }
396
+ }
397
+ } else {
398
+ console.log(" Skipped. Run: openclaw openclaw-linear auth when ready.");
399
+ }
400
+ }
401
+
402
+ // ---------------------------------------------------------------
403
+ // Step 3: Webhook
404
+ // ---------------------------------------------------------------
405
+ console.log("\nStep 3: Webhook");
406
+ console.log("─".repeat(50));
407
+
408
+ const freshToken = resolveLinearToken(pluginConfig);
409
+ if (!freshToken.accessToken) {
410
+ console.log(" ⚠ Skipped — no auth token available. Complete Step 2 first.");
411
+ } else {
412
+ const { provisionWebhook, getWebhookStatus } = await import("./webhook-provision.js");
413
+ const linearApi = new LinearAgentApi(freshToken.accessToken, {
414
+ refreshToken: freshToken.refreshToken,
415
+ expiresAt: freshToken.expiresAt,
416
+ });
417
+
418
+ const webhookUrl = (pluginConfig?.webhookUrl as string)
419
+ ?? "https://linear.calltelemetry.com/linear/webhook";
420
+
421
+ try {
422
+ const status = await getWebhookStatus(linearApi, webhookUrl);
423
+ if (status && status.issues.length === 0) {
424
+ console.log(` ✓ Webhook already configured (${webhookUrl})`);
425
+ } else {
426
+ const result = await provisionWebhook(linearApi, webhookUrl, { allPublicTeams: true });
427
+ if (result.action === "created") {
428
+ console.log(` ✓ Webhook created (${result.webhookId})`);
429
+ } else if (result.action === "updated") {
430
+ console.log(` ✓ Webhook updated (${result.webhookId})`);
431
+ } else {
432
+ console.log(` ✓ Webhook OK (${result.webhookId})`);
433
+ }
434
+ }
435
+ } catch (err) {
436
+ console.log(` ⚠ Webhook check failed: ${err instanceof Error ? err.message : String(err)}`);
437
+ console.log(" Run: openclaw openclaw-linear webhooks setup");
438
+ }
439
+ }
440
+
441
+ // ---------------------------------------------------------------
442
+ // Step 4: Verify
443
+ // ---------------------------------------------------------------
444
+ console.log("\nStep 4: Verification");
445
+ console.log("─".repeat(50));
446
+
447
+ try {
448
+ const { runDoctor, formatReport } = await import("./doctor.js");
449
+ const report = await runDoctor({
450
+ fix: false,
451
+ json: false,
452
+ pluginConfig,
453
+ });
454
+
455
+ console.log(formatReport(report));
456
+
457
+ if (report.summary.errors === 0) {
458
+ const profiles = loadAgentProfiles();
459
+ const defaultAgent = Object.entries(profiles).find(([, p]) => p.isDefault);
460
+ const agentName = defaultAgent?.[1]?.label ?? defaultAgent?.[0] ?? "your agent";
461
+ console.log("─".repeat(50));
462
+ console.log(`Setup complete! ${agentName} is ready to receive Linear issues.`);
463
+ console.log(`\nRestart the gateway to apply changes:`);
464
+ console.log(` systemctl --user restart openclaw-gateway\n`);
465
+ } else {
466
+ console.log("─".repeat(50));
467
+ console.log("Some issues remain. Fix them and run: openclaw openclaw-linear doctor --fix\n");
468
+ process.exitCode = 1;
469
+ }
470
+ } catch (err) {
471
+ console.log(` ⚠ Doctor failed: ${err instanceof Error ? err.message : String(err)}`);
472
+ console.log(" Run: openclaw openclaw-linear doctor for details.\n");
473
+ }
474
+ });
475
+
216
476
  // --- openclaw openclaw-linear worktrees ---
217
477
  linear
218
478
  .command("worktrees")
@@ -14,6 +14,7 @@ import { loadPrompts, clearPromptCache } from "../pipeline/pipeline.js";
14
14
  import { listWorktrees } from "./codex-worktree.js";
15
15
  import { loadCodingConfig, resolveCodingBackend, type CodingBackend } from "../tools/code-tool.js";
16
16
  import { getWebhookStatus, provisionWebhook, REQUIRED_RESOURCE_TYPES } from "./webhook-provision.js";
17
+ import { createAgentProfilesFile } from "./shared-profiles.js";
17
18
 
18
19
  // ---------------------------------------------------------------------------
19
20
  // Types
@@ -227,22 +228,51 @@ export async function checkAuth(pluginConfig?: Record<string, unknown>): Promise
227
228
  // Section 2: Agent Configuration
228
229
  // ---------------------------------------------------------------------------
229
230
 
230
- export function checkAgentConfig(pluginConfig?: Record<string, unknown>): CheckResult[] {
231
+ export function checkAgentConfig(pluginConfig?: Record<string, unknown>, fix = false): CheckResult[] {
231
232
  const checks: CheckResult[] = [];
232
233
 
233
234
  // Load profiles
234
235
  let profiles: Record<string, AgentProfile>;
235
236
  try {
236
237
  if (!existsSync(AGENT_PROFILES_PATH)) {
237
- checks.push(fail(
238
- "agent-profiles.json not found",
239
- `Expected at: ${AGENT_PROFILES_PATH}`,
240
- ));
241
- return checks;
238
+ if (fix) {
239
+ try {
240
+ createAgentProfilesFile({
241
+ agentId: "my-agent",
242
+ label: "My Agent",
243
+ mentionAliases: ["my-agent"],
244
+ });
245
+ checks.push(pass('agent-profiles.json created with default "my-agent" profile (--fix)'));
246
+ checks.push(warn(
247
+ 'Customize your agent profile',
248
+ undefined,
249
+ { fix: `Edit ${AGENT_PROFILES_PATH} to set your agent's name and aliases, or run: openclaw openclaw-linear setup` },
250
+ ));
251
+ // Reload after creation
252
+ const raw = readFileSync(AGENT_PROFILES_PATH, "utf8");
253
+ profiles = JSON.parse(raw).agents ?? {};
254
+ // Continue to remaining checks below
255
+ } catch (err) {
256
+ checks.push(fail(
257
+ "Failed to create agent-profiles.json",
258
+ err instanceof Error ? err.message : String(err),
259
+ "Run: openclaw openclaw-linear setup",
260
+ ));
261
+ return checks;
262
+ }
263
+ } else {
264
+ checks.push(fail(
265
+ "agent-profiles.json not found",
266
+ `Expected at: ${AGENT_PROFILES_PATH}`,
267
+ "Run: openclaw openclaw-linear setup",
268
+ ));
269
+ return checks;
270
+ }
271
+ } else {
272
+ const raw = readFileSync(AGENT_PROFILES_PATH, "utf8");
273
+ const parsed = JSON.parse(raw);
274
+ profiles = parsed.agents ?? {};
242
275
  }
243
- const raw = readFileSync(AGENT_PROFILES_PATH, "utf8");
244
- const parsed = JSON.parse(raw);
245
- profiles = parsed.agents ?? {};
246
276
  } catch (err) {
247
277
  checks.push(fail(
248
278
  "agent-profiles.json invalid JSON",
@@ -730,7 +760,7 @@ export async function runDoctor(opts: DoctorOptions): Promise<DoctorReport> {
730
760
  sections.push({ name: "Authentication & Tokens", checks: auth.checks });
731
761
 
732
762
  // 2. Agent config
733
- sections.push({ name: "Agent Configuration", checks: checkAgentConfig(opts.pluginConfig) });
763
+ sections.push({ name: "Agent Configuration", checks: checkAgentConfig(opts.pluginConfig, opts.fix) });
734
764
 
735
765
  // 3. Coding tools
736
766
  sections.push({ name: "Coding Tools", checks: checkCodingTools(opts.pluginConfig) });
@@ -4,12 +4,18 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
4
4
  // Mocks
5
5
  // ---------------------------------------------------------------------------
6
6
 
7
- const { mockReadFileSync } = vi.hoisted(() => ({
7
+ const { mockReadFileSync, mockWriteFileSync, mockMkdirSync, mockExistsSync } = vi.hoisted(() => ({
8
8
  mockReadFileSync: vi.fn(),
9
+ mockWriteFileSync: vi.fn(),
10
+ mockMkdirSync: vi.fn(),
11
+ mockExistsSync: vi.fn().mockReturnValue(true),
9
12
  }));
10
13
 
11
14
  vi.mock("node:fs", () => ({
12
15
  readFileSync: mockReadFileSync,
16
+ writeFileSync: mockWriteFileSync,
17
+ mkdirSync: mockMkdirSync,
18
+ existsSync: mockExistsSync,
13
19
  }));
14
20
 
15
21
  // ---------------------------------------------------------------------------
@@ -21,6 +27,9 @@ import {
21
27
  buildMentionPattern,
22
28
  resolveAgentFromAlias,
23
29
  resolveDefaultAgent,
30
+ createAgentProfilesFile,
31
+ validateProfiles,
32
+ PROFILES_PATH,
24
33
  _resetProfilesCacheForTesting,
25
34
  type AgentProfile,
26
35
  } from "./shared-profiles.js";
@@ -61,6 +70,7 @@ beforeEach(() => {
61
70
  vi.clearAllMocks();
62
71
  _resetProfilesCacheForTesting();
63
72
  mockReadFileSync.mockReturnValue(PROFILES_JSON);
73
+ mockExistsSync.mockReturnValue(true);
64
74
  });
65
75
 
66
76
  afterEach(() => {
@@ -260,3 +270,115 @@ describe("resolveDefaultAgent", () => {
260
270
  expect(result).toBe("mal");
261
271
  });
262
272
  });
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // createAgentProfilesFile
276
+ // ---------------------------------------------------------------------------
277
+
278
+ describe("createAgentProfilesFile", () => {
279
+ it("writes correct JSON structure to PROFILES_PATH", () => {
280
+ createAgentProfilesFile({
281
+ agentId: "bobbin",
282
+ label: "Bobbin",
283
+ mentionAliases: ["bobbin", "bob"],
284
+ });
285
+
286
+ expect(mockMkdirSync).toHaveBeenCalledTimes(1);
287
+ expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
288
+
289
+ const [path, content] = mockWriteFileSync.mock.calls[0];
290
+ expect(path).toBe(PROFILES_PATH);
291
+
292
+ const parsed = JSON.parse(content);
293
+ expect(parsed.agents.bobbin).toEqual({
294
+ label: "Bobbin",
295
+ mission: "AI assistant for Linear issues",
296
+ isDefault: true,
297
+ mentionAliases: ["bobbin", "bob"],
298
+ });
299
+ });
300
+
301
+ it("uses custom mission when provided", () => {
302
+ createAgentProfilesFile({
303
+ agentId: "claw",
304
+ label: "The Claw",
305
+ mentionAliases: ["claw"],
306
+ mission: "Code review specialist",
307
+ });
308
+
309
+ const [, content] = mockWriteFileSync.mock.calls[0];
310
+ const parsed = JSON.parse(content);
311
+ expect(parsed.agents.claw.mission).toBe("Code review specialist");
312
+ });
313
+
314
+ it("creates parent directory recursively", () => {
315
+ createAgentProfilesFile({
316
+ agentId: "test",
317
+ label: "Test",
318
+ mentionAliases: ["test"],
319
+ });
320
+
321
+ expect(mockMkdirSync).toHaveBeenCalledWith(
322
+ expect.any(String),
323
+ { recursive: true },
324
+ );
325
+ });
326
+
327
+ it("busts the profile cache", () => {
328
+ // Load to populate cache
329
+ loadAgentProfiles();
330
+ expect(mockReadFileSync).toHaveBeenCalledTimes(1);
331
+
332
+ // Create a new profile — should bust cache
333
+ createAgentProfilesFile({
334
+ agentId: "fresh",
335
+ label: "Fresh",
336
+ mentionAliases: ["fresh"],
337
+ });
338
+
339
+ // Next load should re-read from disk (cache was busted)
340
+ loadAgentProfiles();
341
+ expect(mockReadFileSync).toHaveBeenCalledTimes(2);
342
+ });
343
+ });
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // validateProfiles
347
+ // ---------------------------------------------------------------------------
348
+
349
+ describe("validateProfiles", () => {
350
+ it("returns error when file is missing", () => {
351
+ mockExistsSync.mockReturnValue(false);
352
+
353
+ const result = validateProfiles();
354
+ expect(result).not.toBeNull();
355
+ expect(result).toContain("not found");
356
+ expect(result).toContain("openclaw openclaw-linear setup");
357
+ });
358
+
359
+ it("returns null when file is valid with agents", () => {
360
+ mockExistsSync.mockReturnValue(true);
361
+ mockReadFileSync.mockReturnValue(PROFILES_JSON);
362
+
363
+ const result = validateProfiles();
364
+ expect(result).toBeNull();
365
+ });
366
+
367
+ it("returns error when JSON is invalid", () => {
368
+ mockExistsSync.mockReturnValue(true);
369
+ mockReadFileSync.mockImplementation(() => { throw new SyntaxError("Unexpected token"); });
370
+
371
+ const result = validateProfiles();
372
+ expect(result).not.toBeNull();
373
+ expect(result).toContain("could not be parsed");
374
+ });
375
+
376
+ it("returns error when agents object is empty", () => {
377
+ mockExistsSync.mockReturnValue(true);
378
+ mockReadFileSync.mockReturnValue(JSON.stringify({ agents: {} }));
379
+
380
+ const result = validateProfiles();
381
+ expect(result).not.toBeNull();
382
+ expect(result).toContain("no agents configured");
383
+ });
384
+ });
@@ -5,8 +5,8 @@
5
5
  * resolveAgentFromAlias() implementations that were previously in
6
6
  * webhook.ts, intent-classify.ts, and tier-assess.ts.
7
7
  */
8
- import { readFileSync, existsSync } from "node:fs";
9
- import { join } from "node:path";
8
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
9
+ import { join, dirname } from "node:path";
10
10
  import { homedir } from "node:os";
11
11
 
12
12
  // ---------------------------------------------------------------------------
@@ -26,7 +26,7 @@ export interface AgentProfile {
26
26
  // Cached profile loader (5s TTL)
27
27
  // ---------------------------------------------------------------------------
28
28
 
29
- const PROFILES_PATH = join(homedir(), ".openclaw", "agent-profiles.json");
29
+ export const PROFILES_PATH = join(homedir(), ".openclaw", "agent-profiles.json");
30
30
 
31
31
  let profilesCache: { data: Record<string, AgentProfile>; loadedAt: number } | null = null;
32
32
  const PROFILES_CACHE_TTL_MS = 5_000;
@@ -137,7 +137,7 @@ export function validateProfiles(): string | null {
137
137
  `EOF\n` +
138
138
  "```\n\n" +
139
139
  `Then restart the gateway: \`systemctl --user restart openclaw-gateway\`\n\n` +
140
- `Run \`openclaw openclaw-linear doctor\` to verify your setup.`
140
+ `Or run the guided setup: \`openclaw openclaw-linear setup\``
141
141
  );
142
142
  }
143
143
 
@@ -158,13 +158,45 @@ export function validateProfiles(): string | null {
158
158
  return (
159
159
  `**Critical setup error:** \`agent-profiles.json\` has no agents configured.\n\n` +
160
160
  `Add at least one agent entry to the \`"agents"\` object in \`${PROFILES_PATH}\`.\n` +
161
- `Run \`openclaw openclaw-linear doctor\` for a guided setup check.`
161
+ `Run \`openclaw openclaw-linear setup\` for guided setup.`
162
162
  );
163
163
  }
164
164
 
165
165
  return null;
166
166
  }
167
167
 
168
+ // ---------------------------------------------------------------------------
169
+ // Profile creation (setup wizard / doctor --fix)
170
+ // ---------------------------------------------------------------------------
171
+
172
+ export interface CreateProfileOpts {
173
+ agentId: string;
174
+ label: string;
175
+ mentionAliases: string[];
176
+ mission?: string;
177
+ }
178
+
179
+ /**
180
+ * Create `agent-profiles.json` with a single default agent.
181
+ * Creates the parent directory if needed. Throws on write failure.
182
+ */
183
+ export function createAgentProfilesFile(opts: CreateProfileOpts): void {
184
+ const data = {
185
+ agents: {
186
+ [opts.agentId]: {
187
+ label: opts.label,
188
+ mission: opts.mission ?? "AI assistant for Linear issues",
189
+ isDefault: true,
190
+ mentionAliases: opts.mentionAliases,
191
+ },
192
+ },
193
+ };
194
+ mkdirSync(dirname(PROFILES_PATH), { recursive: true });
195
+ writeFileSync(PROFILES_PATH, JSON.stringify(data, null, 2) + "\n", "utf8");
196
+ // Bust the cache so the next load picks up the new file
197
+ profilesCache = null;
198
+ }
199
+
168
200
  // ---------------------------------------------------------------------------
169
201
  // Test-only: reset cache
170
202
  // ---------------------------------------------------------------------------
@@ -42,9 +42,16 @@ let _affinityTtlMs = 30 * 60_000; // 30 minutes default
42
42
  /**
43
43
  * Register the active session for an issue. Idempotent — calling again
44
44
  * for the same issue just updates the session.
45
+ *
46
+ * Also eagerly records agent affinity so that follow-up webhooks arriving
47
+ * during or after the run resolve to the correct agent — even if the
48
+ * gateway restarts before clearActiveSession is called.
45
49
  */
46
50
  export function setActiveSession(session: ActiveSession): void {
47
51
  sessions.set(session.issueId, session);
52
+ if (session.agentId) {
53
+ recordIssueAffinity(session.issueId, session.agentId);
54
+ }
48
55
  }
49
56
 
50
57
  /**
@@ -93,7 +93,7 @@ describe("extractGuidanceFromPromptContext", () => {
93
93
  describe("formatGuidanceAppendix", () => {
94
94
  it("formats guidance as appendix block", () => {
95
95
  const result = formatGuidanceAppendix("Use main branch.");
96
- expect(result).toContain("## Additional Guidance");
96
+ expect(result).toContain("## IMPORTANT — Workspace Guidance");
97
97
  expect(result).toContain("Use main branch.");
98
98
  expect(result).toMatch(/^---\n/);
99
99
  expect(result).toMatch(/\n---$/);
@@ -119,7 +119,9 @@ export function formatGuidanceAppendix(guidance: string | null): string {
119
119
  const trimmed = guidance.trim().slice(0, MAX_GUIDANCE_CHARS);
120
120
  return [
121
121
  `---`,
122
- `## Additional Guidance (from Linear workspace/team settings)`,
122
+ `## IMPORTANT — Workspace Guidance (MUST follow)`,
123
+ `The workspace owner has set the following mandatory instructions. You MUST incorporate these into your response:`,
124
+ ``,
123
125
  trimmed,
124
126
  `---`,
125
127
  ].join("\n");
@@ -405,7 +405,7 @@ describe("buildWorkerTask (additional branches)", () => {
405
405
  const { task } = buildWorkerTask(issue, "/wt/API-42", {
406
406
  guidance: "Always use TypeScript strict mode",
407
407
  });
408
- expect(task).toContain("Additional Guidance");
408
+ expect(task).toContain("Workspace Guidance");
409
409
  expect(task).toContain("Always use TypeScript strict mode");
410
410
  });
411
411
 
@@ -413,7 +413,7 @@ describe("buildWorkerTask (additional branches)", () => {
413
413
  const { task } = buildWorkerTask(issue, "/wt/API-42", {
414
414
  guidance: undefined,
415
415
  });
416
- expect(task).not.toContain("Additional Guidance");
416
+ expect(task).not.toContain("Workspace Guidance");
417
417
  });
418
418
 
419
419
  it("uses undefined description as (no description)", () => {
@@ -443,7 +443,7 @@ describe("buildAuditTask (additional branches)", () => {
443
443
  const { task } = buildAuditTask(issue, "/wt/API-99", undefined, {
444
444
  guidance: "Focus on security",
445
445
  });
446
- expect(task).toContain("Additional Guidance");
446
+ expect(task).toContain("Workspace Guidance");
447
447
  expect(task).toContain("Focus on security");
448
448
  });
449
449
 
@@ -451,7 +451,7 @@ describe("buildAuditTask (additional branches)", () => {
451
451
  const { task } = buildAuditTask(issue, "/wt/API-99", undefined, {
452
452
  guidance: undefined,
453
453
  });
454
- expect(task).not.toContain("Additional Guidance");
454
+ expect(task).not.toContain("Workspace Guidance");
455
455
  });
456
456
 
457
457
  it("handles undefined description", () => {
@@ -197,7 +197,7 @@ export function buildWorkerTask(
197
197
  attempt: String(opts?.attempt ?? 0),
198
198
  gaps: opts?.gaps?.length ? "- " + opts.gaps.join("\n- ") : "",
199
199
  guidance: opts?.guidance
200
- ? `\n---\n## Additional Guidance (from Linear workspace/team settings)\n${opts.guidance.slice(0, 2000)}\n---`
200
+ ? `\n---\n## IMPORTANT — Workspace Guidance (MUST follow)\nThe workspace owner has set the following mandatory instructions. You MUST incorporate these into your response:\n\n${opts.guidance.slice(0, 2000)}\n---`
201
201
  : "",
202
202
  };
203
203
 
@@ -231,7 +231,7 @@ export function buildAuditTask(
231
231
  attempt: "0",
232
232
  gaps: "",
233
233
  guidance: opts?.guidance
234
- ? `\n---\n## Additional Guidance (from Linear workspace/team settings)\n${opts.guidance.slice(0, 2000)}\n---`
234
+ ? `\n---\n## IMPORTANT — Workspace Guidance (MUST follow)\nThe workspace owner has set the following mandatory instructions. You MUST incorporate these into your response:\n\n${opts.guidance.slice(0, 2000)}\n---`
235
235
  : "",
236
236
  };
237
237
 
@@ -457,12 +457,17 @@ export async function handleLinearWebhook(
457
457
  ? [`**Your role:** Orchestrator with full Linear access. You can update issue fields, change status, and dispatch work via \`code_run\`. Do NOT post comments yourself — the handler posts your text output.`]
458
458
  : [`**Your role:** You are the dispatcher. For any coding or implementation work, use \`code_run\` to dispatch it. Workers return text output. You summarize results. You do NOT update issue status or post comments via linear_issues — the audit system handles lifecycle transitions.`];
459
459
 
460
+ if (guidanceAppendix) {
461
+ api.logger.info(`Guidance injected (${guidanceCtx.source}): ${guidanceCtx.guidance?.slice(0, 120)}...`);
462
+ }
463
+
460
464
  const message = [
461
465
  `You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
462
466
  ``,
463
467
  ...toolAccessLines,
464
468
  ``,
465
469
  ...roleLines,
470
+ guidanceAppendix ? `\n${guidanceAppendix}` : "",
466
471
  ``,
467
472
  `## Issue: ${issueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
468
473
  `**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
@@ -473,17 +478,16 @@ export async function handleLinearWebhook(
473
478
  userMessage ? `\n**Latest message:**\n> ${userMessage}` : "",
474
479
  ``,
475
480
  `Respond to the user's request. For work requests, dispatch via \`code_run\` and summarize the result. Be concise and action-oriented.`,
476
- guidanceAppendix,
477
481
  ].filter(Boolean).join("\n");
478
482
 
479
483
  // Run agent directly (non-blocking)
480
484
  activeRuns.add(issue.id);
481
485
  void (async () => {
482
486
  const profiles = loadAgentProfiles();
483
- const defaultProfile = Object.entries(profiles).find(([, p]) => p.isDefault);
484
- const label = defaultProfile?.[1]?.label ?? profiles[agentId]?.label ?? agentId;
487
+ const label = profiles[agentId]?.label ?? agentId;
485
488
 
486
489
  // Register active session for tool resolution (code_run, etc.)
490
+ // Also eagerly records affinity so follow-ups route to the same agent.
487
491
  setActiveSession({
488
492
  agentSessionId: session.id,
489
493
  issueIdentifier: enrichedIssue?.identifier ?? issue.identifier ?? issue.id,
@@ -527,7 +531,7 @@ export async function handleLinearWebhook(
527
531
  }).then(() => true).catch(() => false);
528
532
 
529
533
  if (!emitted) {
530
- const avatarUrl = defaultProfile?.[1]?.avatarUrl ?? profiles[agentId]?.avatarUrl;
534
+ const avatarUrl = profiles[agentId]?.avatarUrl;
531
535
  const agentOpts = avatarUrl
532
536
  ? { createAsUser: label, displayIconUrl: avatarUrl }
533
537
  : undefined;
@@ -644,8 +648,7 @@ export async function handleLinearWebhook(
644
648
  activeRuns.add(issue.id);
645
649
  void (async () => {
646
650
  const profiles = loadAgentProfiles();
647
- const defaultProfile = Object.entries(profiles).find(([, p]) => p.isDefault);
648
- const label = defaultProfile?.[1]?.label ?? profiles[agentId]?.label ?? agentId;
651
+ const label = profiles[agentId]?.label ?? agentId;
649
652
 
650
653
  // Fetch full issue details for context
651
654
  let enrichedIssue: any = issue;
@@ -697,12 +700,17 @@ export async function handleLinearWebhook(
697
700
  ? [`**Your role:** Orchestrator with full Linear access. You can update issue fields, change status, and dispatch work via \`code_run\`. Do NOT post comments yourself — the handler posts your text output.`]
698
701
  : [`**Your role:** Dispatcher. For work requests, use \`code_run\`. You do NOT update issue status — the audit system handles lifecycle.`];
699
702
 
703
+ if (followUpGuidanceAppendix) {
704
+ api.logger.info(`Follow-up guidance injected: ${(guidanceCtxPrompted.guidance ?? "cached").slice(0, 120)}...`);
705
+ }
706
+
700
707
  const message = [
701
708
  `You are an orchestrator responding in a Linear issue session. Your text output will be posted as activities visible to the user.`,
702
709
  ``,
703
710
  ...followUpToolAccessLines,
704
711
  ``,
705
712
  ...followUpRoleLines,
713
+ followUpGuidanceAppendix ? `\n${followUpGuidanceAppendix}` : "",
706
714
  ``,
707
715
  `## Issue: ${followUpIssueRef} — ${enrichedIssue?.title ?? issue.title ?? "(untitled)"}`,
708
716
  `**Status:** ${enrichedIssue?.state?.name ?? "Unknown"} | **Assignee:** ${enrichedIssue?.assignee?.name ?? "Unassigned"}`,
@@ -713,7 +721,6 @@ export async function handleLinearWebhook(
713
721
  `\n**User's follow-up message:**\n> ${userMessage}`,
714
722
  ``,
715
723
  `Respond to the user's follow-up. For work requests, dispatch via \`code_run\`. Be concise and action-oriented.`,
716
- followUpGuidanceAppendix,
717
724
  ].filter(Boolean).join("\n");
718
725
 
719
726
  setActiveSession({
@@ -756,7 +763,7 @@ export async function handleLinearWebhook(
756
763
  }).then(() => true).catch(() => false);
757
764
 
758
765
  if (!emitted) {
759
- const avatarUrl = defaultProfile?.[1]?.avatarUrl ?? profiles[agentId]?.avatarUrl;
766
+ const avatarUrl = profiles[agentId]?.avatarUrl;
760
767
  const agentOpts = avatarUrl
761
768
  ? { createAsUser: label, displayIconUrl: avatarUrl }
762
769
  : undefined;
@@ -1142,9 +1149,8 @@ export async function handleLinearWebhook(
1142
1149
  // Dispatch triage (non-blocking)
1143
1150
  void (async () => {
1144
1151
  const profiles = loadAgentProfiles();
1145
- const defaultProfile = Object.entries(profiles).find(([, p]) => p.isDefault);
1146
- const label = defaultProfile?.[1]?.label ?? profiles[agentId]?.label ?? agentId;
1147
- const avatarUrl = defaultProfile?.[1]?.avatarUrl ?? profiles[agentId]?.avatarUrl;
1152
+ const label = profiles[agentId]?.label ?? agentId;
1153
+ const avatarUrl = profiles[agentId]?.avatarUrl;
1148
1154
  let agentSessionId: string | null = null;
1149
1155
 
1150
1156
  try {