@ctliz/agent-intercom-pi 0.11.0-connect.2 → 0.12.0-connect.1

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.
Files changed (3) hide show
  1. package/index.ts +57 -10
  2. package/package.json +2 -2
  3. package/team.ts +188 -50
package/index.ts CHANGED
@@ -487,17 +487,50 @@ function parseSubagentIntercomPayload(payload: unknown): { to: string; message:
487
487
  const requestId = typeof record.requestId === "string" ? record.requestId : undefined;
488
488
  return { to: record.to, message: record.message, ...(requestId ? { requestId } : {}) };
489
489
  }
490
- function resolveIntercomPresenceName(sessionName: string | undefined, sessionId: string): string {
490
+
491
+ const GENERIC_SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
492
+
493
+ export function resolveIntercomSessionId(
494
+ fallbackSessionId: string,
495
+ environment: NodeJS.ProcessEnv = process.env,
496
+ ): string {
497
+ const harnessSessionId = environment[INTERCOM_SESSION_ID_ENV]?.trim();
498
+ if (harnessSessionId) {
499
+ return harnessSessionId;
500
+ }
501
+ const genericSessionId = environment.AGENT_INTERCOM_SESSION_ID?.trim();
502
+ if (genericSessionId) {
503
+ if (!GENERIC_SESSION_ID_PATTERN.test(genericSessionId)) {
504
+ throw new Error("Invalid AGENT_INTERCOM_SESSION_ID: must match ^[A-Za-z0-9_-]{1,128}$");
505
+ }
506
+ return genericSessionId;
507
+ }
508
+ return fallbackSessionId;
509
+ }
510
+
511
+ export function resolveIntercomPresenceName(
512
+ sessionName: string | undefined,
513
+ sessionId: string,
514
+ environment: NodeJS.ProcessEnv = process.env,
515
+ ): string {
491
516
  const trimmedName = sessionName?.trim();
492
517
  if (trimmedName) {
493
518
  return trimmedName;
494
519
  }
520
+ const subagentName = environment[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
521
+ if (subagentName) {
522
+ return subagentName;
523
+ }
524
+ const genericName = environment.AGENT_INTERCOM_SESSION_NAME?.trim();
525
+ if (genericName) {
526
+ return genericName;
527
+ }
495
528
  const normalizedSessionId = sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId;
496
529
  return `${DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX}-${normalizedSessionId.slice(0, 8)}`;
497
530
  }
498
- function buildPresenceIdentity(pi: ExtensionAPI, sessionId: string): { name: string } {
531
+ function buildPresenceIdentity(pi: ExtensionAPI, sessionId: string, environment: NodeJS.ProcessEnv = process.env): { name: string } {
499
532
  return {
500
- name: resolveIntercomPresenceName(pi.getSessionName(), sessionId),
533
+ name: resolveIntercomPresenceName(pi.getSessionName(), sessionId, environment),
501
534
  };
502
535
  }
503
536
  function formatSessionLabel(session: SessionInfo, duplicates: Set<string>, idPrefix: string): string {
@@ -623,6 +656,14 @@ function getNamePollMs(): number {
623
656
  }
624
657
  export default function piIntercomExtension(pi: ExtensionAPI) {
625
658
  const runtimeScopeId = intercomScopeIdFromEnvForRegistration();
659
+ const initialHarnessSessionId = process.env[INTERCOM_SESSION_ID_ENV]?.trim();
660
+ const initialGenericSessionId = process.env.AGENT_INTERCOM_SESSION_ID?.trim();
661
+ if (initialGenericSessionId && !initialHarnessSessionId) {
662
+ if (!GENERIC_SESSION_ID_PATTERN.test(initialGenericSessionId)) {
663
+ throw new Error("Invalid AGENT_INTERCOM_SESSION_ID: must match ^[A-Za-z0-9_-]{1,128}$");
664
+ }
665
+ }
666
+ const configuredSessionId = initialHarnessSessionId || initialGenericSessionId;
626
667
  let client: IntercomClient | null = null;
627
668
  const config: IntercomConfig = loadConfig();
628
669
  const bossTeamScope = readBossTeamScope();
@@ -739,8 +780,11 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
739
780
  return null;
740
781
  }
741
782
  try {
742
- if (currentSessionId && ctx.sessionManager.getSessionId() !== currentSessionId) {
743
- return null;
783
+ if (currentSessionId) {
784
+ const expectedSessionId = configuredSessionId || ctx.sessionManager.getSessionId();
785
+ if (expectedSessionId !== currentSessionId) {
786
+ return null;
787
+ }
744
788
  }
745
789
  void ctx.hasUI;
746
790
  return ctx;
@@ -789,11 +833,12 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
789
833
  ...(runtimeInstanceId ? { runtimeInstanceId } : {}),
790
834
  };
791
835
  }
792
- function syncPresenceIdentity(sessionId: string): void {
836
+ function syncPresenceIdentity(sessionId?: string): void {
793
837
  if (!client || !getLiveContext()) {
794
838
  return;
795
839
  }
796
- const identity = buildPresenceIdentity(pi, sessionId);
840
+ const resolvedId = currentSessionId ?? (configuredSessionId || (sessionId ? sessionId : "default"));
841
+ const identity = buildPresenceIdentity(pi, resolvedId);
797
842
  lastPresenceName = identity.name;
798
843
  client.updatePresence({ ...identity, status: currentStatus() });
799
844
  }
@@ -1343,7 +1388,8 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1343
1388
  rejectAllReplyWaiters(new Error("Session replaced"));
1344
1389
  replyTracker.reset();
1345
1390
  runtimeContext = ctx;
1346
- currentSessionId = ctx.sessionManager.getSessionId();
1391
+ currentSessionId = configuredSessionId || ctx.sessionManager.getSessionId();
1392
+ publishIntercomSessionId(currentSessionId);
1347
1393
  if (bossTeamScope.present && bossSelfSessionError(bossTeamScope, currentSessionId)) {
1348
1394
  const staleOutbox = new PersistentOutboundOutbox(currentSessionId);
1349
1395
  const removed = staleOutbox.list().length;
@@ -1666,7 +1712,8 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1666
1712
  scheduleInboundFlush(0);
1667
1713
  });
1668
1714
  pi.on("turn_start", (_event, ctx) => {
1669
- const sessionId = ctx.sessionManager.getSessionId();
1715
+ const rawSessionId = ctx.sessionManager.getSessionId();
1716
+ const sessionId = configuredSessionId || rawSessionId;
1670
1717
  if (!currentSessionId || sessionId !== currentSessionId) {
1671
1718
  if (!config.enabled) {
1672
1719
  return;
@@ -1688,7 +1735,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
1688
1735
  currentModel = event.model.id;
1689
1736
  if (client) {
1690
1737
  client.updatePresence({
1691
- ...buildPresenceIdentity(pi, ctx.sessionManager.getSessionId()),
1738
+ ...buildPresenceIdentity(pi, currentSessionId ?? (configuredSessionId || ctx.sessionManager.getSessionId())),
1692
1739
  model: event.model.id,
1693
1740
  status: currentStatus(),
1694
1741
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctliz/agent-intercom-pi",
3
- "version": "0.11.0-connect.2",
3
+ "version": "0.12.0-connect.1",
4
4
  "license": "AGPL-3.0-or-later",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -44,7 +44,7 @@
44
44
  ]
45
45
  },
46
46
  "dependencies": {
47
- "@ctliz/agent-intercom-core": "git+https://github.com/ctliz/agent-intercom-core.git#37e074970e2a9de32a16fc325607c3b476b0bd45",
47
+ "@ctliz/agent-intercom-core": "0.2.0",
48
48
  "@earendil-works/pi-ai": "*",
49
49
  "@earendil-works/pi-coding-agent": "*",
50
50
  "@earendil-works/pi-tui": "*",
package/team.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { readTeamManifest, TeamManifestError } from "@ctliz/agent-intercom-core/team-manifest";
3
4
  import { getAgentDirPath } from "./broker/paths.ts";
4
5
  import { bossSelfSessionError, resolveBossLiveSession, type BossTeamScope } from "./boss-team-scope.ts";
5
6
 
6
7
  export interface TeamSession {
7
8
  id: string;
8
9
  name?: string;
10
+ model?: string;
9
11
  origin?: "local" | "remote";
10
12
  }
11
13
 
@@ -29,9 +31,12 @@ export interface TeamMember {
29
31
  connected: boolean;
30
32
  }
31
33
 
34
+ export type IntercomTeamSource = "orchestrator" | "manifest" | "live_roster" | "standalone";
35
+
32
36
  export interface IntercomTeam {
37
+ source: IntercomTeamSource;
33
38
  teamId?: string;
34
- self: { id: string; workerId?: string; isManager: boolean };
39
+ self: { id: string; workerId?: string; isManager: boolean; role?: string };
35
40
  manager?: { target: string; connected: boolean };
36
41
  controller?: { target: string; connected: boolean };
37
42
  coworkers: TeamMember[];
@@ -46,6 +51,9 @@ export function resolveManagedInboxSession(input: {
46
51
  if (!input.team.self.isManager) {
47
52
  throw new Error("Only a manager may inspect another session's pending-ask inbox");
48
53
  }
54
+ if (input.team.source !== "orchestrator") {
55
+ throw new Error("Pending-ask inbox inspection is only available for orchestrator-managed teams");
56
+ }
49
57
  const member = input.team.coworkers.find((entry) => entry.target === input.requestedSession);
50
58
  if (!member) {
51
59
  throw new Error(`Pending-ask inbox access denied for "${input.requestedSession}"; select an owned coworker target returned by intercom_team`);
@@ -80,6 +88,13 @@ async function readWorkers(agentDir: string): Promise<StoredWorker[]> {
80
88
  }
81
89
  }
82
90
 
91
+ /**
92
+ * Resolves the Intercom team following the exact priority hierarchy:
93
+ * 1. Authoritative Orchestrator record in workers.json (matching AGENT_INTERCOM_WORKER_ID or active manager ownership)
94
+ * 2. Explicit TmuxDeck Manifest (AGENT_INTERCOM_TEAM_MANIFEST)
95
+ * 3. Workspace Live Same-Scope Non-Human Roster Fallback (AGENT_INTERCOM_SCOPE_ID present)
96
+ * 4. Standalone unmanaged session
97
+ */
83
98
  export async function resolveIntercomTeam(input: {
84
99
  selfId: string;
85
100
  sessions: TeamSession[];
@@ -93,39 +108,184 @@ export async function resolveIntercomTeam(input: {
93
108
  const current = workerId
94
109
  ? workers.find((worker) => stringValue(worker.id) === workerId && (!runId || stringValue(worker.runId) === runId))
95
110
  : undefined;
96
- const managerTarget = stringValue(current?.managerSessionId) ?? stringValue(env.AGENT_INTERCOM_MANAGER_TARGET) ?? stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
97
- const teamId = managerTarget ?? input.selfId;
98
- const isManager = !managerTarget;
99
- const coworkers = workers
100
- .filter((worker) => worker.owned === true)
101
- .filter((worker) => stringValue(worker.managerSessionId) === teamId)
102
- .filter((worker) => LIVE_STATES.has(stringValue(worker.state) ?? ""))
103
- .filter((worker) => stringValue(worker.id) !== workerId)
104
- .map((worker): TeamMember | undefined => {
105
- const id = stringValue(worker.id);
106
- if (!id) return undefined;
107
- const target = stringValue(worker.intercomTarget) ?? id;
111
+
112
+ // 1. Authoritative Orchestrator Record
113
+ if (current) {
114
+ const managerTarget =
115
+ stringValue(current.managerSessionId) ??
116
+ stringValue(env.AGENT_INTERCOM_MANAGER_TARGET) ??
117
+ stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
118
+
119
+ const coworkers: TeamMember[] = managerTarget
120
+ ? workers
121
+ .filter((worker) => worker.owned === true)
122
+ .filter((worker) => stringValue(worker.managerSessionId) === managerTarget)
123
+ .filter((worker) => LIVE_STATES.has(stringValue(worker.state) ?? ""))
124
+ .filter((worker) => stringValue(worker.id) !== workerId)
125
+ .map((worker): TeamMember | undefined => {
126
+ const id = stringValue(worker.id);
127
+ if (!id) return undefined;
128
+ const target = stringValue(worker.intercomTarget) ?? id;
129
+ return {
130
+ id,
131
+ target,
132
+ ...(stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {}),
133
+ ...(stringValue(worker.role) ? { role: stringValue(worker.role) } : {}),
134
+ ...(stringValue(worker.state) ? { state: stringValue(worker.state) } : {}),
135
+ connected: connectedTo(input.sessions, target),
136
+ };
137
+ })
138
+ .filter((member): member is TeamMember => Boolean(member))
139
+ : [];
140
+
141
+ return {
142
+ source: "orchestrator",
143
+ ...(managerTarget ? { teamId: managerTarget } : {}),
144
+ self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager: false },
145
+ ...(managerTarget
146
+ ? { manager: { target: managerTarget, connected: connectedTo(input.sessions, managerTarget) } }
147
+ : {}),
148
+ coworkers,
149
+ };
150
+ }
151
+
152
+ if (workerId === undefined && input.selfId) {
153
+ const managerOwnedCoworkers = workers
154
+ .filter((worker) => worker.owned === true)
155
+ .filter((worker) => stringValue(worker.managerSessionId) === input.selfId)
156
+ .filter((worker) => LIVE_STATES.has(stringValue(worker.state) ?? ""))
157
+ .map((worker): TeamMember | undefined => {
158
+ const id = stringValue(worker.id);
159
+ if (!id) return undefined;
160
+ const target = stringValue(worker.intercomTarget) ?? id;
161
+ return {
162
+ id,
163
+ target,
164
+ ...(stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {}),
165
+ ...(stringValue(worker.role) ? { role: stringValue(worker.role) } : {}),
166
+ ...(stringValue(worker.state) ? { state: stringValue(worker.state) } : {}),
167
+ connected: connectedTo(input.sessions, target),
168
+ };
169
+ })
170
+ .filter((member): member is TeamMember => Boolean(member));
171
+
172
+ if (managerOwnedCoworkers.length > 0) {
108
173
  return {
109
- id,
110
- target,
111
- ...(stringValue(worker.harness) ? { harness: stringValue(worker.harness) } : {}),
112
- ...(stringValue(worker.role) ? { role: stringValue(worker.role) } : {}),
113
- ...(stringValue(worker.state) ? { state: stringValue(worker.state) } : {}),
114
- connected: connectedTo(input.sessions, target),
174
+ source: "orchestrator",
175
+ teamId: input.selfId,
176
+ self: { id: input.selfId, isManager: true },
177
+ manager: { target: input.selfId, connected: true },
178
+ coworkers: managerOwnedCoworkers,
115
179
  };
116
- })
117
- .filter((member): member is TeamMember => Boolean(member));
180
+ }
181
+ }
118
182
 
183
+ // 2. Explicit TmuxDeck Team Manifest
184
+ if (env.AGENT_INTERCOM_TEAM_MANIFEST !== undefined) {
185
+ const rawManifestPath = env.AGENT_INTERCOM_TEAM_MANIFEST;
186
+ if (typeof rawManifestPath !== "string" || !rawManifestPath.trim()) {
187
+ throw new TeamManifestError("ERR_TEAM_MANIFEST_INVALID");
188
+ }
189
+ const manifest = readTeamManifest(rawManifestPath.trim());
190
+ if (!manifest) {
191
+ throw new TeamManifestError("ERR_TEAM_MANIFEST_INVALID");
192
+ }
193
+
194
+ const selfMember = manifest.members.find((member) => member.sessionId === input.selfId);
195
+ if (!selfMember) {
196
+ throw new TeamManifestError("ERR_TEAM_MANIFEST_INVALID");
197
+ }
198
+
199
+ const isManager = input.selfId === manifest.leadId;
200
+ const managerTarget = manifest.leadId;
201
+
202
+ // Coworkers:
203
+ // Lead: all workers
204
+ // Worker: other workers (excluding self and excluding Lead/manager)
205
+ const coworkers: TeamMember[] = manifest.members
206
+ .filter((member) => member.sessionId !== input.selfId && member.sessionId !== manifest.leadId)
207
+ .map((member) => ({
208
+ id: member.sessionId,
209
+ target: member.sessionId,
210
+ role: member.role,
211
+ connected: connectedTo(input.sessions, member.sessionId),
212
+ }));
213
+
214
+ return {
215
+ source: "manifest",
216
+ teamId: manifest.runId,
217
+ self: {
218
+ id: input.selfId,
219
+ isManager,
220
+ role: selfMember.role,
221
+ },
222
+ manager: { target: managerTarget, connected: connectedTo(input.sessions, managerTarget) },
223
+ coworkers,
224
+ };
225
+ }
226
+
227
+ // 3. Workspace Live Same-Scope Non-Human Roster Fallback
228
+ const scopeId = stringValue(env.AGENT_INTERCOM_SCOPE_ID);
229
+ if (scopeId !== undefined) {
230
+ const managerTarget =
231
+ stringValue(env.AGENT_INTERCOM_MANAGER_TARGET) ??
232
+ stringValue(env.AGENT_INTERCOM_MANAGER_SESSION_ID);
233
+ const isManager = !managerTarget || managerTarget === input.selfId;
234
+ const effectiveManagerTarget = isManager ? input.selfId : managerTarget;
235
+
236
+ const coworkers: TeamMember[] = input.sessions
237
+ .filter((session) => session.id !== input.selfId)
238
+ .filter((session) => session.model !== "human")
239
+ .filter((session) => isManager || session.id !== effectiveManagerTarget)
240
+ .map((session) => ({
241
+ id: session.id,
242
+ target: session.id,
243
+ connected: true,
244
+ }));
245
+
246
+ return {
247
+ source: "live_roster",
248
+ teamId: effectiveManagerTarget,
249
+ self: { id: input.selfId, isManager },
250
+ manager: {
251
+ target: effectiveManagerTarget,
252
+ connected: connectedTo(input.sessions, effectiveManagerTarget),
253
+ },
254
+ coworkers,
255
+ };
256
+ }
257
+
258
+ // 4. Standalone Session Fallback
119
259
  return {
120
- teamId,
121
- self: { id: input.selfId, ...(workerId ? { workerId } : {}), isManager },
122
- manager: managerTarget
123
- ? { target: managerTarget, connected: connectedTo(input.sessions, managerTarget) }
124
- : { target: input.selfId, connected: true },
125
- coworkers,
260
+ source: "standalone",
261
+ self: { id: input.selfId, isManager: false },
262
+ coworkers: [],
126
263
  };
127
264
  }
128
265
 
266
+ export function formatIntercomTeam(team: IntercomTeam): string {
267
+ const manager = team.manager
268
+ ? `${team.manager.target} [${team.manager.connected ? "connected" : "not connected"}]`
269
+ : "unknown";
270
+ const lines = [
271
+ `Manager: ${manager}`,
272
+ `You: ${team.self.id}${team.self.isManager ? " [manager]" : ""}`,
273
+ ];
274
+ if (team.controller) {
275
+ lines.push(`Controller: ${team.controller.target} [connected]`);
276
+ }
277
+ if (team.coworkers.length === 0) {
278
+ lines.push("Coworkers: none");
279
+ } else {
280
+ lines.push("Coworkers:");
281
+ for (const coworker of team.coworkers) {
282
+ const metadata = [coworker.harness, coworker.role, coworker.state].filter(Boolean).join(", ");
283
+ lines.push(`- ${coworker.id} target=${coworker.target}${metadata ? ` (${metadata})` : ""} [${coworker.connected ? "connected" : "not connected"}]`);
284
+ }
285
+ }
286
+ return lines.join("\n");
287
+ }
288
+
129
289
  export function resolveBossIntercomTeam(input: {
130
290
  selfId: string;
131
291
  sessions: TeamSession[];
@@ -160,6 +320,7 @@ export function resolveBossIntercomTeam(input: {
160
320
  }
161
321
 
162
322
  return {
323
+ source: "orchestrator",
163
324
  ...(manager ? { teamId: manager.target } : {}),
164
325
  self: { id: input.selfId, isManager },
165
326
  ...(manager ? { manager } : {}),
@@ -167,26 +328,3 @@ export function resolveBossIntercomTeam(input: {
167
328
  coworkers,
168
329
  };
169
330
  }
170
-
171
- export function formatIntercomTeam(team: IntercomTeam): string {
172
- const manager = team.manager
173
- ? `${team.manager.target} [${team.manager.connected ? "connected" : "not connected"}]`
174
- : "unknown";
175
- const lines = [
176
- `Manager: ${manager}`,
177
- `You: ${team.self.id}${team.self.isManager ? " [manager]" : ""}`,
178
- ];
179
- if (team.controller) {
180
- lines.push(`Controller: ${team.controller.target} [connected]`);
181
- }
182
- if (team.coworkers.length === 0) {
183
- lines.push("Coworkers: none");
184
- } else {
185
- lines.push("Coworkers:");
186
- for (const coworker of team.coworkers) {
187
- const metadata = [coworker.harness, coworker.role, coworker.state].filter(Boolean).join(", ");
188
- lines.push(`- ${coworker.id} target=${coworker.target}${metadata ? ` (${metadata})` : ""} [${coworker.connected ? "connected" : "not connected"}]`);
189
- }
190
- }
191
- return lines.join("\n");
192
- }