@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/bridge-daemon.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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=bridge-daemon sourceSha256=04013522326f3042c72f6454a9c79e26d2d1c8de7af57769ed890e50fca514e3\n");
|
|
3
3
|
|
|
4
4
|
// codex/bridge-daemon.ts
|
|
5
5
|
import { once } from "node:events";
|
|
@@ -1566,6 +1566,7 @@ function isMessage(value) {
|
|
|
1566
1566
|
if (typeof content.text !== "string") {
|
|
1567
1567
|
return false;
|
|
1568
1568
|
}
|
|
1569
|
+
if (content.team !== void 0 && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
|
|
1569
1570
|
return content.attachments === void 0 || Array.isArray(content.attachments) && content.attachments.every(isAttachment);
|
|
1570
1571
|
}
|
|
1571
1572
|
function isSessionInfo(value) {
|
|
@@ -2156,6 +2157,7 @@ var IntercomClient = class extends EventEmitter2 {
|
|
|
2156
2157
|
expectsReply: options.expectsReply,
|
|
2157
2158
|
content: {
|
|
2158
2159
|
text: options.text,
|
|
2160
|
+
...options.team === void 0 ? {} : { team: options.team },
|
|
2159
2161
|
attachments: options.attachments
|
|
2160
2162
|
}
|
|
2161
2163
|
};
|
|
@@ -3064,6 +3066,213 @@ function formatIntercomTeam(team) {
|
|
|
3064
3066
|
return lines.join("\n");
|
|
3065
3067
|
}
|
|
3066
3068
|
|
|
3069
|
+
// codex/named-teams.ts
|
|
3070
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3071
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
3072
|
+
import { join as join8 } from "node:path";
|
|
3073
|
+
var NAMED_TEAMS_FILE = "named-teams.json";
|
|
3074
|
+
var NAMED_TEAMS_VERSION = 1;
|
|
3075
|
+
var NAMED_TEAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
3076
|
+
function isNamedTeamScope(value) {
|
|
3077
|
+
if (value.length !== 48) return false;
|
|
3078
|
+
for (const char of value) {
|
|
3079
|
+
if (!(char >= "0" && char <= "9" || char >= "a" && char <= "f")) return false;
|
|
3080
|
+
}
|
|
3081
|
+
return true;
|
|
3082
|
+
}
|
|
3083
|
+
function teamsFilePath(agentDir) {
|
|
3084
|
+
return join8(getIntercomDirPath(agentDir ?? getAgentDirPath()), NAMED_TEAMS_FILE);
|
|
3085
|
+
}
|
|
3086
|
+
function genericReadError() {
|
|
3087
|
+
return new Error("Could not read the local named-team list.");
|
|
3088
|
+
}
|
|
3089
|
+
function genericWriteError() {
|
|
3090
|
+
return new Error("Could not create that named team.");
|
|
3091
|
+
}
|
|
3092
|
+
function isPlainObject(value) {
|
|
3093
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3094
|
+
}
|
|
3095
|
+
function parseStoredTeam(value) {
|
|
3096
|
+
if (!isPlainObject(value)) return void 0;
|
|
3097
|
+
if (typeof value.name !== "string" || !NAMED_TEAM_NAME_PATTERN.test(value.name)) return void 0;
|
|
3098
|
+
if (typeof value.scopeId !== "string" || !isNamedTeamScope(value.scopeId)) return void 0;
|
|
3099
|
+
if (typeof value.managerSessionId !== "string" || !value.managerSessionId.trim()) return void 0;
|
|
3100
|
+
if (value.managerSessionId !== value.managerSessionId.trim() || /[\u0000-\u001f\u007f]/.test(value.managerSessionId)) {
|
|
3101
|
+
return void 0;
|
|
3102
|
+
}
|
|
3103
|
+
if (typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt <= 0) {
|
|
3104
|
+
return void 0;
|
|
3105
|
+
}
|
|
3106
|
+
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;
|
|
3107
|
+
if (value.work !== void 0 && (typeof value.work !== "string" || !value.work.trim() || value.work.length > 2e3)) return void 0;
|
|
3108
|
+
return {
|
|
3109
|
+
name: value.name,
|
|
3110
|
+
scopeId: value.scopeId,
|
|
3111
|
+
managerSessionId: value.managerSessionId,
|
|
3112
|
+
createdAt: value.createdAt,
|
|
3113
|
+
...value.memberSessionIds === void 0 ? {} : { memberSessionIds: value.memberSessionIds },
|
|
3114
|
+
...value.work === void 0 ? {} : { work: value.work }
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
function parseTeamName(raw) {
|
|
3118
|
+
const name = raw.trim();
|
|
3119
|
+
if (!name || name.includes(" ") || !NAMED_TEAM_NAME_PATTERN.test(name)) {
|
|
3120
|
+
throw new Error("Team names start with a letter and may include letters, numbers, hyphens, or underscores (max 32).");
|
|
3121
|
+
}
|
|
3122
|
+
return name;
|
|
3123
|
+
}
|
|
3124
|
+
function generateNamedTeamScope(existing = []) {
|
|
3125
|
+
const taken = new Set(existing);
|
|
3126
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
3127
|
+
const scopeId = randomBytes2(24).toString("hex");
|
|
3128
|
+
if (!taken.has(scopeId)) return scopeId;
|
|
3129
|
+
}
|
|
3130
|
+
throw genericWriteError();
|
|
3131
|
+
}
|
|
3132
|
+
function listNamedTeams(agentDir) {
|
|
3133
|
+
let raw;
|
|
3134
|
+
try {
|
|
3135
|
+
raw = readFileSync8(teamsFilePath(agentDir), "utf8");
|
|
3136
|
+
} catch (error) {
|
|
3137
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return [];
|
|
3138
|
+
throw genericReadError();
|
|
3139
|
+
}
|
|
3140
|
+
let parsed;
|
|
3141
|
+
try {
|
|
3142
|
+
parsed = JSON.parse(raw);
|
|
3143
|
+
} catch {
|
|
3144
|
+
throw genericReadError();
|
|
3145
|
+
}
|
|
3146
|
+
if (!isPlainObject(parsed) || parsed.version !== NAMED_TEAMS_VERSION || !Array.isArray(parsed.teams)) {
|
|
3147
|
+
throw genericReadError();
|
|
3148
|
+
}
|
|
3149
|
+
const teams = [];
|
|
3150
|
+
const names = /* @__PURE__ */ new Set();
|
|
3151
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
3152
|
+
for (const entry of parsed.teams) {
|
|
3153
|
+
const team = parseStoredTeam(entry);
|
|
3154
|
+
if (!team || names.has(team.name) || scopes.has(team.scopeId)) throw genericReadError();
|
|
3155
|
+
names.add(team.name);
|
|
3156
|
+
scopes.add(team.scopeId);
|
|
3157
|
+
teams.push(team);
|
|
3158
|
+
}
|
|
3159
|
+
return teams;
|
|
3160
|
+
}
|
|
3161
|
+
function formatJoinableNamedTeamList(teams) {
|
|
3162
|
+
if (teams.length === 0) {
|
|
3163
|
+
return 'No joinable named teams found.\nCreate one with intercom_join({ name: "billing", create: true }).';
|
|
3164
|
+
}
|
|
3165
|
+
return [
|
|
3166
|
+
"Joinable named teams:",
|
|
3167
|
+
...teams.map((team, index) => ` ${index + 1}) ${team.name}`)
|
|
3168
|
+
].join("\n");
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
// codex/named-team-membership.ts
|
|
3172
|
+
import { mkdirSync as mkdirSync5, rmSync } from "node:fs";
|
|
3173
|
+
import { join as join9 } from "node:path";
|
|
3174
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
3175
|
+
function namedTeamMemberIds(team) {
|
|
3176
|
+
return team.memberSessionIds ?? [team.managerSessionId];
|
|
3177
|
+
}
|
|
3178
|
+
function sessionNamedTeams(sessionId, agentDir) {
|
|
3179
|
+
return listNamedTeams(agentDir).filter((team) => namedTeamMemberIds(team).includes(sessionId));
|
|
3180
|
+
}
|
|
3181
|
+
async function appendNamedTeamMembership(input) {
|
|
3182
|
+
const name = parseTeamName(input.name);
|
|
3183
|
+
const memberIds = [.../* @__PURE__ */ new Set([input.selfId, ...input.members ?? []])];
|
|
3184
|
+
if (memberIds.some((id) => !id || id.trim() !== id || /[\u0000-\u001f\u007f]/.test(id))) {
|
|
3185
|
+
throw new Error("Invalid team member session ID");
|
|
3186
|
+
}
|
|
3187
|
+
if (input.work !== void 0 && (!input.work.trim() || input.work.length > 2e3)) {
|
|
3188
|
+
throw new Error("Work must be a non-empty task description (max 2000 characters)");
|
|
3189
|
+
}
|
|
3190
|
+
const dir = getIntercomDirPath(input.agentDir ?? getAgentDirPath());
|
|
3191
|
+
ensureIntercomRuntimeDir(dir);
|
|
3192
|
+
const lock = join9(dir, "named-teams.lock");
|
|
3193
|
+
const deadline = Date.now() + 5e3;
|
|
3194
|
+
for (; ; ) {
|
|
3195
|
+
try {
|
|
3196
|
+
mkdirSync5(lock, { mode: 448 });
|
|
3197
|
+
break;
|
|
3198
|
+
} catch (error) {
|
|
3199
|
+
if (error.code !== "EEXIST") throw error;
|
|
3200
|
+
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.");
|
|
3201
|
+
await delay2(25);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
try {
|
|
3205
|
+
const teams = listNamedTeams(input.agentDir);
|
|
3206
|
+
let team = teams.find((entry) => entry.name === name);
|
|
3207
|
+
if (input.create) {
|
|
3208
|
+
if (team) throw new Error(`A named team called ${name} already exists; join it instead.`);
|
|
3209
|
+
team = { name, scopeId: generateNamedTeamScope(teams.map((entry) => entry.scopeId)), managerSessionId: input.selfId, createdAt: Date.now() };
|
|
3210
|
+
teams.push(team);
|
|
3211
|
+
}
|
|
3212
|
+
if (!team) throw new Error(`Team "${name}" does not exist; create it explicitly.`);
|
|
3213
|
+
if (memberIds.some((id) => id !== input.selfId) && team.managerSessionId !== input.selfId) {
|
|
3214
|
+
throw new Error(`Only the manager of team "${name}" may add other sessions`);
|
|
3215
|
+
}
|
|
3216
|
+
if (input.work !== void 0 && !input.create) {
|
|
3217
|
+
throw new Error("Work is set when creating a team; use a new team for a different task");
|
|
3218
|
+
}
|
|
3219
|
+
const updated = {
|
|
3220
|
+
...team,
|
|
3221
|
+
memberSessionIds: [.../* @__PURE__ */ new Set([...namedTeamMemberIds(team), ...memberIds])],
|
|
3222
|
+
...input.work === void 0 ? {} : { work: input.work.trim() }
|
|
3223
|
+
};
|
|
3224
|
+
writeDurableJson(join9(dir, NAMED_TEAMS_FILE), {
|
|
3225
|
+
version: NAMED_TEAMS_VERSION,
|
|
3226
|
+
teams: teams.map((entry) => entry.name === name ? updated : entry)
|
|
3227
|
+
});
|
|
3228
|
+
return updated;
|
|
3229
|
+
} finally {
|
|
3230
|
+
rmSync(lock, { recursive: true });
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
function requireNamedTeamMembers(name, selfId, peerId, agentDir) {
|
|
3234
|
+
const team = listNamedTeams(agentDir).find((entry) => entry.name === name);
|
|
3235
|
+
if (!team) throw new Error(`Unknown team "${name}"`);
|
|
3236
|
+
const members = namedTeamMemberIds(team);
|
|
3237
|
+
if (!members.includes(selfId) || !members.includes(peerId)) {
|
|
3238
|
+
throw new Error(`Both sessions must belong to team "${name}" before messaging`);
|
|
3239
|
+
}
|
|
3240
|
+
return team;
|
|
3241
|
+
}
|
|
3242
|
+
function resolveNamedMessageTeam(selfId, peerId, requested, agentDir) {
|
|
3243
|
+
if (requested !== void 0) return requireNamedTeamMembers(requested, selfId, peerId, agentDir).name;
|
|
3244
|
+
const mine = sessionNamedTeams(selfId, agentDir);
|
|
3245
|
+
const shared = mine.filter((team) => namedTeamMemberIds(team).includes(peerId));
|
|
3246
|
+
if (shared.length === 1) return shared[0].name;
|
|
3247
|
+
if (shared.length > 1) throw new Error("Multiple shared teams; specify `team` for this task");
|
|
3248
|
+
return void 0;
|
|
3249
|
+
}
|
|
3250
|
+
function namedTeamRoster(team, selfId, sessions) {
|
|
3251
|
+
return {
|
|
3252
|
+
name: team.name,
|
|
3253
|
+
...team.work ? { work: team.work } : {},
|
|
3254
|
+
self: { id: selfId, isManager: selfId === team.managerSessionId },
|
|
3255
|
+
manager: { target: team.managerSessionId, connected: sessions.some((entry) => entry.id === team.managerSessionId) },
|
|
3256
|
+
members: namedTeamMemberIds(team).map((id) => ({
|
|
3257
|
+
id,
|
|
3258
|
+
target: id,
|
|
3259
|
+
name: sessions.find((entry) => entry.id === id)?.name,
|
|
3260
|
+
role: id === team.managerSessionId ? "manager" : "member",
|
|
3261
|
+
connected: sessions.some((entry) => entry.id === id)
|
|
3262
|
+
}))
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
function formatNamedTeamRoster(team) {
|
|
3266
|
+
return [
|
|
3267
|
+
`Team: ${team.name}`,
|
|
3268
|
+
...team.work ? [`Work: ${team.work}`] : [],
|
|
3269
|
+
`You: ${team.self.id}${team.self.isManager ? " [manager]" : ""}`,
|
|
3270
|
+
`Manager: ${team.manager.target}${team.manager.connected ? "" : " [offline]"}`,
|
|
3271
|
+
"Members:",
|
|
3272
|
+
...team.members.map((member) => `- ${member.name || member.id} (${member.id}) [${member.role}]${member.connected ? "" : " [offline]"}`)
|
|
3273
|
+
].join("\n");
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3067
3276
|
// codex/runtime.ts
|
|
3068
3277
|
function formatAttachments(attachments) {
|
|
3069
3278
|
if (!attachments?.length) return "";
|
|
@@ -3119,10 +3328,20 @@ function formatSessionList(sessions, currentSessionId, currentCwd) {
|
|
|
3119
3328
|
}).join("\n");
|
|
3120
3329
|
}
|
|
3121
3330
|
|
|
3331
|
+
// codex/team-guidance.ts
|
|
3332
|
+
var TASK_TEAM_GUIDANCE = `Intercom task-team rules:
|
|
3333
|
+
- 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.
|
|
3334
|
+
- 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.
|
|
3335
|
+
- 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.
|
|
3336
|
+
- 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.
|
|
3337
|
+
- 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.
|
|
3338
|
+
- 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.`;
|
|
3339
|
+
|
|
3122
3340
|
// codex/bridge-daemon.ts
|
|
3123
3341
|
var APPROVED_INTERCOM_TOOLS = /* @__PURE__ */ new Set([
|
|
3124
3342
|
"intercom_whoami",
|
|
3125
3343
|
"intercom_team",
|
|
3344
|
+
"intercom_join",
|
|
3126
3345
|
"intercom_status",
|
|
3127
3346
|
"intercom_list",
|
|
3128
3347
|
"intercom_set_summary",
|
|
@@ -3156,6 +3375,7 @@ ${agent.instructions}` : "";
|
|
|
3156
3375
|
return [
|
|
3157
3376
|
`Intercom message for ${agent.name}.`,
|
|
3158
3377
|
`From: ${formatSessionDisplay(from)} (${from.id})`,
|
|
3378
|
+
...message.content.team ? [`[Team: ${message.content.team}]`] : [],
|
|
3159
3379
|
`Message id: ${message.id}`,
|
|
3160
3380
|
"",
|
|
3161
3381
|
message.content.text,
|
|
@@ -3251,7 +3471,7 @@ function intercomSendFromArgs(rawArgs) {
|
|
|
3251
3471
|
} catch {
|
|
3252
3472
|
return null;
|
|
3253
3473
|
}
|
|
3254
|
-
return typeof args.to === "string" && typeof args.message === "string" ? { to: args.to, message: args.message } : null;
|
|
3474
|
+
return typeof args.to === "string" && typeof args.message === "string" ? { to: args.to, message: args.message, ...typeof args.team === "string" ? { team: args.team } : {} } : null;
|
|
3255
3475
|
}
|
|
3256
3476
|
function getCompletedIntercomSend(params) {
|
|
3257
3477
|
if (!params || typeof params !== "object") return null;
|
|
@@ -3279,7 +3499,7 @@ function asOptionalPositiveInteger(value, name) {
|
|
|
3279
3499
|
return validateAskTimeoutMs(value, name);
|
|
3280
3500
|
}
|
|
3281
3501
|
function normalizeToolName(name) {
|
|
3282
|
-
const mcpMatch = name.match(/(?:^|__|\.)intercom_(whoami|status|list|set_summary|send|ask|pending|reply)$/);
|
|
3502
|
+
const mcpMatch = name.match(/(?:^|__|\.)intercom_(whoami|team|join|status|list|set_summary|send|ask|pending|reply)$/);
|
|
3283
3503
|
if (mcpMatch) return `intercom_${mcpMatch[1]}`;
|
|
3284
3504
|
return name;
|
|
3285
3505
|
}
|
|
@@ -3402,7 +3622,7 @@ var VirtualCodexAgent = class {
|
|
|
3402
3622
|
}
|
|
3403
3623
|
scheduleReconnect() {
|
|
3404
3624
|
if (!this.reconnectEnabled || this.reconnectTimer) return;
|
|
3405
|
-
const
|
|
3625
|
+
const delay3 = this.reconnectDelays[Math.min(this.reconnectAttempt, this.reconnectDelays.length - 1)];
|
|
3406
3626
|
this.reconnectTimer = setTimeout(() => {
|
|
3407
3627
|
this.reconnectTimer = null;
|
|
3408
3628
|
void this.connectIntercom().then(() => {
|
|
@@ -3416,7 +3636,7 @@ var VirtualCodexAgent = class {
|
|
|
3416
3636
|
`);
|
|
3417
3637
|
this.scheduleReconnect();
|
|
3418
3638
|
});
|
|
3419
|
-
},
|
|
3639
|
+
}, delay3);
|
|
3420
3640
|
this.reconnectTimer.unref?.();
|
|
3421
3641
|
}
|
|
3422
3642
|
clearReconnectTimer() {
|
|
@@ -3455,6 +3675,14 @@ var VirtualCodexAgent = class {
|
|
|
3455
3675
|
onNotification(message) {
|
|
3456
3676
|
const threadId = getNotificationThreadId(message.params);
|
|
3457
3677
|
if (!threadId || threadId !== this.threadId) return;
|
|
3678
|
+
if (message.method === "thread/name/updated" && isRecord2(message.params)) {
|
|
3679
|
+
const name = message.params.threadName;
|
|
3680
|
+
if (typeof name === "string" && name.trim() && name !== this.agent.name) {
|
|
3681
|
+
this.agent.name = name;
|
|
3682
|
+
this.client.updatePresence({ name });
|
|
3683
|
+
}
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3458
3686
|
if (message.method === "error") {
|
|
3459
3687
|
const params = isRecord2(message.params) ? message.params : {};
|
|
3460
3688
|
const detail = isRecord2(params.error) && typeof params.error.message === "string" ? params.error.message : "Codex turn error";
|
|
@@ -3501,13 +3729,19 @@ var VirtualCodexAgent = class {
|
|
|
3501
3729
|
if (this.threadId) {
|
|
3502
3730
|
try {
|
|
3503
3731
|
const sandbox2 = bridgeAgentSandboxMode(this.agent);
|
|
3504
|
-
await this.app.request("thread/resume", {
|
|
3732
|
+
const result2 = await this.app.request("thread/resume", {
|
|
3505
3733
|
threadId: this.threadId,
|
|
3506
3734
|
cwd: this.agent.cwd,
|
|
3507
3735
|
model: this.agent.model ?? null,
|
|
3508
3736
|
approvalPolicy: bridgeAgentApprovalPolicy(this.agent),
|
|
3509
3737
|
sandbox: sandbox2
|
|
3510
3738
|
});
|
|
3739
|
+
if (isRecord2(result2) && isRecord2(result2.thread)) {
|
|
3740
|
+
this.onNotification({ method: "thread/name/updated", params: {
|
|
3741
|
+
threadId: this.threadId,
|
|
3742
|
+
threadName: result2.thread.name
|
|
3743
|
+
} });
|
|
3744
|
+
}
|
|
3511
3745
|
return this.threadId;
|
|
3512
3746
|
} catch {
|
|
3513
3747
|
this.threadId = null;
|
|
@@ -3520,7 +3754,7 @@ var VirtualCodexAgent = class {
|
|
|
3520
3754
|
approvalPolicy: bridgeAgentApprovalPolicy(this.agent),
|
|
3521
3755
|
sandbox,
|
|
3522
3756
|
serviceName: "codex-intercom",
|
|
3523
|
-
developerInstructions: this.agent.instructions
|
|
3757
|
+
developerInstructions: [this.agent.instructions, TASK_TEAM_GUIDANCE].filter(Boolean).join("\n\n"),
|
|
3524
3758
|
threadSource: "cli"
|
|
3525
3759
|
});
|
|
3526
3760
|
this.threadId = getThreadId(result);
|
|
@@ -3533,7 +3767,7 @@ var VirtualCodexAgent = class {
|
|
|
3533
3767
|
routeMessage(from, message) {
|
|
3534
3768
|
const toolWaiter = this.toolReplyWaiters.get(message.replyTo ?? "");
|
|
3535
3769
|
if (toolWaiter) {
|
|
3536
|
-
if (from.id === toolWaiter.from) {
|
|
3770
|
+
if (from.id === toolWaiter.from && message.content.team === toolWaiter.team) {
|
|
3537
3771
|
this.toolReplyWaiters.delete(message.replyTo ?? "");
|
|
3538
3772
|
clearTimeout(toolWaiter.timeout);
|
|
3539
3773
|
toolWaiter.cleanup?.();
|
|
@@ -3579,7 +3813,7 @@ var VirtualCodexAgent = class {
|
|
|
3579
3813
|
this.waiters.delete(turnId);
|
|
3580
3814
|
const reply = this.finalMessages.get(turnId)?.trim() || "Codex turn completed without a final message.";
|
|
3581
3815
|
for (const waiter of waiters) {
|
|
3582
|
-
await this.client.send(waiter.from.id, { text: reply, replyTo: waiter.message.id }).catch((error) => {
|
|
3816
|
+
await this.client.send(waiter.from.id, { text: reply, replyTo: waiter.message.id, team: waiter.message.content.team }).catch((error) => {
|
|
3583
3817
|
process.stderr.write(`reply failed for ${this.agent.id}: ${error instanceof Error ? error.message : String(error)}
|
|
3584
3818
|
`);
|
|
3585
3819
|
});
|
|
@@ -3591,12 +3825,16 @@ var VirtualCodexAgent = class {
|
|
|
3591
3825
|
const lowerTo = send.to.toLowerCase();
|
|
3592
3826
|
const remaining = [];
|
|
3593
3827
|
for (const waiter of waiters) {
|
|
3828
|
+
if (send.team !== void 0 && send.team !== waiter.message.content.team) {
|
|
3829
|
+
remaining.push(waiter);
|
|
3830
|
+
continue;
|
|
3831
|
+
}
|
|
3594
3832
|
const matchesSender = send.to === waiter.from.id || waiter.from.id.startsWith(send.to) || waiter.from.name?.toLowerCase() === lowerTo;
|
|
3595
3833
|
if (!matchesSender) {
|
|
3596
3834
|
remaining.push(waiter);
|
|
3597
3835
|
continue;
|
|
3598
3836
|
}
|
|
3599
|
-
await this.client.send(waiter.from.id, { text: send.message, replyTo: waiter.message.id }).catch((error) => {
|
|
3837
|
+
await this.client.send(waiter.from.id, { text: send.message, replyTo: waiter.message.id, team: waiter.message.content.team }).catch((error) => {
|
|
3600
3838
|
remaining.push(waiter);
|
|
3601
3839
|
process.stderr.write(`reply failed for ${this.agent.id}: ${error instanceof Error ? error.message : String(error)}
|
|
3602
3840
|
`);
|
|
@@ -3661,8 +3899,32 @@ name: ${this.agent.name}
|
|
|
3661
3899
|
cwd: ${this.agent.cwd}`,
|
|
3662
3900
|
{ session_id: this.agent.id, name: this.agent.name, cwd: this.agent.cwd, model: this.agent.model ?? "codex-app-server" }
|
|
3663
3901
|
);
|
|
3902
|
+
case "intercom_join": {
|
|
3903
|
+
if (args.name === void 0) {
|
|
3904
|
+
if (args.create || args.members !== void 0 || args.work !== void 0) throw new Error("Creating or extending a team requires a name");
|
|
3905
|
+
return textToolResult(formatJoinableNamedTeamList(listNamedTeams()));
|
|
3906
|
+
}
|
|
3907
|
+
if (args.members !== void 0 && !Array.isArray(args.members)) throw new Error("members must be an array");
|
|
3908
|
+
const sessions = await this.client.listSessions();
|
|
3909
|
+
const members = Array.from(args.members ?? [], (member) => {
|
|
3910
|
+
const id = resolveSessionTarget(sessions, asString(member, "member"));
|
|
3911
|
+
if (!id) throw new Error("Team member is not connected");
|
|
3912
|
+
return id;
|
|
3913
|
+
});
|
|
3914
|
+
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") } });
|
|
3915
|
+
const roster = namedTeamRoster(team, this.agent.id, sessions);
|
|
3916
|
+
return textToolResult(formatNamedTeamRoster(roster), { ok: true, team: team.name, roster });
|
|
3917
|
+
}
|
|
3664
3918
|
case "intercom_team": {
|
|
3665
3919
|
const sessions = await this.client.listSessions();
|
|
3920
|
+
const mine = sessionNamedTeams(this.agent.id);
|
|
3921
|
+
const requested = args.team === void 0 ? void 0 : asString(args.team, "team");
|
|
3922
|
+
const selected = requested ? mine.filter((entry) => entry.name === requested) : mine;
|
|
3923
|
+
if (requested && !selected.length) throw new Error("You do not belong to that team");
|
|
3924
|
+
if (selected.length) {
|
|
3925
|
+
const teams = selected.map((entry) => namedTeamRoster(entry, this.agent.id, sessions));
|
|
3926
|
+
return textToolResult(teams.map(formatNamedTeamRoster).join("\n\n"), { teams });
|
|
3927
|
+
}
|
|
3666
3928
|
const team = await resolveIntercomTeam({ selfId: this.agent.id, sessions });
|
|
3667
3929
|
return textToolResult(formatIntercomTeam(team), team);
|
|
3668
3930
|
}
|
|
@@ -3691,7 +3953,8 @@ Active sessions: ${sessions.length}`,
|
|
|
3691
3953
|
const to = asString(args.to, "to");
|
|
3692
3954
|
const message = asString(args.message, "message");
|
|
3693
3955
|
const sendTo = await this.resolveTarget(to);
|
|
3694
|
-
const
|
|
3956
|
+
const team = resolveNamedMessageTeam(this.agent.id, sendTo, args.team === void 0 ? void 0 : asString(args.team, "team"));
|
|
3957
|
+
const result = await this.client.send(sendTo, { text: message, team });
|
|
3695
3958
|
if (!result.delivered) {
|
|
3696
3959
|
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);
|
|
3697
3960
|
}
|
|
@@ -3704,10 +3967,11 @@ Active sessions: ${sessions.length}`,
|
|
|
3704
3967
|
const message = asString(args.message, "message");
|
|
3705
3968
|
const timeoutMs = asOptionalPositiveInteger(args.timeout_ms, "timeout_ms") ?? DEFAULT_ASK_TIMEOUT_MS;
|
|
3706
3969
|
const sendTo = await this.resolveTarget(to);
|
|
3970
|
+
const team = resolveNamedMessageTeam(this.agent.id, sendTo, args.team === void 0 ? void 0 : asString(args.team, "team"));
|
|
3707
3971
|
const questionId = randomUUID4();
|
|
3708
|
-
const replyPromise = this.waitForToolReply(sendTo, questionId, timeoutMs, signal);
|
|
3972
|
+
const replyPromise = this.waitForToolReply(sendTo, questionId, timeoutMs, signal, team);
|
|
3709
3973
|
void replyPromise.catch(() => void 0);
|
|
3710
|
-
const result = await this.client.send(sendTo, { messageId: questionId, text: message, expectsReply: true });
|
|
3974
|
+
const result = await this.client.send(sendTo, { messageId: questionId, text: message, expectsReply: true, team });
|
|
3711
3975
|
if (!result.delivered) {
|
|
3712
3976
|
this.rejectToolReply(questionId, new Error(result.reason ?? "Session may not exist or has disconnected."));
|
|
3713
3977
|
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);
|
|
@@ -3743,7 +4007,7 @@ ${reply.content.text}${formatAttachments(reply.content.attachments)}`, { ok: tru
|
|
|
3743
4007
|
const sessions = await this.client.listSessions();
|
|
3744
4008
|
return resolveSessionTarget(sessions, to) ?? to;
|
|
3745
4009
|
}
|
|
3746
|
-
waitForToolReply(from, replyTo, timeoutMs = DEFAULT_ASK_TIMEOUT_MS, signal) {
|
|
4010
|
+
waitForToolReply(from, replyTo, timeoutMs = DEFAULT_ASK_TIMEOUT_MS, signal, team) {
|
|
3747
4011
|
return new Promise((resolve4, reject) => {
|
|
3748
4012
|
if (signal?.aborted) {
|
|
3749
4013
|
reject(new Error("intercom_ask cancelled"));
|
|
@@ -3767,7 +4031,7 @@ ${reply.content.text}${formatAttachments(reply.content.attachments)}`, { ok: tru
|
|
|
3767
4031
|
reject(new Error(`No reply from "${from}" within ${Math.round(timeoutMs / 1e3)} seconds`));
|
|
3768
4032
|
}, timeoutMs);
|
|
3769
4033
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3770
|
-
this.toolReplyWaiters.set(replyTo, { from, resolve: resolve4, reject, timeout, cleanup });
|
|
4034
|
+
this.toolReplyWaiters.set(replyTo, { from, team, resolve: resolve4, reject, timeout, cleanup });
|
|
3771
4035
|
});
|
|
3772
4036
|
}
|
|
3773
4037
|
rejectToolReply(replyTo, error) {
|
package/dist/broker.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.
|
|
1
|
+
process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.13.0 target=broker sourceSha256=04013522326f3042c72f6454a9c79e26d2d1c8de7af57769ed890e50fca514e3\n");
|
|
2
2
|
|
|
3
3
|
// broker/broker.ts
|
|
4
4
|
import net from "net";
|
|
@@ -1291,6 +1291,7 @@ function isMessage(value) {
|
|
|
1291
1291
|
if (typeof content.text !== "string" || Buffer.byteLength(content.text, "utf-8") > MAX_MESSAGE_TEXT_BYTES) {
|
|
1292
1292
|
return false;
|
|
1293
1293
|
}
|
|
1294
|
+
if (content.team !== void 0 && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
|
|
1294
1295
|
return content.attachments === void 0 || Array.isArray(content.attachments) && content.attachments.length <= MAX_ATTACHMENTS && content.attachments.every(isAttachment);
|
|
1295
1296
|
}
|
|
1296
1297
|
function isSessionId(value) {
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@ctliz/agent-intercom-codex",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"sourceSha256": "
|
|
4
|
+
"version": "0.13.0",
|
|
5
|
+
"sourceSha256": "04013522326f3042c72f6454a9c79e26d2d1c8de7af57769ed890e50fca514e3",
|
|
6
6
|
"targets": [
|
|
7
7
|
"codex-server",
|
|
8
8
|
"broker",
|