@ctliz/agent-intercom-codex 0.12.1 → 0.13.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/dist/coi.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.12.1 target=coi sourceSha256=9e94589286ec909b8fd50e96f38fa52f7c21424081d18e8155e01cd3bffde8fb\n");
2
+ process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.13.0 target=coi sourceSha256=04013522326f3042c72f6454a9c79e26d2d1c8de7af57769ed890e50fca514e3\n");
3
3
 
4
4
  // codex/coi.ts
5
5
  import { once as once2 } from "node:events";
6
6
  import { spawn as spawn4, spawnSync as spawnSync2 } from "node:child_process";
7
7
  import { createHash as createHash4 } from "node:crypto";
8
- import { existsSync as existsSync6, readdirSync, rmSync, statSync } from "node:fs";
9
- import { basename as basename2, join as join8, resolve as resolve4 } from "node:path";
10
- import { setTimeout as delay2 } from "node:timers/promises";
8
+ import { existsSync as existsSync6, readdirSync, rmSync as rmSync2, statSync } from "node:fs";
9
+ import { basename as basename2, join as join10, resolve as resolve4 } from "node:path";
10
+ import { setTimeout as delay3 } from "node:timers/promises";
11
11
  import { types as nodeUtilTypes4 } from "node:util";
12
12
 
13
13
  // codex/bridge-daemon.ts
