@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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +134 -0
- package/README.md +20 -3
- package/broker/broker.ts +1 -0
- package/broker/client.ts +3 -0
- package/codex/bridge-daemon.ts +66 -16
- package/codex/mcp-protocol.ts +31 -9
- package/codex/named-team-membership.ts +124 -0
- package/codex/named-teams.ts +11 -0
- package/codex/reply-context.ts +49 -0
- package/codex/runtime.ts +44 -30
- package/codex/team-guidance.ts +7 -0
- package/dist/bridge-daemon.mjs +279 -15
- package/dist/broker.mjs +2 -1
- package/dist/build-info.json +2 -2
- package/dist/codex-server.mjs +235 -80
- package/dist/coi.mjs +290 -26
- package/package.json +2 -1
- package/skills/codex-intercom/SKILL.md +5 -5
- package/types.ts +1 -0
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
|
|
190
|
-
- `intercom_join
|
|
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
|
|
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
|
};
|
package/codex/bridge-daemon.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
package/codex/mcp-protocol.ts
CHANGED
|
@@ -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
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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, {});
|