@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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +14 -3
- package/broker/broker.ts +1 -0
- package/broker/client.ts +3 -0
- package/codex/bridge-daemon.ts +51 -15
- 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 +264 -14
- package/dist/broker.mjs +2 -1
- package/dist/build-info.json +2 -2
- package/dist/codex-server.mjs +235 -80
- package/dist/coi.mjs +275 -25
- package/package.json +1 -1
- package/skills/codex-intercom/SKILL.md +5 -5
- package/types.ts +1 -0
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.
|
|
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
|
|
10
|
-
import { setTimeout as
|
|
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
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
4894
|
+
const path = join10(intercomDir, entry);
|
|
4645
4895
|
try {
|
|
4646
4896
|
const stat = statSync(path);
|
|
4647
|
-
if (now - stat.mtimeMs > maxAgeMs)
|
|
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)
|
|
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 ??
|
|
4681
|
-
const statePath = options.statePath ??
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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`:
|
|
27
|
-
- `intercom_join`: list, join, or create
|
|
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.
|
|
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
|
-
|
|
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({
|