@ctliz/agent-intercom-codex 0.12.2 → 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.2 target=coi sourceSha256=fb36315f414b3f19538f70942c884026e951ee63fb0060970c2c041e21aed32d\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() {
@@ -3549,7 +3769,7 @@ var VirtualCodexAgent = class {
3549
3769
  approvalPolicy: bridgeAgentApprovalPolicy(this.agent),
3550
3770
  sandbox,
3551
3771
  serviceName: "codex-intercom",
3552
- developerInstructions: this.agent.instructions ?? null,
3772
+ developerInstructions: [this.agent.instructions, TASK_TEAM_GUIDANCE].filter(Boolean).join("\n\n"),
3553
3773
  threadSource: "cli"
3554
3774
  });
3555
3775
  this.threadId = getThreadId(result);
@@ -3562,7 +3782,7 @@ var VirtualCodexAgent = class {
3562
3782
  routeMessage(from, message) {
3563
3783
  const toolWaiter = this.toolReplyWaiters.get(message.replyTo ?? "");
3564
3784
  if (toolWaiter) {
3565
- if (from.id === toolWaiter.from) {
3785
+ if (from.id === toolWaiter.from && message.content.team === toolWaiter.team) {
3566
3786
  this.toolReplyWaiters.delete(message.replyTo ?? "");
3567
3787
  clearTimeout(toolWaiter.timeout);
3568
3788
  toolWaiter.cleanup?.();
@@ -3608,7 +3828,7 @@ var VirtualCodexAgent = class {
3608
3828
  this.waiters.delete(turnId);
3609
3829
  const reply = this.finalMessages.get(turnId)?.trim() || "Codex turn completed without a final message.";
3610
3830
  for (const waiter of waiters) {
3611
- 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) => {
3612
3832
  process.stderr.write(`reply failed for ${this.agent.id}: ${error instanceof Error ? error.message : String(error)}
3613
3833
  `);
3614
3834
  });
@@ -3620,12 +3840,16 @@ var VirtualCodexAgent = class {
3620
3840
  const lowerTo = send.to.toLowerCase();
3621
3841
  const remaining = [];
3622
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
+ }
3623
3847
  const matchesSender = send.to === waiter.from.id || waiter.from.id.startsWith(send.to) || waiter.from.name?.toLowerCase() === lowerTo;
3624
3848
  if (!matchesSender) {
3625
3849
  remaining.push(waiter);
3626
3850
  continue;
3627
3851
  }
3628
- 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) => {
3629
3853
  remaining.push(waiter);
3630
3854
  process.stderr.write(`reply failed for ${this.agent.id}: ${error instanceof Error ? error.message : String(error)}
3631
3855
  `);
@@ -3690,8 +3914,32 @@ name: ${this.agent.name}
3690
3914
  cwd: ${this.agent.cwd}`,
3691
3915
  { session_id: this.agent.id, name: this.agent.name, cwd: this.agent.cwd, model: this.agent.model ?? "codex-app-server" }
3692
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
+ }
3693
3933
  case "intercom_team": {
3694
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
+ }
3695
3943
  const team = await resolveIntercomTeam({ selfId: this.agent.id, sessions });
3696
3944
  return textToolResult(formatIntercomTeam(team), team);
3697
3945
  }
@@ -3720,7 +3968,8 @@ Active sessions: ${sessions.length}`,
3720
3968
  const to = asString(args.to, "to");
3721
3969
  const message = asString(args.message, "message");
3722
3970
  const sendTo = await this.resolveTarget(to);
3723
- 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 });
3724
3973
  if (!result.delivered) {
3725
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);
3726
3975
  }
@@ -3733,10 +3982,11 @@ Active sessions: ${sessions.length}`,
3733
3982
  const message = asString(args.message, "message");
3734
3983
  const timeoutMs = asOptionalPositiveInteger(args.timeout_ms, "timeout_ms") ?? DEFAULT_ASK_TIMEOUT_MS;
3735
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"));
3736
3986
  const questionId = randomUUID4();
3737
- const replyPromise = this.waitForToolReply(sendTo, questionId, timeoutMs, signal);
3987
+ const replyPromise = this.waitForToolReply(sendTo, questionId, timeoutMs, signal, team);
3738
3988
  void replyPromise.catch(() => void 0);
3739
- 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 });
3740
3990
  if (!result.delivered) {
3741
3991
  this.rejectToolReply(questionId, new Error(result.reason ?? "Session may not exist or has disconnected."));
3742
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);
@@ -3772,7 +4022,7 @@ ${reply.content.text}${formatAttachments(reply.content.attachments)}`, { ok: tru
3772
4022
  const sessions = await this.client.listSessions();
3773
4023
  return resolveSessionTarget(sessions, to) ?? to;
3774
4024
  }
3775
- waitForToolReply(from, replyTo, timeoutMs = DEFAULT_ASK_TIMEOUT_MS, signal) {
4025
+ waitForToolReply(from, replyTo, timeoutMs = DEFAULT_ASK_TIMEOUT_MS, signal, team) {
3776
4026
  return new Promise((resolve5, reject) => {
3777
4027
  if (signal?.aborted) {
3778
4028
  reject(new Error("intercom_ask cancelled"));
@@ -3796,7 +4046,7 @@ ${reply.content.text}${formatAttachments(reply.content.attachments)}`, { ok: tru
3796
4046
  reject(new Error(`No reply from "${from}" within ${Math.round(timeoutMs / 1e3)} seconds`));
3797
4047
  }, timeoutMs);
3798
4048
  signal?.addEventListener("abort", onAbort, { once: true });
3799
- this.toolReplyWaiters.set(replyTo, { from, resolve: resolve5, reject, timeout, cleanup });
4049
+ this.toolReplyWaiters.set(replyTo, { from, team, resolve: resolve5, reject, timeout, cleanup });
3800
4050
  });
3801
4051
  }
3802
4052
  rejectToolReply(replyTo, error) {
@@ -4517,7 +4767,7 @@ async function waitForSocket(socketPath, proc, timeoutMs = 1e4) {
4517
4767
  throw new Error(`Codex app-server exited before creating ${socketPath}`);
4518
4768
  }
4519
4769
  if (existsSync6(socketPath)) return;
4520
- await delay2(50);
4770
+ await delay3(50);
4521
4771
  }
4522
4772
  throw new Error(`Timed out waiting for Codex app-server socket: ${socketPath}`);
4523
4773
  }
@@ -4641,16 +4891,16 @@ function cleanupOldCoiStateFiles(intercomDir, now = Date.now(), maxAgeMs = COI_S
4641
4891
  }
4642
4892
  for (const entry of entries) {
4643
4893
  if (!/^coi-.+-state\.json$/.test(entry)) continue;
4644
- const path = join8(intercomDir, entry);
4894
+ const path = join10(intercomDir, entry);
4645
4895
  try {
4646
4896
  const stat = statSync(path);
4647
- if (now - stat.mtimeMs > maxAgeMs) rmSync(path, { force: true });
4897
+ if (now - stat.mtimeMs > maxAgeMs) rmSync2(path, { force: true });
4648
4898
  } catch {
4649
4899
  }
4650
4900
  }
4651
4901
  }
4652
4902
  function resetCoiStateForFreshStart(statePath, fresh) {
4653
- if (fresh) rmSync(statePath, { force: true });
4903
+ if (fresh) rmSync2(statePath, { force: true });
4654
4904
  }
4655
4905
  async function runCoi(options, env = process.env) {
4656
4906
  const scopeId = intercomScopeIdFromEnv(env);
@@ -4677,11 +4927,11 @@ async function runCoi(options, env = process.env) {
4677
4927
  const name = options.name ?? identity.name;
4678
4928
  const intercomDir = getIntercomDirPath();
4679
4929
  cleanupOldCoiStateFiles(intercomDir);
4680
- const socketPath = options.socketPath ?? join8(intercomDir, `coi-${process.pid}.sock`);
4681
- 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`);
4682
4932
  const fresh = env.AGENT_INTERCOM_FRESH === "1";
4683
4933
  resetCoiStateForFreshStart(statePath, fresh);
4684
- rmSync(socketPath, { force: true });
4934
+ rmSync2(socketPath, { force: true });
4685
4935
  const resumeRequest = resolveCoiResumeRequest(options.codexArgs);
4686
4936
  const agent = {
4687
4937
  id,
@@ -4706,7 +4956,7 @@ async function runCoi(options, env = process.env) {
4706
4956
  const cleanup = async () => {
4707
4957
  await daemon?.stop().catch(() => void 0);
4708
4958
  await stopChild(appServer);
4709
- rmSync(socketPath, { force: true });
4959
+ rmSync2(socketPath, { force: true });
4710
4960
  };
4711
4961
  let daemon = null;
4712
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.2",
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",
@@ -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
  }