pi-extended-teams 2.3.3 → 2.4.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.
@@ -122,7 +122,7 @@ export function createAgentCommunicationTools(options: AgentCommunicationToolsOp
122
122
  {
123
123
  name: "read_inbox",
124
124
  label: "Read Inbox",
125
- description: "Read this agent's inbox in the current Pi session.",
125
+ description: "Read this agent's inbox with current sender message-admission status. Status is a snapshot, not proof of process liveness; do not send to unavailable senders.",
126
126
  parameters: Type.Object({
127
127
  unread_only: Type.Optional(Type.Boolean({ default: true })),
128
128
  mark_as_read: Type.Optional(Type.Boolean({ default: true, description: "Set false to peek without marking messages read." })),
@@ -131,7 +131,7 @@ export function createAgentCommunicationTools(options: AgentCommunicationToolsOp
131
131
  const teamName = requireCurrentSession(options);
132
132
  const markAsRead = params.mark_as_read !== false;
133
133
  const unreadOnly = params.unread_only !== false;
134
- const msgs = await messaging.readInbox(teamName, options.agentName, unreadOnly, markAsRead);
134
+ const msgs = await messaging.readInboxWithSenderStatus(teamName, options.agentName, unreadOnly, markAsRead);
135
135
  const lifecycleRunId = options.getLifecycleRunId();
136
136
  // Do not fail after read flags are persisted; telemetry is best-effort.
137
137
  if (markAsRead && lifecycleRunId) {
@@ -333,7 +333,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
333
333
  pi.registerTool({
334
334
  name: "read_inbox",
335
335
  label: "Read Inbox",
336
- description: "Read messages from the current Pi session inbox. Defaults to this agent's inbox.",
336
+ description: "Read messages from the current Pi session inbox with current sender message-admission status. Defaults to your inbox. Status is a snapshot, not proof of process liveness; do not send to unavailable senders.",
337
337
  parameters: Type.Object({
338
338
  agent_name: Type.Optional(Type.String({ description: "Whose inbox to read. Defaults to your own." })),
339
339
  unread_only: Type.Optional(Type.Boolean({ default: true })),
@@ -345,7 +345,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
345
345
  if (process.env.PI_EXTENDED_TEAMS_HERDR_RESUME === "1" && targetAgent !== options.agentName) throw new Error("A resumed subagent can only read its own inbox.");
346
346
  const markAsRead = params.mark_as_read !== false;
347
347
  const unreadOnly = params.unread_only !== false;
348
- const msgs = await messaging.readInbox(targetTeamName, targetAgent, unreadOnly, markAsRead);
348
+ const msgs = await messaging.readInboxWithSenderStatus(targetTeamName, targetAgent, unreadOnly, markAsRead);
349
349
 
350
350
  if (markAsRead && options.isTeammate && targetAgent === options.agentName) {
351
351
  const config = await teams.readConfig(targetTeamName).catch(() => null);
@@ -104,7 +104,11 @@ export function summarizeInboxMessage(message: any): string {
104
104
  const from = message?.from || "unknown";
105
105
  const summary = message?.summary || "message";
106
106
  const timestamp = message?.timestamp ? new Date(message.timestamp).toLocaleTimeString() : "";
107
- return `${from}${timestamp ? ` ${timestamp}` : ""}: ${summary}`;
107
+ const sender = message?.senderStatus;
108
+ const availability = sender
109
+ ? ` [sender now: ${sender.status}; can message: ${sender.canMessage === null ? "unknown" : sender.canMessage ? "yes" : "no"}]`
110
+ : "";
111
+ return `${from}${availability}${timestamp ? ` ${timestamp}` : ""}: ${summary}`;
108
112
  }
109
113
 
110
114
  export function formatInboxMessagesForModel(messages: any[]): string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extended-teams",
3
- "version": "2.3.3",
3
+ "version": "2.4.0",
4
4
  "description": "Control-first, session-connected subagents for Pi with live navigation and intent-tier routing",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,7 +7,12 @@ import { InboxMessage, TeamConfig } from "./models";
7
7
  import { withLock } from "./lock";
8
8
  import { configPath, inboxPath } from "./paths";
9
9
  import { readConfig } from "./teams";
10
- import { assertLifecycleTombstoneAbsent, withLifecycleTombstoneLock } from "./lifecycle-tombstone";
10
+ import {
11
+ assertLifecycleTombstoneAbsent,
12
+ readLifecycleTombstoneSnapshot,
13
+ withLifecycleTombstoneLock,
14
+ type LifecycleTombstonePhase,
15
+ } from "./lifecycle-tombstone";
11
16
 
12
17
  export interface MessageMetadataOptions {
13
18
  id?: string;
@@ -272,6 +277,57 @@ export async function readInbox(
272
277
  });
273
278
  }
274
279
 
280
+ export interface InboxSenderStatus {
281
+ status: "lead" | "active" | "not_running" | "quarantined" | "unknown" | LifecycleTombstonePhase;
282
+ canMessage: boolean | null;
283
+ }
284
+
285
+ export async function readInboxWithSenderStatus(
286
+ teamName: string,
287
+ agentName: string,
288
+ unreadOnly = false,
289
+ markAsRead = true
290
+ ): Promise<Array<InboxMessage & { senderStatus: InboxSenderStatus }>> {
291
+ const messages = await readInbox(teamName, agentName, unreadOnly, markAsRead);
292
+ if (messages.length === 0) return [];
293
+
294
+ // Advisory admission snapshot, not process liveness or a reservation to send.
295
+ // Enrich only returned copies: persisted inbox history must not cache live status.
296
+ let members: TeamConfig["members"] | undefined;
297
+ try {
298
+ const config = JSON.parse(fs.readFileSync(configPath(teamName), "utf-8")) as TeamConfig;
299
+ if (Array.isArray(config.members)) members = config.members;
300
+ } catch (error) {
301
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") members = [];
302
+ }
303
+
304
+ const senders = new Map<string, InboxSenderStatus>();
305
+ for (const sender of new Set(messages.map(message => message.from))) {
306
+ let senderStatus: InboxSenderStatus = { status: "unknown", canMessage: null };
307
+ try {
308
+ if (sender === "team-lead") {
309
+ senderStatus = { status: "lead", canMessage: true };
310
+ } else {
311
+ const fence = readLifecycleTombstoneSnapshot(teamName, sender);
312
+ if (fence.status !== "absent") {
313
+ senderStatus = {
314
+ status: fence.status === "corrupt" ? "quarantined" : fence.tombstone.phase,
315
+ canMessage: false,
316
+ };
317
+ } else if (members) {
318
+ const member = members.find(member => member.name === sender);
319
+ const canMessage = !!member && member.isActive !== false;
320
+ senderStatus = { status: canMessage ? "active" : "not_running", canMessage };
321
+ }
322
+ }
323
+ } catch {
324
+ // Status lookup must not discard messages after read flags were saved.
325
+ }
326
+ senders.set(sender, senderStatus);
327
+ }
328
+ return messages.map(message => ({ ...message, senderStatus: senders.get(message.from)! }));
329
+ }
330
+
275
331
  export async function markInboxMessagesRead(teamName: string, agentName: string, ids: string[]): Promise<void> {
276
332
  const p = inboxPath(teamName, agentName);
277
333
  if (!fs.existsSync(p)) throw new Error("Referenced inbox is unavailable.");