@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-intercom",
3
- "version": "0.1.0",
3
+ "version": "0.13.0",
4
4
  "description": "Codex MCP plugin and bridge daemon for local Pi/Codex intercom messaging.",
5
5
  "author": {
6
6
  "name": "ctliz",
package/CHANGELOG.md ADDED
@@ -0,0 +1,134 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.13.0 - 2026-10-02
6
+
7
+ - Add additive task teams shared with Pi, Claude, and OpenCode; managers can add connected peers and task descriptions in one call without changing registration scopes.
8
+ - Expose exact pending ask/context selectors; ordinary-message replies and blocking-ask replies inherit the original task team, never a mutable current team.
9
+ - Preserve teams in app-server worker replies and tools; add one-time approval guidance and allow initial contact across unrelated memberships.
10
+
11
+ ## 0.12.2 - 2026-09-30
12
+
13
+ - Synchronize native `thread/name/updated` notifications into Intercom presence without changing stable session IDs; retain the updated name on broker reconnect.
14
+ - Adopt persisted `thread.name` on resume instead of overriding it with launcher defaults.
15
+ - Verify rename notification schemas against Codex CLI 0.155.1 and 0.159.2; keep protocol v4 framing, durable queues, and delivery ACK semantics unchanged.
16
+
17
+ ## 0.12.1 - 2026-09-06
18
+
19
+ - Stable release of named teams without tmux. `intercom_join` works outside TmuxDeck.
20
+
21
+ ## 0.12.0-connect.8 - 2026-09-06
22
+
23
+ - Add `intercom_join` so Codex sessions can create or join a named team without tmux. Named teams share `~/.pi/agent/intercom/named-teams.json` with Pi, Claude, and OpenCode.
24
+
25
+ ## 0.12.0-connect.7 - 2026-08-24
26
+
27
+ - Document Grok Build and AGY support in detail on the npm package page, including packages, tools, identity, shared scope, Auto-Team participation, and polling-only delivery.
28
+ - Add Grok Build and AGY to npm package description and search metadata.
29
+
30
+ ## 0.12.0-connect.6 - 2026-08-24
31
+
32
+ - Synchronize companion install pins with the hosted-CI-validated Orchestrator release.
33
+
34
+ ## 0.12.0-connect.5 - 2026-08-24
35
+
36
+ - Synchronize companion install pins with the final systemd-validated Orchestrator release.
37
+
38
+ ## 0.12.0-connect.4 - 2026-08-24
39
+
40
+ - Synchronize Codex, Pi, and Orchestrator install pins with the final Grok/AGY documentation release.
41
+
42
+ ## 0.12.0-connect.3 - 2026-08-24
43
+
44
+ - Add the npm-packaged Grok Build and AGY MCP adapters to the synchronized Agent Intercom family documentation.
45
+ - Refresh npm and companion Pi/Orchestrator install examples to current releases.
46
+
47
+ ## 0.12.0-connect.2 - 2026-08-23
48
+
49
+ - Add complete OpenAI MCP tool annotations for all nine Intercom tools.
50
+ - Cover every MCP tool by name and verify the exact annotation semantics in tests.
51
+ - Explicitly disable shell execution for production child-process launches while keeping arguments separated.
52
+
53
+ ## 0.11.0-connect.1 - 2026-08-14
54
+
55
+ - Support Protocol v4 broker-enforced scope isolation (`AGENT_INTERCOM_SCOPE_ID`) and canonical `ctliz` distribution.
56
+ - Pin Core dependency to canonical commit `aad1985e125516b318181560293145bf2507cc6d` (`v0.1.0-connect.1`).
57
+ - Reject competing live runtimes that claim an active stable session ID while preserving legitimate reconnects and pending deliveries.
58
+ - Add ID-free `oldest`/`latest` selection for multiple pending asks from one sender, hide protocol IDs from pending output, and refuse a second unresolved ask to the same recipient.
59
+ - Automatically reconnect persistent bridges and MCP runtimes with their stable Intercom identity after broker restarts.
60
+ - Clarify that assignments and progress/status checkpoints use `intercom_send`, reserving `intercom_ask` for blocking decisions.
61
+ - Keep `coi` alive when Codex app-server emits retryable `error` notifications, and honor orchestrator fresh starts by discarding persisted bridge thread state.
62
+
63
+ ## 0.10.0 - 2026-07-16
64
+
65
+ - Add `intercom_team` to the MCP and app-server bridge surfaces so owned Codex coworkers can find their manager and live siblings without a global peer search.
66
+ - Forward orchestrator worker identity into Codex's MCP subprocess so team discovery resolves the owned worker instead of the helper session.
67
+
68
+ ## 0.9.3 - 2026-07-15
69
+
70
+ - Coordinate the Agent Intercom family on the `0.9.3` release line.
71
+
72
+ ## 0.9.2 - 2026-07-14
73
+
74
+ - Coordinate the Agent Intercom family on the `0.9.2` release line.
75
+ - Declare canonical GitHub repository metadata for npm provenance verification.
76
+
77
+ - Add CI for branches and pull requests.
78
+ - Add tag-driven npm trusted publishing with provenance and automatic GitHub Releases.
79
+
80
+ ## 0.9.1 - 2026-07-14
81
+
82
+ - Publish the package under the public npm scope `@dataforxyz/agent-intercom-codex`.
83
+ - Keep the Git repository and executable names unchanged.
84
+
85
+ ## 0.9.0 - 2026-07-14
86
+
87
+ - Align the Agent Intercom family on one coordinated `0.9.0` release line.
88
+ - No behavior change from the immediately preceding AGPL release.
89
+
90
+ ## 0.3.0 - 2026-07-14
91
+
92
+ ### Changed
93
+ - Changed the current project license to `AGPL-3.0-or-later`. Previously published MIT versions remain under MIT, and original `pi-intercom` notices are preserved in `THIRD_PARTY_NOTICES.md`.
94
+
95
+ ### Added
96
+ - Added protocol v3 delivery acknowledgements, explicit ask-control
97
+ confirmations, and a durable sender outbox that replays safely after broker
98
+ reconnects.
99
+ - Added an `Alt+I` shortcut to `coi` that copies the current session's usable
100
+ intercom contact target.
101
+ - Added terminal-protocol, OSC 52, editor insertion, and stable-ID fallbacks for
102
+ the shortcut, plus an opt-out flag for environments that do not want PTY
103
+ interception.
104
+
105
+ ### Changed
106
+ - Upgraded the bundled broker and client to the strict `pi-intercom` protocol
107
+ v3 and automatically replace an incompatible older local broker.
108
+ - Ask timeouts now defer the ask, preserving late replies without holding the
109
+ reverse-ask edge blocked indefinitely. Explicit cancellation still closes it.
110
+ - Made `node-pty` optional and clipboard helper execution asynchronous.
111
+
112
+ ## [0.1.0] - 2026-07-03
113
+
114
+ ### Added
115
+ - Added Codex MCP stdio server for local intercom messaging.
116
+ - Added tools for identity, status, session listing, summary updates, send,
117
+ ask, pending, and reply.
118
+ - Added Codex plugin metadata and a `codex-intercom` skill.
119
+ - Vendored the minimal pi-intercom broker/client protocol for compatibility.
120
+ - Added an app-server bridge daemon that publishes virtual Codex workers as
121
+ intercom sessions and wakes app-server turns on inbound messages.
122
+ - Added bridge config/state helpers and a `npm run codex:bridge` script.
123
+
124
+ ### Fixed
125
+ - Shut down the MCP server when stdio closes so completed Codex runs do not
126
+ leave stale intercom sessions behind.
127
+ - Defaulted bridge app-server launch to direct `codex app-server` mode so it
128
+ works without the standalone managed daemon install.
129
+
130
+ ### Docs
131
+ - Documented Codex MCP environment-variable behavior and the recommended
132
+ `intercom_set_summary` discovery flow for ad hoc multi-Codex runs.
133
+ - Documented bridge configuration, security defaults, and managed daemon proxy
134
+ mode.
package/README.md CHANGED
@@ -186,15 +186,26 @@ intercom tools.
186
186
  ## Tools
187
187
 
188
188
  - `intercom_whoami`: show this session's intercom ID, name, cwd, and model.
189
- - `intercom_team`: show the current manager and live coworkers owned by that manager.
190
- - `intercom_join`: list, join, or create a named team without tmux (`create: true` joins as manager).
189
+ - `intercom_team({ team? })`: show all named task memberships, inspect one team, or fall back to managed-team discovery.
190
+ - `intercom_join({ name?, create?, members?, work? })`: add task-team membership without leaving earlier teams; managers add connected peers in one call.
191
191
  - `intercom_status`: show connection status and pending message counts.
192
192
  - `intercom_list`: list local Pi, Codex, Claude Code, and OpenCode sessions in your scope (protocol v4 is same-scope; cross-scope contact requires an exact full session ID).
193
193
  - `intercom_set_summary`: publish a short discoverable status.
194
194
  - `intercom_send`: send a non-blocking message.
195
195
  - `intercom_ask`: send a question and wait for the target's reply.
196
196
  - `intercom_pending`: read queued inbound messages and unresolved asks.
197
- - `intercom_reply`: reply to a pending inbound ask; use `to` plus `which: "oldest" | "latest"` if one sender has multiple unresolved asks.
197
+ - `intercom_reply`: reply to an inbound ask or ordinary message via `askId` or `contextId` from `intercom_pending`; replies inherit the original team. Within one task, `to` plus `which: "oldest" | "latest"` also selects an ask.
198
+
199
+ ### Task teams
200
+
201
+ When the user delegates to named peers, the agent asks once whether to form a team for this task and waits for approval. Explicit create/join requests are already approval; approved tasks and inbound team messages do not prompt again. Discover the peers, then add everyone in one call:
202
+
203
+ ```typescript
204
+ intercom_join({ name: "launch", create: true, members: ["front", "writer"], work: "Launch page" })
205
+ intercom_send({ to: "front", team: "launch", message: "Implement the header." })
206
+ ```
207
+
208
+ Membership and roles are per task and additive; joining preserves earlier teams and registration scope. Reuse an approved team for the same task and append approved peers as manager. For initial contact without a shared team, omit `team`; unrelated memberships do not block messaging or silently create teams. Multiple shared teams require `team` on sends/asks. Replies retain the original message's team via exact selectors, not a mutable current team. App-server workers also retain the team on automatic replies.
198
209
 
199
210
  Pending output never exposes protocol message IDs. Keep at most one unresolved `intercom_ask` to the same recipient; the broker rejects a second ask and recommends `intercom_send` for a non-blocking follow-up. Use `intercom_send`—not `intercom_ask`—for assignments and progress/status checkpoints.
200
211
 
@@ -224,6 +235,12 @@ For longer work, use `intercom_send` and check later with `intercom_pending`.
224
235
  for that socket, creates or resumes the sidecar's app-server thread, then
225
236
  launches an interactive Codex UI attached to the same socket and thread.
226
237
 
238
+ Native thread renames (`thread/name/updated`) update the sidecar's Intercom
239
+ presence without changing its stable ID. Reconnects retain the updated name;
240
+ resuming a thread adopts its persisted `thread.name` rather than replacing it
241
+ with the launcher's default. The notification schema was checked against Codex
242
+ CLI 0.155.1 and 0.159.2. Protocol v4 and delivery ACK behavior are unchanged.
243
+
227
244
  Start a named worker:
228
245
 
229
246
  ```bash
package/broker/broker.ts CHANGED
@@ -240,6 +240,7 @@ function isMessage(value: unknown): value is Message {
240
240
  return false;
241
241
  }
242
242
 
243
+ if (content.team !== undefined && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
243
244
  return content.attachments === undefined
244
245
  || (
245
246
  Array.isArray(content.attachments)
package/broker/client.ts CHANGED
@@ -41,6 +41,7 @@ import type {
41
41
 
42
42
  export interface SendOptions {
43
43
  text: string;
44
+ team?: string;
44
45
  attachments?: Attachment[];
45
46
  replyTo?: string;
46
47
  expectsReply?: boolean;
@@ -134,6 +135,7 @@ function isMessage(value: unknown): value is Message {
134
135
  return false;
135
136
  }
136
137
 
138
+ if (content.team !== undefined && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
137
139
  return content.attachments === undefined
138
140
  || (Array.isArray(content.attachments) && content.attachments.every(isAttachment));
139
141
  }
@@ -905,6 +907,7 @@ export class IntercomClient extends EventEmitter {
905
907
  expectsReply: options.expectsReply,
906
908
  content: {
907
909
  text: options.text,
910
+ ...(options.team === undefined ? {} : { team: options.team }),
908
911
  attachments: options.attachments,
909
912
  },
910
913
  };
@@ -23,6 +23,9 @@ import type { Message, SessionInfo } from "../types.ts";
23
23
  import { resolveContactTarget, type IntercomContact } from "./contact.ts";
24
24
  import { formatAttachments, formatSessionDisplay, formatSessionList, resolveSessionTarget, type ToolResult } from "./runtime.ts";
25
25
  import { formatIntercomTeam, resolveIntercomTeam } from "./team.ts";
26
+ import { appendNamedTeamMembership, sessionNamedTeams, namedTeamRoster, formatNamedTeamRoster, resolveNamedMessageTeam } from "./named-team-membership.ts";
27
+ import { formatJoinableNamedTeamList, listNamedTeams } from "./named-teams.ts";
28
+ import { TASK_TEAM_GUIDANCE } from "./team-guidance.ts";
26
29
 
27
30
  interface TurnWaiter {
28
31
  from: SessionInfo;
@@ -31,6 +34,7 @@ interface TurnWaiter {
31
34
 
32
35
  interface ToolReplyWaiter {
33
36
  from: string;
37
+ team?: string;
34
38
  resolve: (message: Message) => void;
35
39
  reject: (error: Error) => void;
36
40
  timeout: NodeJS.Timeout;
@@ -58,6 +62,7 @@ export interface VirtualCodexAgentOptions {
58
62
  const APPROVED_INTERCOM_TOOLS = new Set([
59
63
  "intercom_whoami",
60
64
  "intercom_team",
65
+ "intercom_join",
61
66
  "intercom_status",
62
67
  "intercom_list",
63
68
  "intercom_set_summary",
@@ -88,6 +93,7 @@ function formatMessage(from: SessionInfo, message: Message, agent: BridgeAgentCo
88
93
  return [
89
94
  `Intercom message for ${agent.name}.`,
90
95
  `From: ${formatSessionDisplay(from)} (${from.id})`,
96
+ ...(message.content.team ? [`[Team: ${message.content.team}]`] : []),
91
97
  `Message id: ${message.id}`,
92
98
  "",
93
99
  message.content.text,
@@ -191,7 +197,7 @@ function getCompletedAgentText(params: unknown): string | null {
191
197
  return raw.type === "agentMessage" && typeof raw.text === "string" ? raw.text : null;
192
198
  }
193
199
 
194
- function intercomSendFromArgs(rawArgs: unknown): { to: string; message: string } | null {
200
+ function intercomSendFromArgs(rawArgs: unknown): { to: string; message: string; team?: string } | null {
195
201
  let args: Record<string, unknown>;
196
202
  try {
197
203
  args = parseToolArguments(rawArgs);
@@ -199,11 +205,11 @@ function intercomSendFromArgs(rawArgs: unknown): { to: string; message: string }
199
205
  return null;
200
206
  }
201
207
  return typeof args.to === "string" && typeof args.message === "string"
202
- ? { to: args.to, message: args.message }
208
+ ? { to: args.to, message: args.message, ...(typeof args.team === "string" ? { team: args.team } : {}) }
203
209
  : null;
204
210
  }
205
211
 
206
- export function getCompletedIntercomSend(params: unknown): { to: string; message: string } | null {
212
+ export function getCompletedIntercomSend(params: unknown): { to: string; message: string; team?: string } | null {
207
213
  if (!params || typeof params !== "object") return null;
208
214
  const item = (params as Record<string, unknown>).item;
209
215
  if (!isRecord(item)) return null;
@@ -212,7 +218,7 @@ export function getCompletedIntercomSend(params: unknown): { to: string; message
212
218
  return intercomSendFromArgs(item.arguments ?? item.args ?? item.input);
213
219
  }
214
220
 
215
- export function getApprovedIntercomSend(params: unknown): { to: string; message: string } | null {
221
+ export function getApprovedIntercomSend(params: unknown): { to: string; message: string; team?: string } | null {
216
222
  if (getApprovedIntercomToolFromApproval(params) !== "intercom_send") return null;
217
223
  if (!isRecord(params)) return null;
218
224
  const meta = isRecord(params._meta) ? params._meta : {};
@@ -234,7 +240,7 @@ function asOptionalPositiveInteger(value: unknown, name: string): number | undef
234
240
  }
235
241
 
236
242
  function normalizeToolName(name: string): string {
237
- const mcpMatch = name.match(/(?:^|__|\.)intercom_(whoami|status|list|set_summary|send|ask|pending|reply)$/);
243
+ const mcpMatch = name.match(/(?:^|__|\.)intercom_(whoami|team|join|status|list|set_summary|send|ask|pending|reply)$/);
238
244
  if (mcpMatch) return `intercom_${mcpMatch[1]}`;
239
245
  return name;
240
246
  }
@@ -426,6 +432,15 @@ export class VirtualCodexAgent {
426
432
  const threadId = getNotificationThreadId(message.params);
427
433
  if (!threadId || threadId !== this.threadId) return;
428
434
 
435
+ if (message.method === "thread/name/updated" && isRecord(message.params)) {
436
+ const name = message.params.threadName;
437
+ if (typeof name === "string" && name.trim() && name !== this.agent.name) {
438
+ this.agent.name = name;
439
+ this.client.updatePresence({ name });
440
+ }
441
+ return;
442
+ }
443
+
429
444
  if (message.method === "error") {
430
445
  const params = isRecord(message.params) ? message.params : {};
431
446
  const detail = isRecord(params.error) && typeof params.error.message === "string"
@@ -477,13 +492,18 @@ export class VirtualCodexAgent {
477
492
  if (this.threadId) {
478
493
  try {
479
494
  const sandbox = bridgeAgentSandboxMode(this.agent);
480
- await this.app.request("thread/resume", {
495
+ const result = await this.app.request("thread/resume", {
481
496
  threadId: this.threadId,
482
497
  cwd: this.agent.cwd,
483
498
  model: this.agent.model ?? null,
484
499
  approvalPolicy: bridgeAgentApprovalPolicy(this.agent),
485
500
  sandbox,
486
501
  });
502
+ if (isRecord(result) && isRecord(result.thread)) {
503
+ this.onNotification({ method: "thread/name/updated", params: {
504
+ threadId: this.threadId, threadName: result.thread.name,
505
+ } });
506
+ }
487
507
  return this.threadId;
488
508
  } catch {
489
509
  this.threadId = null;
@@ -497,7 +517,7 @@ export class VirtualCodexAgent {
497
517
  approvalPolicy: bridgeAgentApprovalPolicy(this.agent),
498
518
  sandbox,
499
519
  serviceName: "codex-intercom",
500
- developerInstructions: this.agent.instructions ?? null,
520
+ developerInstructions: [this.agent.instructions, TASK_TEAM_GUIDANCE].filter(Boolean).join("\n\n"),
501
521
  threadSource: "cli",
502
522
  });
503
523
  this.threadId = getThreadId(result);
@@ -511,7 +531,7 @@ export class VirtualCodexAgent {
511
531
  private routeMessage(from: SessionInfo, message: Message): Promise<void> {
512
532
  const toolWaiter = this.toolReplyWaiters.get(message.replyTo ?? "");
513
533
  if (toolWaiter) {
514
- if (from.id === toolWaiter.from) {
534
+ if (from.id === toolWaiter.from && message.content.team === toolWaiter.team) {
515
535
  this.toolReplyWaiters.delete(message.replyTo ?? "");
516
536
  clearTimeout(toolWaiter.timeout);
517
537
  toolWaiter.cleanup?.();
@@ -564,18 +584,22 @@ export class VirtualCodexAgent {
564
584
  this.waiters.delete(turnId);
565
585
  const reply = this.finalMessages.get(turnId)?.trim() || "Codex turn completed without a final message.";
566
586
  for (const waiter of waiters) {
567
- await this.client.send(waiter.from.id, { text: reply, replyTo: waiter.message.id }).catch((error) => {
587
+ await this.client.send(waiter.from.id, { text: reply, replyTo: waiter.message.id, team: waiter.message.content.team }).catch((error) => {
568
588
  process.stderr.write(`reply failed for ${this.agent.id}: ${error instanceof Error ? error.message : String(error)}\n`);
569
589
  });
570
590
  }
571
591
  }
572
592
 
573
- async replyToWaitersFromIntercomSend(turnId: string, send: { to: string; message: string }): Promise<void> {
593
+ async replyToWaitersFromIntercomSend(turnId: string, send: { to: string; message: string; team?: string }): Promise<void> {
574
594
  const waiters = this.waiters.get(turnId);
575
595
  if (!waiters?.length) return;
576
596
  const lowerTo = send.to.toLowerCase();
577
597
  const remaining: TurnWaiter[] = [];
578
598
  for (const waiter of waiters) {
599
+ if (send.team !== undefined && send.team !== waiter.message.content.team) {
600
+ remaining.push(waiter);
601
+ continue;
602
+ }
579
603
  const matchesSender = send.to === waiter.from.id
580
604
  || waiter.from.id.startsWith(send.to)
581
605
  || waiter.from.name?.toLowerCase() === lowerTo;
@@ -583,7 +607,7 @@ export class VirtualCodexAgent {
583
607
  remaining.push(waiter);
584
608
  continue;
585
609
  }
586
- await this.client.send(waiter.from.id, { text: send.message, replyTo: waiter.message.id }).catch((error) => {
610
+ await this.client.send(waiter.from.id, { text: send.message, replyTo: waiter.message.id, team: waiter.message.content.team }).catch((error) => {
587
611
  remaining.push(waiter);
588
612
  process.stderr.write(`reply failed for ${this.agent.id}: ${error instanceof Error ? error.message : String(error)}\n`);
589
613
  });
@@ -650,8 +674,32 @@ export class VirtualCodexAgent {
650
674
  `session_id: ${this.agent.id}\nname: ${this.agent.name}\ncwd: ${this.agent.cwd}`,
651
675
  { session_id: this.agent.id, name: this.agent.name, cwd: this.agent.cwd, model: this.agent.model ?? "codex-app-server" },
652
676
  );
677
+ case "intercom_join": {
678
+ if (args.name === undefined) {
679
+ if (args.create || args.members !== undefined || args.work !== undefined) throw new Error("Creating or extending a team requires a name");
680
+ return textToolResult(formatJoinableNamedTeamList(listNamedTeams()));
681
+ }
682
+ if (args.members !== undefined && !Array.isArray(args.members)) throw new Error("members must be an array");
683
+ const sessions = await this.client.listSessions();
684
+ const members = Array.from((args.members ?? []) as unknown[], (member) => {
685
+ const id = resolveSessionTarget(sessions, asString(member, "member"));
686
+ if (!id) throw new Error("Team member is not connected");
687
+ return id;
688
+ });
689
+ const team = await appendNamedTeamMembership({ name: asString(args.name, "name"), selfId: this.agent.id, members, create: args.create === true, ...(args.work === undefined ? {} : { work: asString(args.work, "work") }) });
690
+ const roster = namedTeamRoster(team, this.agent.id, sessions);
691
+ return textToolResult(formatNamedTeamRoster(roster), { ok: true, team: team.name, roster });
692
+ }
653
693
  case "intercom_team": {
654
694
  const sessions = await this.client.listSessions();
695
+ const mine = sessionNamedTeams(this.agent.id);
696
+ const requested = args.team === undefined ? undefined : asString(args.team, "team");
697
+ const selected = requested ? mine.filter((entry) => entry.name === requested) : mine;
698
+ if (requested && !selected.length) throw new Error("You do not belong to that team");
699
+ if (selected.length) {
700
+ const teams = selected.map((entry) => namedTeamRoster(entry, this.agent.id, sessions));
701
+ return textToolResult(teams.map(formatNamedTeamRoster).join("\n\n"), { teams });
702
+ }
655
703
  const team = await resolveIntercomTeam({ selfId: this.agent.id, sessions });
656
704
  return textToolResult(formatIntercomTeam(team), team as unknown as Record<string, unknown>);
657
705
  }
@@ -678,7 +726,8 @@ export class VirtualCodexAgent {
678
726
  const to = asString(args.to, "to");
679
727
  const message = asString(args.message, "message");
680
728
  const sendTo = await this.resolveTarget(to);
681
- const result = await this.client.send(sendTo, { text: message });
729
+ const team = resolveNamedMessageTeam(this.agent.id, sendTo, args.team === undefined ? undefined : asString(args.team, "team"));
730
+ const result = await this.client.send(sendTo, { text: message, team });
682
731
  if (!result.delivered) {
683
732
  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);
684
733
  }
@@ -691,10 +740,11 @@ export class VirtualCodexAgent {
691
740
  const message = asString(args.message, "message");
692
741
  const timeoutMs = asOptionalPositiveInteger(args.timeout_ms, "timeout_ms") ?? DEFAULT_ASK_TIMEOUT_MS;
693
742
  const sendTo = await this.resolveTarget(to);
743
+ const team = resolveNamedMessageTeam(this.agent.id, sendTo, args.team === undefined ? undefined : asString(args.team, "team"));
694
744
  const questionId = randomUUID();
695
- const replyPromise = this.waitForToolReply(sendTo, questionId, timeoutMs, signal);
745
+ const replyPromise = this.waitForToolReply(sendTo, questionId, timeoutMs, signal, team);
696
746
  void replyPromise.catch(() => undefined);
697
- const result = await this.client.send(sendTo, { messageId: questionId, text: message, expectsReply: true });
747
+ const result = await this.client.send(sendTo, { messageId: questionId, text: message, expectsReply: true, team });
698
748
  if (!result.delivered) {
699
749
  this.rejectToolReply(questionId, new Error(result.reason ?? "Session may not exist or has disconnected."));
700
750
  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);
@@ -734,7 +784,7 @@ export class VirtualCodexAgent {
734
784
  return resolveSessionTarget(sessions, to) ?? to;
735
785
  }
736
786
 
737
- private waitForToolReply(from: string, replyTo: string, timeoutMs = DEFAULT_ASK_TIMEOUT_MS, signal?: AbortSignal): Promise<Message> {
787
+ private waitForToolReply(from: string, replyTo: string, timeoutMs = DEFAULT_ASK_TIMEOUT_MS, signal?: AbortSignal, team?: string): Promise<Message> {
738
788
  return new Promise((resolve, reject) => {
739
789
  if (signal?.aborted) {
740
790
  reject(new Error("intercom_ask cancelled"));
@@ -758,7 +808,7 @@ export class VirtualCodexAgent {
758
808
  reject(new Error(`No reply from "${from}" within ${Math.round(timeoutMs / 1000)} seconds`));
759
809
  }, timeoutMs);
760
810
  signal?.addEventListener("abort", onAbort, { once: true });
761
- this.toolReplyWaiters.set(replyTo, { from, resolve, reject, timeout, cleanup });
811
+ this.toolReplyWaiters.set(replyTo, { from, team, resolve, reject, timeout, cleanup });
762
812
  });
763
813
  }
764
814
 
@@ -1,5 +1,6 @@
1
1
  import type { CodexIntercomRuntime, ToolResult } from "./runtime.ts";
2
2
  import { validateAskTimeoutMs } from "../config.ts";
3
+ import { TASK_TEAM_GUIDANCE } from "./team-guidance.ts";
3
4
  import type { Attachment } from "../types.ts";
4
5
 
5
6
  interface JsonRpcRequest {
@@ -34,6 +35,16 @@ function asString(value: unknown, name: string): string {
34
35
  return value;
35
36
  }
36
37
 
38
+ function optionalString(value: unknown, name: string): string | undefined {
39
+ return value === undefined ? undefined : asString(value, name);
40
+ }
41
+
42
+ function memberArray(value: unknown): string[] | undefined {
43
+ if (value === undefined) return undefined;
44
+ if (!Array.isArray(value)) throw new Error("members must be an array of session names or IDs");
45
+ return Array.from(value, (member) => asString(member, "member"));
46
+ }
47
+
37
48
  function asBoolean(value: unknown, defaultValue: boolean): boolean {
38
49
  return typeof value === "boolean" ? value : defaultValue;
39
50
  }
@@ -85,26 +96,30 @@ export function buildToolDefinitions(runtime: CodexIntercomRuntime): ToolDefinit
85
96
  },
86
97
  {
87
98
  name: "intercom_team",
88
- description: "Show your current manager and the live coworkers owned by that manager. No arguments are required.",
89
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
99
+ description: "Show all your named task teams, or inspect one by name. Falls back to managed-team discovery.",
100
+ inputSchema: { type: "object", properties: { team: { type: "string" } }, additionalProperties: false },
90
101
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
91
- handler: async () => runtime.team(),
102
+ handler: async (args) => runtime.team(optionalString(args.team, "team")),
92
103
  },
93
104
  {
94
105
  name: "intercom_join",
95
- description: "List, join, or create a named intercom team without tmux. Omit name to list joinable teams. Set create=true to create a team and join as manager.",
106
+ description: "After user approval, create or join a task team. Membership is additive; members lets the manager add connected peers in one call. Omit name to list teams.",
96
107
  inputSchema: {
97
108
  type: "object",
98
109
  properties: {
99
110
  name: { type: "string", description: "Team name to join. Omit to list joinable teams." },
100
111
  create: { type: "boolean", description: "Create this named team and join as manager. Requires name." },
112
+ members: { type: "array", items: { type: "string" }, description: "Connected session names or IDs to add; manager only." },
113
+ work: { type: "string", maxLength: 2000, description: "Task description when creating a team." },
101
114
  },
102
115
  additionalProperties: false,
103
116
  },
104
117
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
105
118
  handler: async (args) => runtime.join(
106
- typeof args.name === "string" ? args.name : undefined,
119
+ optionalString(args.name, "name"),
107
120
  args.create === true,
121
+ memberArray(args.members),
122
+ optionalString(args.work, "work"),
108
123
  ),
109
124
  },
110
125
  {
@@ -152,12 +167,13 @@ export function buildToolDefinitions(runtime: CodexIntercomRuntime): ToolDefinit
152
167
  to: { type: "string" },
153
168
  message: { type: "string" },
154
169
  attachments: attachmentsSchema,
170
+ team: { type: "string", description: "Task team; required when multiple teams are shared. Omit for ungrouped contact." },
155
171
  },
156
172
  required: ["to", "message"],
157
173
  additionalProperties: false,
158
174
  },
159
175
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
160
- handler: async (args) => runtime.send(asString(args.to, "to"), asString(args.message, "message"), asAttachmentArray(args.attachments)),
176
+ handler: async (args) => runtime.send(asString(args.to, "to"), asString(args.message, "message"), asAttachmentArray(args.attachments), undefined, optionalString(args.team, "team")),
161
177
  },
162
178
  {
163
179
  name: "intercom_ask",
@@ -168,6 +184,7 @@ export function buildToolDefinitions(runtime: CodexIntercomRuntime): ToolDefinit
168
184
  to: { type: "string" },
169
185
  message: { type: "string" },
170
186
  attachments: attachmentsSchema,
187
+ team: { type: "string", description: "Task team; required when multiple teams are shared." },
171
188
  timeout_ms: { type: "integer", minimum: 1, maximum: 120000, description: "Maximum time to wait for a reply before returning an error. Use intercom_send plus intercom_pending for longer work." },
172
189
  },
173
190
  required: ["to", "message"],
@@ -180,6 +197,7 @@ export function buildToolDefinitions(runtime: CodexIntercomRuntime): ToolDefinit
180
197
  asAttachmentArray(args.attachments),
181
198
  asOptionalPositiveInteger(args.timeout_ms, "timeout_ms"),
182
199
  signal,
200
+ optionalString(args.team, "team"),
183
201
  ),
184
202
  },
185
203
  {
@@ -195,19 +213,22 @@ export function buildToolDefinitions(runtime: CodexIntercomRuntime): ToolDefinit
195
213
  },
196
214
  {
197
215
  name: "intercom_reply",
198
- description: "Reply to a pending inbound ask. Use to plus which=oldest/latest when one sender has multiple unresolved asks.",
216
+ description: "Reply to an inbound ask or ordinary message using askId or contextId from intercom_pending. Inherits the original team; team cannot override it.",
199
217
  inputSchema: {
200
218
  type: "object",
201
219
  properties: {
202
220
  message: { type: "string" },
203
221
  to: { type: "string", description: "Optional sender/session selector; never a message or thread ID." },
222
+ askId: { type: "string", description: "Stable receiver-local pending ask selector." },
223
+ contextId: { type: "string", description: "Exact inbound context selector, including ordinary messages." },
224
+ team: { type: "string", description: "Must match the original message; omit to inherit." },
204
225
  which: { type: "string", enum: ["oldest", "latest"], description: "Select the oldest or latest ask from the chosen sender." },
205
226
  },
206
227
  required: ["message"],
207
228
  additionalProperties: false,
208
229
  },
209
230
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
210
- handler: async (args) => runtime.reply(asString(args.message, "message"), typeof args.to === "string" ? args.to : undefined, args.which === "oldest" || args.which === "latest" ? args.which : undefined),
231
+ handler: async (args) => runtime.reply(asString(args.message, "message"), typeof args.to === "string" ? args.to : undefined, args.which === "oldest" || args.which === "latest" ? args.which : undefined, optionalString(args.askId, "askId"), optionalString(args.contextId, "contextId"), optionalString(args.team, "team")),
211
232
  },
212
233
  ];
213
234
  }
@@ -243,7 +264,8 @@ export async function handleMcpRequest(request: JsonRpcRequest, runtime: CodexIn
243
264
  return ok(request.id, {
244
265
  protocolVersion: "2025-06-18",
245
266
  capabilities: { tools: {} },
246
- serverInfo: { name: "codex-intercom", version: "0.1.0" },
267
+ instructions: TASK_TEAM_GUIDANCE,
268
+ serverInfo: { name: "codex-intercom", version: "0.13.0" },
247
269
  });
248
270
  case "ping":
249
271
  return ok(request.id, {});