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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
package/src/utils/messaging.ts
CHANGED
|
@@ -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 {
|
|
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.");
|