@@ -1575,6 +1575,7 @@ function isMessage(value) {
1575
1575
  if (typeof content.text !== "string") {
1576
1576
  return false;
1577
1577
  }
1578
+ if (content.team !== void 0 && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
1578
1579
  return content.attachments === void 0 || Array.isArray(content.attachments) && content.attachments.every(isAttachment);
1579
1580
  }
1580
1581
  function isSessionInfo(value) {
@@ -2165,6 +2166,7 @@ var IntercomClient = class extends EventEmitter2 {
2165
2166
  expectsReply: options.expectsReply,
2166
2167
  content: {
2167
2168
  text: options.text,
2169
+ ...options.team === void 0 ? {} : { team: options.team },
2168
2170
  attachments: options.attachments
2169
2171
  }
2170
2172
  };
@@ -3079,6 +3081,213 @@ function formatIntercomTeam(team) {
3079
3081
  return lines.join("\n");
3080
3082
  }
3081
3083
 
3084
+ // codex/named-teams.ts
3085
+ import { randomBytes as randomBytes2 } from "node:crypto";
3086
+ import { readFileSync as readFileSync8 } from "node:fs";
3087
+ import { join as join8 } from "node:path";
3088
+ var NAMED_TEAMS_FILE = "named-teams.json";
3089
+ var NAMED_TEAMS_VERSION = 1;
3090
+ var NAMED_TEAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
3091
+ function isNamedTeamScope(value) {
3092
+ if (value.length !== 48) return false;
3093
+ for (const char of value) {
3094
+ if (!(char >= "0" && char <= "9" || char >= "a" && char <= "f")) return false;
3095
+ }
3096
+ return true;
3097
+ }
3098
+ function teamsFilePath(agentDir) {
3099
+ return join8(getIntercomDirPath(agentDir ?? getAgentDirPath()), NAMED_TEAMS_FILE);
3100
+ }
3101
+ function genericReadError() {
3102
+ return new Error("Could not read the local named-team list.");
3103
+ }
3104
+ function genericWriteError() {
3105
+ return new Error("Could not create that named team.");
3106
+ }
3107
+ function isPlainObject(value) {
3108
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3109
+ }
3110
+ function parseStoredTeam(value) {
3111
+ if (!isPlainObject(value)) return void 0;
3112
+ if (typeof value.name !== "string" || !NAMED_TEAM_NAME_PATTERN.test(value.name)) return void 0;
3113
+ if (typeof value.scopeId !== "string" || !isNamedTeamScope(value.scopeId)) return void 0;
3114
+ if (typeof value.managerSessionId !== "string" || !value.managerSessionId.trim()) return void 0;
3115
+ if (value.managerSessionId !== value.managerSessionId.trim() || /[\u0000-\u001f\u007f]/.test(value.managerSessionId)) {
3116
+ return void 0;
3117
+ }
3118
+ if (typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt <= 0) {
3119
+ return void 0;
3120
+ }
3121
+ if (value.memberSessionIds !== void 0 && (!Array.isArray(value.memberSessionIds) || !value.memberSessionIds.every((id) => typeof id === "string" && id.trim() === id && id.length > 0 && !/[\u0000-\u001f\u007f]/.test(id)) || new Set(value.memberSessionIds).size !== value.memberSessionIds.length || !value.memberSessionIds.includes(value.managerSessionId))) return void 0;
3122
+ if (value.work !== void 0 && (typeof value.work !== "string" || !value.work.trim() || value.work.length > 2e3)) return void 0;
3123
+ return {
3124
+ name: value.name,
3125
+ scopeId: value.scopeId,
3126
+ managerSessionId: value.managerSessionId,
3127
+ createdAt: value.createdAt,
3128
+ ...value.memberSessionIds === void 0 ? {} : { memberSessionIds: value.memberSessionIds },
3129
+ ...value.work === void 0 ? {} : { work: value.work }
3130
+ };
3131
+ }
3132
+ function parseTeamName(raw) {
3133
+ const name = raw.trim();
3134
+ if (!name || name.includes(" ") || !NAMED_TEAM_NAME_PATTERN.test(name)) {
3135
+ throw new Error("Team names start with a letter and may include letters, numbers, hyphens, or underscores (max 32).");
3136
+ }
3137
+ return name;
3138
+ }
3139
+ function generateNamedTeamScope(existing = []) {
3140
+ const taken = new Set(existing);
3141
+ for (let attempt = 0; attempt < 8; attempt += 1) {
3142
+ const scopeId = randomBytes2(24).toString("hex");
3143
+ if (!taken.has(scopeId)) return scopeId;
3144
+ }
3145
+ throw genericWriteError();
3146
+ }
3147
+ function listNamedTeams(agentDir) {
3148
+ let raw;
3149
+ try {
3150
+ raw = readFileSync8(teamsFilePath(agentDir), "utf8");
3151
+ } catch (error) {
3152
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return [];
3153
+ throw genericReadError();
3154
+ }
3155
+ let parsed;
3156
+ try {
3157
+ parsed = JSON.parse(raw);
3158
+ } catch {
3159
+ throw genericReadError();
3160
+ }
3161
+ if (!isPlainObject(parsed) || parsed.version !== NAMED_TEAMS_VERSION || !Array.isArray(parsed.teams)) {
3162
+ throw genericReadError();
3163
+ }
3164
+ const teams = [];
3165
+ const names = /* @__PURE__ */ new Set();
3166
+ const scopes = /* @__PURE__ */ new Set();
3167
+ for (const entry of parsed.teams) {
3168
+ const team = parseStoredTeam(entry);
3169
+ if (!team || names.has(team.name) || scopes.has(team.scopeId)) throw genericReadError();
3170
+ names.add(team.name);
3171
+ scopes.add(team.scopeId);
3172
+ teams.push(team);
3173
+ }
3174
+ return teams;
3175
+ }
3176
+ function formatJoinableNamedTeamList(teams) {
3177
+ if (teams.length === 0) {
3178
+ return 'No joinable named teams found.\nCreate one with intercom_join({ name: "billing", create: true }).';
3179
+ }
3180
+ return [
3181
+ "Joinable named teams:",
3182
+ ...teams.map((team, index) => ` ${index + 1}) ${team.name}`)
3183
+ ].join("\n");
3184
+ }
3185
+
3186
+ // codex/named-team-membership.ts
3187
+ import { mkdirSync as mkdirSync5, rmSync } from "node:fs";
3188
+ import { join as join9 } from "node:path";
3189
+ import { setTimeout as delay2 } from "node:timers/promises";
3190
+ function namedTeamMemberIds(team) {
3191
+ return team.memberSessionIds ?? [team.managerSessionId];
3192
+ }
3193
+ function sessionNamedTeams(sessionId, agentDir) {
3194
+ return listNamedTeams(agentDir).filter((team) => namedTeamMemberIds(team).includes(sessionId));
3195
+ }
3196
+ async function appendNamedTeamMembership(input) {
3197
+ const name = parseTeamName(input.name);
3198
+ const memberIds = [.../* @__PURE__ */ new Set([input.selfId, ...input.members ?? []])];
3199
+ if (memberIds.some((id) => !id || id.trim() !== id || /[\u0000-\u001f\u007f]/.test(id))) {
3200
+ throw new Error("Invalid team member session ID");
3201
+ }
3202
+ if (input.work !== void 0 && (!input.work.trim() || input.work.length > 2e3)) {
3203
+ throw new Error("Work must be a non-empty task description (max 2000 characters)");
3204
+ }
3205
+ const dir = getIntercomDirPath(input.agentDir ?? getAgentDirPath());
3206
+ ensureIntercomRuntimeDir(dir);
3207
+ const lock = join9(dir, "named-teams.lock");
3208
+ const deadline = Date.now() + 5e3;
3209
+ for (; ; ) {
3210
+ try {
3211
+ mkdirSync5(lock, { mode: 448 });
3212
+ break;
3213
+ } catch (error) {
3214
+ if (error.code !== "EEXIST") throw error;
3215
+ if (Date.now() >= deadline) throw new Error("Team registry is busy; retry. If a writer crashed, remove the stale named-teams.lock directory after verifying no writer is active.");
3216
+ await delay2(25);
3217
+ }
3218
+ }
3219
+ try {
3220
+ const teams = listNamedTeams(input.agentDir);
3221
+ let team = teams.find((entry) => entry.name === name);
3222
+ if (input.create) {
3223
+ if (team) throw new Error(`A named team called ${name} already exists; join it instead.`);
3224
+ team = { name, scopeId: generateNamedTeamScope(teams.map((entry) => entry.scopeId)), managerSessionId: input.selfId, createdAt: Date.now() };
3225
+ teams.push(team);
3226
+ }
3227
+ if (!team) throw new Error(`Team "${name}" does not exist; create it explicitly.`);
3228
+ if (memberIds.some((id) => id !== input.selfId) && team.managerSessionId !== input.selfId) {
3229
+ throw new Error(`Only the manager of team "${name}" may add other sessions`);
3230
+ }
3231
+ if (input.work !== void 0 && !input.create) {
3232
+ throw new Error("Work is set when creating a team; use a new team for a different task");
3233
+ }
3234
+ const updated = {
3235
+ ...team,
3236
+ memberSessionIds: [.../* @__PURE__ */ new Set([...namedTeamMemberIds(team), ...memberIds])],
3237
+ ...input.work === void 0 ? {} : { work: input.work.trim() }
3238
+ };
3239
+ writeDurableJson(join9(dir, NAMED_TEAMS_FILE), {
3240
+ version: NAMED_TEAMS_VERSION,
3241
+ teams: teams.map((entry) => entry.name === name ? updated : entry)
3242
+ });
3243
+ return updated;
3244
+ } finally {
3245
+ rmSync(lock, { recursive: true });
3246
+ }
3247
+ }
3248
+ function requireNamedTeamMembers(name, selfId, peerId, agentDir) {
3249
+ const team = listNamedTeams(agentDir).find((entry) => entry.name === name);
3250
+ if (!team) throw new Error(`Unknown team "${name}"`);
3251
+ const members = namedTeamMemberIds(team);
3252
+ if (!members.includes(selfId) || !members.includes(peerId)) {
3253
+ throw new Error(`Both sessions must belong to team "${name}" before messaging`);
3254
+ }
3255
+ return team;
3256
+ }
3257
+ function resolveNamedMessageTeam(selfId, peerId, requested, agentDir) {
3258
+ if (requested !== void 0) return requireNamedTeamMembers(requested, selfId, peerId, agentDir).name;
3259
+ const mine = sessionNamedTeams(selfId, agentDir);
3260
+ const shared = mine.filter((team) => namedTeamMemberIds(team).includes(peerId));
3261
+ if (shared.length === 1) return shared[0].name;
3262
+ if (shared.length > 1) throw new Error("Multiple shared teams; specify `team` for this task");
3263
+ return void 0;
3264
+ }
3265
+ function namedTeamRoster(team, selfId, sessions) {
3266
+ return {
3267
+ name: team.name,
3268
+ ...team.work ? { work: team.work } : {},
3269
+ self: { id: selfId, isManager: selfId === team.managerSessionId },
3270
+ manager: { target: team.managerSessionId, connected: sessions.some((entry) => entry.id === team.managerSessionId) },
3271
+ members: namedTeamMemberIds(team).map((id) => ({
3272
+ id,
3273
+ target: id,
3274
+ name: sessions.find((entry) => entry.id === id)?.name,
3275
+ role: id === team.managerSessionId ? "manager" : "member",
3276
+ connected: sessions.some((entry) => entry.id === id)
3277
+ }))
3278
+ };
3279
+ }
3280
+ function formatNamedTeamRoster(team) {
3281
+ return [
3282
+ `Team: ${team.name}`,
3283
+ ...team.work ? [`Work: ${team.work}`] : [],
3284
+ `You: ${team.self.id}${team.self.isManager ? " [manager]" : ""}`,
3285
+ `Manager: ${team.manager.target}${team.manager.connected ? "" : " [offline]"}`,
3286
+ "Members:",
3287
+ ...team.members.map((member) => `- ${member.name || member.id} (${member.id}) [${member.role}]${member.connected ? "" : " [offline]"}`)
3288
+ ].join("\n");
3289
+ }
3290
+
3082
3291
  // codex/runtime.ts
3083
3292
  function formatAttachments(attachments) {
3084
3293
  if (!attachments?.length) return "";
@@ -3134,10 +3343,20 @@ function formatSessionList(sessions, currentSessionId, currentCwd) {
3134
3343
  }).join("\n");
3135
3344
  }
3136
3345
 
3346
+ // codex/team-guidance.ts
3347
+ var TASK_TEAM_GUIDANCE = `Intercom task-team rules:
3348
+ - When the user delegates to named peers and this task has no approved team, ask once whether to form a team with you and those peers. Wait for approval before creating a team or adding peers. An explicit create/join request is approval; never ask again for an approved task or inbound team message.
3349
+ - After approval, discover the connected peers, then intercom_join({ name: "launch", create: true, members: ["front", "writer"], work: "Current task" }) adds everyone in one call. Do not ask the user to join each terminal manually.
3350
+ - Membership is additive: joining another team preserves previous teams. Reuse the approved team for the same task; a different task may need a different team.
3351
+ - Initial contact without a shared team is allowed: omit team for an ungrouped direct message. Unrelated team memberships must not prevent contact or silently create teams.
3352
+ - Include team on task sends/asks, especially when peers share multiple teams. Replies inherit the original message's team via askId or contextId from intercom_pending; never override it using a current team or mix contexts from different tasks.
3353
+ - Use intercom_send for assignments, progress/status requests, notifications and follow-ups. Use intercom_ask only when your next step genuinely depends on the answer; keep only one unresolved ask per recipient.`;
3354
+
3137
3355
  // codex/bridge-daemon.ts
3138
3356
  var APPROVED_INTERCOM_TOOLS = /* @__PURE__ */ new Set([
3139
3357
  "intercom_whoami",
3140
3358
  "intercom_team",
3359
+ "intercom_join",
3141
3360
  "intercom_status",
3142
3361
  "intercom_list",
3143
3362
  "intercom_set_summary",
@@ -3171,6 +3390,7 @@ ${agent.instructions}` : "";
3171
3390
  return [
3172
3391
  `Intercom message for ${agent.name}.`,
3173
3392
  `From: ${formatSessionDisplay(from)} (${from.id})`,
3393
+ ...message.content.team ? [`[Team: ${message.content.team}]`] : [],
3174
3394
  `Message id: ${message.id}`,
3175
3395
  "",
3176
3396
  message.content.text,
@@ -3266,7 +3486,7 @@ function intercomSendFromArgs(rawArgs) {
3266
3486
  } catch {
3267
3487
  return null;
3268
3488
  }
3269
- return typeof args.to === "string" && typeof args.message === "string" ? { to: args.to, message: args.message } : null;
3489
+ return typeof args.to === "string" && typeof args.message === "string" ? { to: args.to, message: args.message, ...typeof args.team === "string" ? { team: args.team } : {} } : null;
3270
3490
  }
3271
3491
  function getCompletedIntercomSend(params) {
3272
3492
  if (!params || typeof params !== "object") return null;
@@ -3294,7 +3514,7 @@ function asOptionalPositiveInteger(value, name) {
3294
3514
  return validateAskTimeoutMs(value, name);
3295
3515
  }
3296
3516
  function normalizeToolName(name) {
3297
- const mcpMatch = name.match(/(?:^|__|\.)intercom_(whoami|status|list|set_summary|send|ask|pending|reply)$/);
3517
+ const mcpMatch = name.match(/(?:^|__|\.)intercom_(whoami|team|join|status|list|set_summary|send|ask|pending|reply)$/);
3298
3518
  if (mcpMatch) return `intercom_${mcpMatch[1]}`;
3299
3519
  return name;
3300
3520
  }
@@ -3417,7 +3637,7 @@ var VirtualCodexAgent = class {
3417
3637
  }
3418
3638
  scheduleReconnect() {
3419
3639
  if (!this.reconnectEnabled || this.reconnectTimer) return;
3420
- const delay3 = this.reconnectDelays[Math.min(this.reconnectAttempt, this.reconnectDelays.length - 1)];
3640
+ const delay4 = this.reconnectDelays[Math.min(this.reconnectAttempt, this.reconnectDelays.length - 1)];
3421
3641
  this.reconnectTimer = setTimeout(() => {
3422
3642
  this.reconnectTimer = null;
3423
3643
  void this.connectIntercom().then(() => {
@@ -3431,7 +3651,7 @@ var VirtualCodexAgent = class {
3431
3651
  `);
3432
3652
  this.scheduleReconnect();
3433
3653
  });
