@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/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() {
|
|
@@ -3470,6 +3690,14 @@ var VirtualCodexAgent = class {
|
|
|
3470
3690
|
onNotification(message) {
|
|
3471
3691
|
const threadId = getNotificationThreadId(message.params);
|
|
3472
3692
|
if (!threadId || threadId !== this.threadId) return;
|
|
3693
|
+
if (message.method === "thread/name/updated" && isRecord2(message.params)) {
|
|
3694
|
+
const name = message.params.threadName;
|
|
3695
|
+
if (typeof name === "string" && name.trim() && name !== this.agent.name) {
|
|
3696
|
+
this.agent.name = name;
|
|
3697
|
+
this.client.updatePresence({ name });
|
|
3698
|
+
}
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3473
3701
|
if (message.method === "error") {
|
|
3474
3702
|
const params = isRecord2(message.params) ? message.params : {};
|
|
3475
3703
|
const detail = isRecord2(params.error) && typeof params.error.message === "string" ? params.error.message : "Codex turn error";
|
|
@@ -3516,13 +3744,19 @@ var VirtualCodexAgent = class {
|
|
|
3516
3744
|
if (this.threadId) {
|
|
3517
3745
|
try {
|
|
3518
3746
|
const sandbox2 = bridgeAgentSandboxMode(this.agent);
|
|
3519
|
-
await this.app.request("thread/resume", {
|
|
3747
|
+
const result2 = await this.app.request("thread/resume", {
|
|
3520
3748
|
threadId: this.threadId,
|
|
3521
3749
|
cwd: this.agent.cwd,
|
|
3522
3750
|
model: this.agent.model ?? null,
|
|
3523
3751
|
approvalPolicy: bridgeAgentApprovalPolicy(this.agent),
|
|
3524
3752
|
sandbox: sandbox2
|
|
3525
3753
|
});
|
|
3754
|
+
if (isRecord2(result2) && isRecord2(result2.thread)) {
|
|
3755
|
+
this.onNotification({ method: "thread/name/updated", params: {
|
|
3756
|
+
threadId: this.threadId,
|
|
3757
|
+
threadName: result2.thread.name
|
|
3758
|
+
} });
|
|
3759
|
+
}
|
|
3526
3760
|
return this.threadId;
|
|
3527
3761
|
} catch {
|
|
3528
3762
|
this.threadId = null;
|
|
@@ -3535,7 +3769,7 @@ var VirtualCodexAgent = class {
|
|
|
3535
3769
|
approvalPolicy: bridgeAgentApprovalPolicy(this.agent),
|
|
3536
3770
|
sandbox,
|
|
3537
3771
|
serviceName: "codex-intercom",
|
|
3538
|
-
developerInstructions: this.agent.instructions
|
|
3772
|
+
developerInstructions: [this.agent.instructions, TASK_TEAM_GUIDANCE].filter(Boolean).join("\n\n"),
|
|
3539
3773
|
threadSource: "cli"
|
|
3540
3774
|
});
|
|
3541
3775
|
this.threadId = getThreadId(result);
|
|
@@ -3548,7 +3782,7 @@ var VirtualCodexAgent = class {
|
|
|
3548
3782
|
routeMessage(from, message) {
|
|
3549
3783
|
const toolWaiter = this.toolReplyWaiters.get(message.replyTo ?? "");
|
|
3550
3784
|
if (toolWaiter) {
|
|
3551
|
-
if (from.id === toolWaiter.from) {
|
|
3785
|
+
if (from.id === toolWaiter.from && message.content.team === toolWaiter.team) {
|
|
3552
3786
|
this.toolReplyWaiters.delete(message.replyTo ?? "");
|
|
3553
3787
|
clearTimeout(toolWaiter.timeout);
|
|
3554
3788
|
toolWaiter.cleanup?.();
|
|
@@ -3594,7 +3828,7 @@ var VirtualCodexAgent = class {
|
|
|
3594
3828
|
this.waiters.delete(turnId);
|
|
3595
3829
|
const reply = this.finalMessages.get(turnId)?.trim() || "Codex turn completed without a final message.";
|
|
3596
3830
|
for (const waiter of waiters) {
|
|
3597
|
-
await this.client.send(waiter.from.id, { text: reply, replyTo: waiter.message.id }).catch((error) => {
|
|
3831
|
+
await this.client.send(waiter.from.id, { text: reply, replyTo: waiter.message.id, team: waiter.message.content.team }).catch((error) => {
|
|
3598
3832
|
process.stderr.write(`reply failed for ${this.agent.id}: ${error instanceof Error ? error.message : String(error)}
|
|
3599
3833
|
`);
|
|
3600
3834
|
});
|
|
@@ -3606,12 +3840,16 @@ var VirtualCodexAgent = class {
|
|
|
3606
3840
|
const lowerTo = send.to.toLowerCase();
|
|
3607
3841
|
const remaining = [];
|
|
3608
3842
|
for (const waiter of waiters) {
|
|
3843
|
+
if (send.team !== void 0 && send.team !== waiter.message.content.team) {
|
|
3844
|
+
remaining.push(waiter);
|
|
3845
|
+
continue;
|
|
3846
|
+
}
|
|
3609
3847
|
const matchesSender = send.to === waiter.from.id || waiter.from.id.startsWith(send.to) || waiter.from.name?.toLowerCase() === lowerTo;
|
|
3610
3848
|
if (!matchesSender) {
|
|
3611
3849
|
remaining.push(waiter);
|
|
3612
3850
|
continue;
|
|
3613
3851
|
}
|
|
3614
|
-
await this.client.send(waiter.from.id, { text: send.message, replyTo: waiter.message.id }).catch((error) => {
|
|
3852
|
+
await this.client.send(waiter.from.id, { text: send.message, replyTo: waiter.message.id, team: waiter.message.content.team }).catch((error) => {
|
|
3615
3853
|
remaining.push(waiter);
|
|
3616
3854
|
process.stderr.write(`reply failed for ${this.agent.id}: ${error instanceof Error ? error.message : String(error)}
|
|
3617
3855
|
`);
|
|
@@ -3676,8 +3914,32 @@ name: ${this.agent.name}
|
|
|
3676
3914
|
cwd: ${this.agent.cwd}`,
|
|
3677
3915
|
{ session_id: this.agent.id, name: this.agent.name, cwd: this.agent.cwd, model: this.agent.model ?? "codex-app-server" }
|
|
3678
3916
|
);
|
|
3917
|
+
case "intercom_join": {
|
|
3918
|
+
if (args.name === void 0) {
|
|
3919
|
+
if (args.create || args.members !== void 0 || args.work !== void 0) throw new Error("Creating or extending a team requires a name");
|
|
3920
|
+
return textToolResult(formatJoinableNamedTeamList(listNamedTeams()));
|
|
3921
|
+
}
|
|
3922
|
+
if (args.members !== void 0 && !Array.isArray(args.members)) throw new Error("members must be an array");
|
|
3923
|
+
const sessions = await this.client.listSessions();
|
|
3924
|
+
const members = Array.from(args.members ?? [], (member) => {
|
|
3925
|
+
const id = resolveSessionTarget(sessions, asString(member, "member"));
|
|
3926
|
+
if (!id) throw new Error("Team member is not connected");
|
|
3927
|
+
return id;
|
|
3928
|
+
});
|
|
3929
|
+
const team = await appendNamedTeamMembership({ name: asString(args.name, "name"), selfId: this.agent.id, members, create: args.create === true, ...args.work === void 0 ? {} : { work: asString(args.work, "work") } });
|
|
3930
|
+
const roster = namedTeamRoster(team, this.agent.id, sessions);
|
|
3931
|
+
return textToolResult(formatNamedTeamRoster(roster), { ok: true, team: team.name, roster });
|
|
3932
|
+
}
|
|
3679
3933
|
case "intercom_team": {
|
|
3680
3934
|
const sessions = await this.client.listSessions();
|
|
3935
|
+
const mine = sessionNamedTeams(this.agent.id);
|
|
3936
|
+
const requested = args.team === void 0 ? void 0 : asString(args.team, "team");
|
|
3937
|
+
const selected = requested ? mine.filter((entry) => entry.name === requested) : mine;
|
|
3938
|
+
if (requested && !selected.length) throw new Error("You do not belong to that team");
|
|
3939
|
+
if (selected.length) {
|
|
3940
|
+
const teams = selected.map((entry) => namedTeamRoster(entry, this.agent.id, sessions));
|
|
3941
|
+
return textToolResult(teams.map(formatNamedTeamRoster).join("\n\n"), { teams });
|
|
3942
|
+
}
|
|
3681
3943
|
const team = await resolveIntercomTeam({ selfId: this.agent.id, sessions });
|
|
3682
3944
|
return textToolResult(formatIntercomTeam(team), team);
|
|
3683
3945
|
}
|
|
@@ -3706,7 +3968,8 @@ Active sessions: ${sessions.length}`,
|
|
|
3706
3968
|
const to = asString(args.to, "to");
|
|
3707
3969
|
const message = asString(args.message, "message");
|
|
3708
3970
|
const sendTo = await this.resolveTarget(to);
|
|
3709
|
-
const
|
|
3971
|
+
const team = resolveNamedMessageTeam(this.agent.id, sendTo, args.team === void 0 ? void 0 : asString(args.team, "team"));
|
|
3972
|
+
const result = await this.client.send(sendTo, { text: message, team });
|
|
3710
3973
|
if (!result.delivered) {
|
|
3711
3974
|
return textToolResult(`Message to "${to}" was not delivered: ${result.reason ?? "Session may not exist or has disconnected."}`, { ok: false, message_id: result.id, reason: result.reason }, true);
|
|
3712
3975
|
}
|
|
@@ -3719,10 +3982,11 @@ Active sessions: ${sessions.length}`,
|
|
|
3719
3982
|
const message = asString(args.message, "message");
|
|
3720
3983
|
const timeoutMs = asOptionalPositiveInteger(args.timeout_ms, "timeout_ms") ?? DEFAULT_ASK_TIMEOUT_MS;
|
|
3721
3984
|
const sendTo = await this.resolveTarget(to);
|
|
3985
|
+
const team = resolveNamedMessageTeam(this.agent.id, sendTo, args.team === void 0 ? void 0 : asString(args.team, "team"));
|
|
3722
3986
|
const questionId = randomUUID4();
|
|
3723
|
-
const replyPromise = this.waitForToolReply(sendTo, questionId, timeoutMs, signal);
|
|
3987
|
+
const replyPromise = this.waitForToolReply(sendTo, questionId, timeoutMs, signal, team);
|
|
3724
3988
|
void replyPromise.catch(() => void 0);
|
|
3725
|
-
const result = await this.client.send(sendTo, { messageId: questionId, text: message, expectsReply: true });
|
|
3989
|
+
const result = await this.client.send(sendTo, { messageId: questionId, text: message, expectsReply: true, team });
|
|
3726
3990
|
if (!result.delivered) {
|
|
3727
3991
|
this.rejectToolReply(questionId, new Error(result.reason ?? "Session may not exist or has disconnected."));
|
|
3728
3992
|
return textToolResult(`Message to "${to}" was not delivered: ${result.reason ?? "Session may not exist or has disconnected."}`, { ok: false, message_id: result.id, reason: result.reason }, true);
|
|
@@ -3758,7 +4022,7 @@ ${reply.content.text}${formatAttachments(reply.content.attachments)}`, { ok: tru
|
|
|
3758
4022
|
const sessions = await this.client.listSessions();
|
|
3759
4023
|
return resolveSessionTarget(sessions, to) ?? to;
|
|
3760
4024
|
}
|
|
3761
|
-
waitForToolReply(from, replyTo, timeoutMs = DEFAULT_ASK_TIMEOUT_MS, signal) {
|
|
4025
|
+
waitForToolReply(from, replyTo, timeoutMs = DEFAULT_ASK_TIMEOUT_MS, signal, team) {
|
|
3762
4026
|
return new Promise((resolve5, reject) => {
|
|
3763
4027
|
if (signal?.aborted) {
|
|
3764
4028
|
reject(new Error("intercom_ask cancelled"));
|
|
@@ -3782,7 +4046,7 @@ ${reply.content.text}${formatAttachments(reply.content.attachments)}`, { ok: tru
|
|
|
3782
4046
|
reject(new Error(`No reply from "${from}" within ${Math.round(timeoutMs / 1e3)} seconds`));
|
|
3783
4047
|
}, timeoutMs);
|
|
3784
4048
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3785
|
-
this.toolReplyWaiters.set(replyTo, { from, resolve: resolve5, reject, timeout, cleanup });
|
|
4049
|
+
this.toolReplyWaiters.set(replyTo, { from, team, resolve: resolve5, reject, timeout, cleanup });
|
|
3786
4050
|
});
|
|
3787
4051
|
}
|
|
3788
4052
|
rejectToolReply(replyTo, error) {
|
|
@@ -4503,7 +4767,7 @@ async function waitForSocket(socketPath, proc, timeoutMs = 1e4) {
|
|
|
4503
4767
|
throw new Error(`Codex app-server exited before creating ${socketPath}`);
|
|
4504
4768
|
}
|
|
4505
4769
|
if (existsSync6(socketPath)) return;
|
|
4506
|
-
await
|
|
4770
|
+
await delay3(50);
|
|
4507
4771
|
}
|
|
4508
4772
|
throw new Error(`Timed out waiting for Codex app-server socket: ${socketPath}`);
|
|
4509
4773
|
}
|
|
@@ -4627,16 +4891,16 @@ function cleanupOldCoiStateFiles(intercomDir, now = Date.now(), maxAgeMs = COI_S
|
|
|
4627
4891
|
}
|
|
4628
4892
|
for (const entry of entries) {
|
|
4629
4893
|
if (!/^coi-.+-state\.json$/.test(entry)) continue;
|
|
4630
|
-
const path =
|
|
4894
|
+
const path = join10(intercomDir, entry);
|
|
4631
4895
|
try {
|
|
4632
4896
|
const stat = statSync(path);
|
|
4633
|
-
if (now - stat.mtimeMs > maxAgeMs)
|
|
4897
|
+
if (now - stat.mtimeMs > maxAgeMs) rmSync2(path, { force: true });
|
|
4634
4898
|
} catch {
|
|
4635
4899
|
}
|
|
4636
4900
|
}
|
|
4637
4901
|
}
|
|
4638
4902
|
function resetCoiStateForFreshStart(statePath, fresh) {
|
|
4639
|
-
if (fresh)
|
|
4903
|
+
if (fresh) rmSync2(statePath, { force: true });
|
|
4640
4904
|
}
|
|
4641
4905
|
async function runCoi(options, env = process.env) {
|
|
4642
4906
|
const scopeId = intercomScopeIdFromEnv(env);
|
|
@@ -4663,11 +4927,11 @@ async function runCoi(options, env = process.env) {
|
|
|
4663
4927
|
const name = options.name ?? identity.name;
|
|
4664
4928
|
const intercomDir = getIntercomDirPath();
|
|
4665
4929
|
cleanupOldCoiStateFiles(intercomDir);
|
|
4666
|
-
const socketPath = options.socketPath ??
|
|
4667
|
-
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`);
|
|
4668
4932
|
const fresh = env.AGENT_INTERCOM_FRESH === "1";
|
|
4669
4933
|
resetCoiStateForFreshStart(statePath, fresh);
|
|
4670
|
-
|
|
4934
|
+
rmSync2(socketPath, { force: true });
|
|
4671
4935
|
const resumeRequest = resolveCoiResumeRequest(options.codexArgs);
|
|
4672
4936
|
const agent = {
|
|
4673
4937
|
id,
|
|
@@ -4692,7 +4956,7 @@ async function runCoi(options, env = process.env) {
|
|
|
4692
4956
|
const cleanup = async () => {
|
|
4693
4957
|
await daemon?.stop().catch(() => void 0);
|
|
4694
4958
|
await stopChild(appServer);
|
|
4695
|
-
|
|
4959
|
+
rmSync2(socketPath, { force: true });
|
|
4696
4960
|
};
|
|
4697
4961
|
let daemon = null;
|
|
4698
4962
|
let cleaned = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctliz/agent-intercom-codex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Codex MCP plugin and wakeable worker for local messaging with Pi, Claude Code, OpenCode, Grok Build, and AGY agents.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
".codex-plugin/**/*",
|
|
39
39
|
".mcp.json",
|
|
40
40
|
"README.md",
|
|
41
|
+
"CHANGELOG.md",
|
|
41
42
|
"LICENSE",
|
|
42
43
|
"THIRD_PARTY_NOTICES.md",
|
|
43
44
|
"licenses/**/*",
|
|
@@ -23,8 +23,8 @@ messages to those workers create or resume app-server threads and start turns.
|
|
|
23
23
|
## Tools
|
|
24
24
|
|
|
25
25
|
- `intercom_whoami`: show this session's intercom ID, name, cwd, and model.
|
|
26
|
-
- `intercom_team`:
|
|
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({
|