3434
- }, delay3);
3654
+ }, delay4);
3435
3655
  this.reconnectTimer.unref?.();
3436
3656
  }
3437
3657
  clearReconnectTimer() {
@@ -3470,6 +3690,14 @@ var VirtualCodexAgent = class {
3470
3690
  onNotification(message) {
3471
3691
  const threadId = getNotificationThreadId(message.params);
3472
3692
  if (!threadId || threadId !== this.threadId) return;
3693
+ if (message.method === "thread/name/updated" && isRecord2(message.params)) {
3694
+ const name = message.params.threadName;
3695
+ if (typeof name === "string" && name.trim() && name !== this.agent.name) {
3696
+ this.agent.name = name;
3697
+ this.client.updatePresence({ name });
3698
+ }
3699
+ return;
3700
+ }
3473
3701
  if (message.method === "error") {
3474
3702
  const params = isRecord2(message.params) ? message.params : {};
3475
3703
  const detail = isRecord2(params.error) && typeof params.error.message === "string" ? params.error.message : "Codex turn error";
@@ -3516,13 +3744,19 @@ var VirtualCodexAgent = class {
3516
3744
  if (this.threadId) {
3517
3745
  try {
3518
3746
  const sandbox2 = bridgeAgentSandboxMode(this.agent);
3519
- await this.app.request("thread/resume", {
3747
+ const result2 = await this.app.request("thread/resume", {
3520
3748
  threadId: this.threadId,
3521
3749
  cwd: this.agent.cwd,
3522
3750
  model: this.agent.model ?? null,
3523
3751
  approvalPolicy: bridgeAgentApprovalPolicy(this.agent),
3524
3752
  sandbox: sandbox2
3525
3753
  });
3754
+ if (isRecord2(result2) && isRecord2(result2.thread)) {
3755
+ this.onNotification({ method: "thread/name/updated", params: {
3756
+ threadId: this.threadId,
3757
+ threadName: result2.thread.name
3758
+ } });
3759
+ }
3526
3760
  return this.threadId;
3527
3761
  } catch {
3528
3762
  this.threadId = null;
@@ -3535,7 +3769,7 @@ var VirtualCodexAgent = class {
3535
3769
  approvalPolicy: bridgeAgentApprovalPolicy(this.agent),
3536
3770
  sandbox,
3537
3771
  serviceName: "codex-intercom",
3538
- developerInstructions: this.agent.instructions ?? null,
3772
+ developerInstructions: [this.agent.instructions, TASK_TEAM_GUIDANCE].filter(Boolean).join("\n\n"),
3539
3773
  threadSource: "cli"
3540
3774
  });
3541
3775
  this.threadId = getThreadId(result);
@@ -3548,7 +3782,7 @@ var VirtualCodexAgent = class {
3548
3782
  routeMessage(from, message) {
3549
3783
  const toolWaiter = this.toolReplyWaiters.get(message.replyTo ?? "");
3550
3784
  if (toolWaiter) {
3551
- if (from.id === toolWaiter.from) {
3785
+ if (from.id === toolWaiter.from && message.content.team === toolWaiter.team) {
3552
3786
  this.toolReplyWaiters.delete(message.replyTo ?? "");
3553
3787
  clearTimeout(toolWaiter.timeout);
3554
3788
  toolWaiter.cleanup?.();
@@ -3594,7 +3828,7 @@ var VirtualCodexAgent = class {
3594
3828
  this.waiters.delete(turnId);
3595
3829
  const reply = this.finalMessages.get(turnId)?.trim() || "Codex turn completed without a final message.";
3596
3830
  for (const waiter of waiters) {
3597
- await this.client.send(waiter.from.id, { text: reply, replyTo: waiter.message.id }).catch((error) => {
3831
+ await this.client.send(waiter.from.id, { text: reply, replyTo: waiter.message.id, team: waiter.message.content.team }).catch((error) => {
3598
3832
  process.stderr.write(`reply failed for ${this.agent.id}: ${error instanceof Error ? error.message : String(error)}
3599
3833
  `);
3600
3834
  });
@@ -3606,12 +3840,16 @@ var VirtualCodexAgent = class {
3606
3840
  const lowerTo = send.to.toLowerCase();
3607
3841
  const remaining = [];
3608
3842
  for (const waiter of waiters) {
3843
+ if (send.team !== void 0 && send.team !== waiter.message.content.team) {
3844
+ remaining.push(waiter);
3845
+ continue;
3846
+ }
3609
3847
  const matchesSender = send.to === waiter.from.id || waiter.from.id.startsWith(send.to) || waiter.from.name?.toLowerCase() === lowerTo;
3610
3848
  if (!matchesSender) {
3611
3849
  remaining.push(waiter);
3612
3850
  continue;
3613
3851
  }
3614
- await this.client.send(waiter.from.id, { text: send.message, replyTo: waiter.message.id }).catch((error) => {
3852
+ await this.client.send(waiter.from.id, { text: send.message, replyTo: waiter.message.id, team: waiter.message.content.team }).catch((error) => {
3615
3853
  remaining.push(waiter);
3616
3854
  process.stderr.write(`reply failed for ${this.agent.id}: ${error instanceof Error ? error.message : String(error)}
3617
3855
  `);
@@ -3676,8 +3914,32 @@ name: ${this.agent.name}
3676
3914
  cwd: ${this.agent.cwd}`,
3677
3915
  { session_id: this.agent.id, name: this.agent.name, cwd: this.agent.cwd, model: this.agent.model ?? "codex-app-server" }
3678
3916
  );
3917
+ case "intercom_join": {
3918
+ if (args.name === void 0) {
3919
+ if (args.create || args.members !== void 0 || args.work !== void 0) throw new Error("Creating or extending a team requires a name");
3920
+ return textToolResult(formatJoinableNamedTeamList(listNamedTeams()));
3921
+ }
3922
+ if (args.members !== void 0 && !Array.isArray(args.members)) throw new Error("members must be an array");
3923
+ const sessions = await this.client.listSessions();
3924
+ const members = Array.from(args.members ?? [], (member) => {
3925
+ const id = resolveSessionTarget(sessions, asString(member, "member"));
3926
+ if (!id) throw new Error("Team member is not connected");
3927
+ return id;
3928
+ });
3929
+ const team = await appendNamedTeamMembership({ name: asString(args.name, "name"), selfId: this.agent.id, members, create: args.create === true, ...args.work === void 0 ? {} : { work: asString(args.work, "work") } });
3930
+ const roster = namedTeamRoster(team, this.agent.id, sessions);
3931
+ return textToolResult(formatNamedTeamRoster(roster), { ok: true, team: team.name, roster });
3932
+ }
3679
3933
  case "intercom_team": {
3680
3934
  const sessions = await this.client.listSessions();
3935
+ const mine = sessionNamedTeams(this.agent.id);
3936
+ const requested = args.team === void 0 ? void 0 : asString(args.team, "team");
3937
+ const selected = requested ? mine.filter((entry) => entry.name === requested) : mine;
3938
+ if (requested && !selected.length) throw new Error("You do not belong to that team");
3939
+ if (selected.length) {
3940
+ const teams = selected.map((entry) => namedTeamRoster(entry, this.agent.id, sessions));
3941
+ return textToolResult(teams.map(formatNamedTeamRoster).join("\n\n"), { teams });
3942
+ }
3681
3943
  const team = await resolveIntercomTeam({ selfId: this.agent.id, sessions });
3682
3944
  return textToolResult(formatIntercomTeam(team), team);
3683
3945
  }
@@ -3706,7 +3968,8 @@ Active sessions: ${sessions.length}`,
3706
3968
  const to = asString(args.to, "to");
3707
3969
  const message = asString(args.message, "message");
3708
3970
  const sendTo = await this.resolveTarget(to);
3709
- const result = await this.client.send(sendTo, { text: message });
3971
+ const team = resolveNamedMessageTeam(this.agent.id, sendTo, args.team === void 0 ? void 0 : asString(args.team, "team"));
3972
+ const result = await this.client.send(sendTo, { text: message, team });
3710
3973
  if (!result.delivered) {
3711
3974
  return textToolResult(`Message to "${to}" was not delivered: ${result.reason ?? "Session may not exist or has disconnected."}`, { ok: false, message_id: result.id, reason: result.reason }, true);
3712
3975
  }
@@ -3719,10 +3982,11 @@ Active sessions: ${sessions.length}`,
3719
3982
  const message = asString(args.message, "message");
3720
3983
  const timeoutMs = asOptionalPositiveInteger(args.timeout_ms, "timeout_ms") ?? DEFAULT_ASK_TIMEOUT_MS;
3721
3984
  const sendTo = await this.resolveTarget(to);
3985
+ const team = resolveNamedMessageTeam(this.agent.id, sendTo, args.team === void 0 ? void 0 : asString(args.team, "team"));
3722
3986
  const questionId = randomUUID4();
3723
- const replyPromise = this.waitForToolReply(sendTo, questionId, timeoutMs, signal);
3987
+ const replyPromise = this.waitForToolReply(sendTo, questionId, timeoutMs, signal, team);
3724
3988
  void replyPromise.catch(() => void 0);
3725
- const result = await this.client.send(sendTo, { messageId: questionId, text: message, expectsReply: true });
3989
+ const result = await this.client.send(sendTo, { messageId: questionId, text: message, expectsReply: true, team });
3726
3990
  if (!result.delivered) {
3727
3991
  this.rejectToolReply(questionId, new Error(result.reason ?? "Session may not exist or has disconnected."));
3728
3992
  return textToolResult(`Message to "${to}" was not delivered: ${result.reason ?? "Session may not exist or has disconnected."}`, { ok: false, message_id: result.id, reason: result.reason }, true);
@@ -3758,7 +4022,7 @@ ${reply.content.text}${formatAttachments(reply.content.attachments)}`, { ok: tru
3758
4022
  const sessions = await this.client.listSessions();
3759
4023
  return resolveSessionTarget(sessions, to) ?? to;
3760
4024
  }
3761
- waitForToolReply(from, replyTo, timeoutMs = DEFAULT_ASK_TIMEOUT_MS, signal) {
4025
+ waitForToolReply(from, replyTo, timeoutMs = DEFAULT_ASK_TIMEOUT_MS, signal, team) {
3762
4026
  return new Promise((resolve5, reject) => {
3763
4027
  if (signal?.aborted) {
3764
4028
  reject(new Error("intercom_ask cancelled"));
@@ -3782,7 +4046,7 @@ ${reply.content.text}${formatAttachments(reply.content.attachments)}`, { ok: tru
3782
4046
  reject(new Error(`No reply from "${from}" within ${Math.round(timeoutMs / 1e3)} seconds`));
3783
4047
  }, timeoutMs);
3784
4048
  signal?.addEventListener("abort", onAbort, { once: true });
3785
- this.toolReplyWaiters.set(replyTo, { from, resolve: resolve5, reject, timeout, cleanup });
4049
+ this.toolReplyWaiters.set(replyTo, { from, team, resolve: resolve5, reject, timeout, cleanup });
3786
4050
  });
3787
4051
  }
3788
4052
  rejectToolReply(replyTo, error) {
@@ -4503,7 +4767,7 @@ async function waitForSocket(socketPath, proc, timeoutMs = 1e4) {
4503
4767
  throw new Error(`Codex app-server exited before creating ${socketPath}`);
4504
4768
  }
4505
4769
  if (existsSync6(socketPath)) return;
4506
- await delay2(50);
4770
+ await delay3(50);
4507
4771
  }
4508
4772
  throw new Error(`Timed out waiting for Codex app-server socket: ${socketPath}`);
4509
4773
  }
@@ -4627,16 +4891,16 @@ function cleanupOldCoiStateFiles(intercomDir, now = Date.now(), maxAgeMs = COI_S
4627
4891
  }
4628
4892
  for (const entry of entries) {
4629
4893
  if (!/^coi-.+-state\.json$/.test(entry)) continue;
4630
- const path = join8(intercomDir, entry);
4894
+ const path = join10(intercomDir, entry);
4631
4895
  try {
4632
4896
  const stat = statSync(path);
4633
- if (now - stat.mtimeMs > maxAgeMs) rmSync(path, { force: true });
4897
+ if (now - stat.mtimeMs > maxAgeMs) rmSync2(path, { force: true });
4634
4898
  } catch {
4635
4899
  }
4636
4900
  }
4637
4901
  }
4638
4902
  function resetCoiStateForFreshStart(statePath, fresh) {
4639
- if (fresh) rmSync(statePath, { force: true });
4903
+ if (fresh) rmSync2(statePath, { force: true });
4640
4904
  }
4641
4905
  async function runCoi(options, env = process.env) {
4642
4906
  const scopeId = intercomScopeIdFromEnv(env);
@@ -4663,11 +4927,11 @@ async function runCoi(options, env = process.env) {
4663
4927
  const name = options.name ?? identity.name;
4664
4928
  const intercomDir = getIntercomDirPath();
4665
4929
  cleanupOldCoiStateFiles(intercomDir);
4666
- const socketPath = options.socketPath ?? join8(intercomDir, `coi-${process.pid}.sock`);
4667
- const statePath = options.statePath ?? join8(intercomDir, `coi-${sanitizeSegment(id)}-state.json`);
4930
+ const socketPath = options.socketPath ?? join10(intercomDir, `coi-${process.pid}.sock`);
4931
+ const statePath = options.statePath ?? join10(intercomDir, `coi-${sanitizeSegment(id)}-state.json`);
4668
4932
  const fresh = env.AGENT_INTERCOM_FRESH === "1";
4669
4933
  resetCoiStateForFreshStart(statePath, fresh);
4670
- rmSync(socketPath, { force: true });
4934
+ rmSync2(socketPath, { force: true });
4671
4935
  const resumeRequest = resolveCoiResumeRequest(options.codexArgs);
4672
4936
  const agent = {
4673
4937
  id,
@@ -4692,7 +4956,7 @@ async function runCoi(options, env = process.env) {
4692
4956
  const cleanup = async () => {
4693
4957
  await daemon?.stop().catch(() => void 0);
4694
4958
  await stopChild(appServer);
4695
- rmSync(socketPath, { force: true });
4959
+ rmSync2(socketPath, { force: true });
4696
4960
  };
4697
4961
  let daemon = null;
4698
4962
  let cleaned = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctliz/agent-intercom-codex",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "Codex MCP plugin and wakeable worker for local messaging with Pi, Claude Code, OpenCode, Grok Build, and AGY agents.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -38,6 +38,7 @@
38
38
  ".codex-plugin/**/*",
39
39
  ".mcp.json",
40
40
  "README.md",
41
+ "CHANGELOG.md",
41
42
  "LICENSE",
42
43
  "THIRD_PARTY_NOTICES.md",
43
44
  "licenses/**/*",
@@ -23,8 +23,8 @@ messages to those workers create or resume app-server threads and start turns.
23
23
  ## Tools
24
24
 
25
25
  - `intercom_whoami`: show this session's intercom ID, name, cwd, and model.
26
- - `intercom_team`: show the current orchestrator manager and live same-manager coworkers.
27
- - `intercom_join`: list, join, or create a named team without tmux (`create: true` joins as manager).
26
+ - `intercom_team({ team? })`: inspect one task team or list all memberships; falls back to managed-team discovery.
27
+ - `intercom_join({ name?, create?, members?, work? })`: list, join, or create an additive task team; the manager adds connected peers in one call.
28
28
  - `intercom_status`: check connection, active session count, unread messages,
29
29
  and unresolved inbound asks.
30
30
  - `intercom_list`: list connected Pi and Codex sessions.
@@ -40,8 +40,8 @@ messages to those workers create or resume app-server threads and start turns.
40
40
  connected.
41
41
  2. Call `intercom_set_summary` with a concise status when other sessions need
42
42
  to discover your role.
43
- 3. If this is an orchestrator-owned coworker, call `intercom_team` to get the manager and sibling targets without searching globally. To form a team without tmux, call `intercom_join({ name: "billing", create: true })` in one session and `intercom_join({ name: "billing" })` in the others. Otherwise use `intercom_list`.
44
- 4. Use `intercom_send` for non-blocking updates and handoffs.
43
+ 3. When the user delegates to named peers and this task has no approved team, ask once whether to form a team with you and those peers. Wait for approval. An explicit create/join request is approval; never ask again for an approved task or inbound team message. After approval, discover peers and call `intercom_join({ name: "billing", create: true, members: ["front", "writer"], work: "Current task" })`. Do not ask the user to join each terminal manually. Reuse the approved team for the same task; new tasks may need new teams. Joining preserves all previous memberships and each team's manager/member roles.
44
+ 4. Use `intercom_send` for non-blocking updates and handoffs. Include `team` for task messages, especially when multiple teams are shared. Without a shared team, omit `team` for initial ungrouped contact; unrelated memberships do not block contact or silently create teams.
45
45
  5. Use `intercom_ask` only when you need the answer before continuing. Assignments, progress/status checkpoints, notifications, and completion reports use `intercom_send`.
46
46
  6. Call `intercom_pending` before ending a coordination turn, then answer
47
47
  blocking asks with `intercom_reply`.
@@ -73,7 +73,7 @@ intercom_pending({ mark_read: false })
73
73
  intercom_reply({ message: "Use GET/PUT/DELETE only, max 3 retries." })
74
74
  ```
75
75
 
76
- When multiple asks are pending, select the sender with `to`. If that sender has multiple unresolved asks, select `oldest` or `latest` without using protocol IDs:
76
+ Replies inherit the original message's team, never a mutable current team. Prefer `askId` or `contextId` returned by `intercom_pending` (contextId also selects ordinary messages). Mixed-team pending contexts require explicit selection; `team` cannot override the original. Within one task, `to` plus `oldest`/`latest` also selects an ask:
77
77
 
78
78
  ```typescript
79
79
  intercom_reply({
package/types.ts CHANGED
@@ -55,6 +55,7 @@ export interface Message {
55
55
  expectsReply?: boolean;
56
56
  content: {
57
57
  text: string;
58
+ team?: string;
58
59
  attachments?: Attachment[];
59
60
  };
60
61
  }