@trim21/personal-pi-extensions 0.0.208 → 0.0.210

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/src/talk/core.ts CHANGED
@@ -3,13 +3,13 @@
3
3
  * storage backend, and yields deliveries and notifications to an adapter
4
4
  * through events. Pi-free — the pi adapter (index.ts) owns the pi API surface.
5
5
  *
6
- * Visibility model: groups are the only visibility boundary. A session in a
7
- * group sees only its co-members; a session in no group sees only itself.
6
+ * Visibility model: groups are the only visibility boundary. An agent in a
7
+ * group sees only its co-members; an agent in no group sees only itself.
8
8
  * Membership is read live from storage on every operation, so joining or
9
- * leaving a group takes effect immediately for every session.
9
+ * leaving a group takes effect immediately for every agent.
10
10
  *
11
11
  * Delivery model: a letter is removed from the inbox only AFTER the adapter
12
- * reports it was handed to the session (`events.deliver` returns true). A
12
+ * reports it was handed to the agent (`events.deliver` returns true). A
13
13
  * letter whose delivery fails stays in the inbox and is retried on the next
14
14
  * poll — so a swallowed sendMessage error no longer destroys the letter.
15
15
  */
@@ -17,7 +17,7 @@
17
17
  import { age, formatListing, refusalUnknown, shortAddr } from "./format.js";
18
18
  import {
19
19
  deleteGroup,
20
- groupForSession,
20
+ groupForAgent,
21
21
  isValidGroupName,
22
22
  listGroups,
23
23
  newGroupId,
@@ -45,12 +45,12 @@ import {
45
45
  } from "./mailbox.js";
46
46
  import { inboundAccepts, OutboundPolicy } from "./policy.js";
47
47
  import {
48
+ type AgentRecord,
48
49
  listRecords,
49
50
  type Presence,
50
51
  presenceOf,
51
52
  readRecord,
52
53
  readStartTime,
53
- type SessionRecord,
54
54
  sweep,
55
55
  writeRecord,
56
56
  } from "./registry.js";
@@ -58,7 +58,7 @@ import type { TalkStorage } from "./storage.js";
58
58
 
59
59
  type AskOutcome =
60
60
  { replied: true; body: string; from: string } | { replied: false; reason: string };
61
- type TargetResult = { ok: true; record: SessionRecord } | { ok: false; error: string };
61
+ type TargetResult = { ok: true; record: AgentRecord } | { ok: false; error: string };
62
62
  type SendResult = { ok: true; letter: Letter; verdict: string } | { ok: false; error: string };
63
63
 
64
64
  export interface TalkCoreEvents {
@@ -84,14 +84,14 @@ const SWEEP_INTERVAL_MS = 30 * 60 * 1000;
84
84
  * Mutual-ask arbitration: true when the peer asked first. The `ts` fields of
85
85
  * the two ask letters are fixed values inside the letters, so both sides
86
86
  * compare the same pair and reach symmetric conclusions. On a same-ms
87
- * collision, `cwd + sessionId` breaks the tie deterministically.
87
+ * collision, `cwd + agentId` breaks the tie deterministically.
88
88
  */
89
89
  export function peerAskedFirst(
90
- peer: { ts: number; cwd: string; sessionId: string },
91
- self: { ts: number; cwd: string; sessionId: string },
90
+ peer: { ts: number; cwd: string; agentId: string },
91
+ self: { ts: number; cwd: string; agentId: string },
92
92
  ): boolean {
93
- const peerKey = `${peer.cwd}\u0000${peer.sessionId}`;
94
- const selfKey = `${self.cwd}\u0000${self.sessionId}`;
93
+ const peerKey = `${peer.cwd}\u0000${peer.agentId}`;
94
+ const selfKey = `${self.cwd}\u0000${self.agentId}`;
95
95
  return peer.ts < self.ts || (peer.ts === self.ts && peerKey < selfKey);
96
96
  }
97
97
 
@@ -100,7 +100,7 @@ export class TalkCore {
100
100
  private readonly events: TalkCoreEvents;
101
101
  private readonly now: () => number;
102
102
 
103
- private self: SessionRecord | undefined;
103
+ private self: AgentRecord | undefined;
104
104
  private readonly policy = new OutboundPolicy();
105
105
  private readonly askWaiters = new Map<string, (outcome: AskOutcome) => void>();
106
106
  private readonly watched = new Map<string, Presence>();
@@ -124,7 +124,7 @@ export class TalkCore {
124
124
  return this.self?.addr;
125
125
  }
126
126
 
127
- private requireSelf(): SessionRecord {
127
+ private requireSelf(): AgentRecord {
128
128
  const self = this.self;
129
129
  if (!self) throw new Error("Talk core is not started");
130
130
  return self;
@@ -132,7 +132,7 @@ export class TalkCore {
132
132
 
133
133
  // ── Lifecycle ──────────────────────────────────────────────────────────
134
134
 
135
- async start(self: SessionRecord): Promise<void> {
135
+ async start(self: AgentRecord): Promise<void> {
136
136
  await this.storage.init();
137
137
  // Record the process start time so presence can rule out pid reuse later.
138
138
  const pidStart = readStartTime(self.pid);
@@ -141,7 +141,7 @@ export class TalkCore {
141
141
  try {
142
142
  await sweep(this.storage, this.now());
143
143
  } catch {
144
- // sweep failure never breaks the session
144
+ // sweep failure never breaks the agent
145
145
  }
146
146
  this.startInboxPoll();
147
147
  // Reclaim dead records periodically, not just at startup.
@@ -150,7 +150,7 @@ export class TalkCore {
150
150
  }, SWEEP_INTERVAL_MS);
151
151
  this.sweeper.unref();
152
152
  // Drain mail queued while offline — deferred: delivering during
153
- // session_start races the session's own first turn.
153
+ // session_start races the agent's own first turn.
154
154
  const initial = setTimeout(() => {
155
155
  void this.checkInbox();
156
156
  }, INITIAL_DRAIN_DELAY_MS);
@@ -183,18 +183,18 @@ export class TalkCore {
183
183
  void this.writeSelf({ status: "idle" });
184
184
  }
185
185
 
186
- setSessionName(name: string): void {
186
+ setAgentName(name: string): void {
187
187
  void this.writeSelf({ name });
188
188
  }
189
189
 
190
- private async writeSelf(patch: Partial<SessionRecord>): Promise<void> {
190
+ private async writeSelf(patch: Partial<AgentRecord>): Promise<void> {
191
191
  if (!this.self) return;
192
- // A dead session pins lastSeenAt to 0 so no later event re-freshens it.
192
+ // A dead agent pins lastSeenAt to 0 so no later event re-freshens it.
193
193
  this.self = { ...this.self, ...patch, lastSeenAt: this.dead ? 0 : this.now() };
194
194
  try {
195
195
  await writeRecord(this.storage, this.self);
196
196
  } catch {
197
- // registration failures never break the session
197
+ // registration failures never break the agent
198
198
  }
199
199
  }
200
200
 
@@ -297,32 +297,32 @@ export class TalkCore {
297
297
  // ── Outbound ───────────────────────────────────────────────────────────
298
298
 
299
299
  /**
300
- * Session ids of the caller's group members, or null when the caller is in
300
+ * Agent ids of the caller's group members, or null when the caller is in
301
301
  * no group. Visibility is read live from storage on every operation, so a
302
- * group change takes effect immediately for every session.
302
+ * group change takes effect immediately for every agent.
303
303
  */
304
304
  private async myGroupMemberIds(): Promise<Set<string> | null> {
305
305
  const self = this.requireSelf();
306
- const group = await groupForSession(this.storage, self.sessionId);
306
+ const group = await groupForAgent(this.storage, self.agentId);
307
307
  return group ? new Set(group.members) : null;
308
308
  }
309
309
 
310
310
  /**
311
- * Resolve a target by its exact session id (uuid). Only co-members of the
312
- * caller's group are reachable; a session in no group sees no peers at all.
311
+ * Resolve a target by its exact agent id (uuid). Only co-members of the
312
+ * caller's group are reachable; an agent in no group sees no peers at all.
313
313
  */
314
314
  private async resolveTarget(to: string): Promise<TargetResult> {
315
315
  const self = this.requireSelf();
316
316
  const records = await listRecords(this.storage);
317
317
  const memberIds = await this.myGroupMemberIds();
318
- const others = records.filter((r) => r.addr !== self.addr && memberIds?.has(r.sessionId));
319
- const target = others.find((r) => r.sessionId === to);
318
+ const others = records.filter((r) => r.addr !== self.addr && memberIds?.has(r.agentId));
319
+ const target = others.find((r) => r.agentId === to);
320
320
  if (!target) return { ok: false, error: refusalUnknown(to) };
321
321
  return { ok: true, record: target };
322
322
  }
323
323
 
324
324
  private async sendLetter(
325
- target: SessionRecord,
325
+ target: AgentRecord,
326
326
  kind: LetterKind,
327
327
  body: string,
328
328
  replyTo?: string,
@@ -337,7 +337,7 @@ export class TalkCore {
337
337
  if (!verdict.ok) return { ok: false, error: verdict.reason };
338
338
  const letter: Letter = {
339
339
  id: newMessageId(),
340
- from: { addr: self.addr, name: self.name, cwd: self.cwd, sessionId: self.sessionId },
340
+ from: { addr: self.addr, name: self.name, cwd: self.cwd, agentId: self.agentId },
341
341
  kind,
342
342
  body,
343
343
  ts: this.now(),
@@ -351,9 +351,7 @@ export class TalkCore {
351
351
  ok: true,
352
352
  letter,
353
353
  verdict:
354
- receipt === "delivered"
355
- ? "delivered"
356
- : "queued (waits on disk until the session resumes)",
354
+ receipt === "delivered" ? "delivered" : "queued (waits on disk until the agent resumes)",
357
355
  };
358
356
  }
359
357
  return {
@@ -407,7 +405,7 @@ export class TalkCore {
407
405
  * - the later ask yields: its waiter is settled with a "peer asked first"
408
406
  * reason, and the peer's ask is delivered so this side answers it first.
409
407
  *
410
- * On a same-millisecond ts collision, session dir + session id (carried in
408
+ * On a same-millisecond ts collision, agent dir + agent id (carried in
411
409
  * `letter.from`) breaks the tie deterministically — both sides compute the
412
410
  * same comparison and reach symmetric conclusions.
413
411
  */
@@ -418,8 +416,8 @@ export class TalkCore {
418
416
  const waiter = this.askWaiters.get(myAsk.askId);
419
417
  if (!waiter) return;
420
418
  const peerFirst = peerAskedFirst(
421
- { ts: letter.ts, cwd: letter.from.cwd, sessionId: letter.from.sessionId },
422
- { ts: myAsk.ts, cwd: self.cwd, sessionId: self.sessionId },
419
+ { ts: letter.ts, cwd: letter.from.cwd, agentId: letter.from.agentId },
420
+ { ts: myAsk.ts, cwd: self.cwd, agentId: self.agentId },
423
421
  );
424
422
  if (!peerFirst) return; // we asked first; keep waiting — the peer will yield
425
423
  this.askWaiters.delete(myAsk.askId);
@@ -433,10 +431,10 @@ export class TalkCore {
433
431
  // ── Tool actions ───────────────────────────────────────────────────────
434
432
 
435
433
  /**
436
- * JSON listing of visible sessions, including self (marked `self: true`).
437
- * Grouped sessions see only their co-members; a session in no group sees
434
+ * JSON listing of visible agents, including self (marked `self: true`).
435
+ * Grouped agents see only their co-members; an agent in no group sees
438
436
  * only itself. Every visible record is listed, live or offline; presence
439
- * decides the per-session status.
437
+ * decides the per-agent status.
440
438
  */
441
439
  async list(): Promise<string> {
442
440
  const self = this.requireSelf();
@@ -444,7 +442,7 @@ export class TalkCore {
444
442
  const memberIds = await this.myGroupMemberIds();
445
443
  const records = all.filter((r) => {
446
444
  if (r.addr === self.addr) return !this.dead;
447
- return memberIds?.has(r.sessionId) ?? false;
445
+ return memberIds?.has(r.agentId) ?? false;
448
446
  });
449
447
  return formatListing(records, self.addr, presenceOf);
450
448
  }
@@ -457,30 +455,30 @@ export class TalkCore {
457
455
  const filtered = records.filter((r) => {
458
456
  if (r.cwd !== cwd) return false;
459
457
  if (r.addr === self.addr) return !this.dead;
460
- return memberIds?.has(r.sessionId) ?? false;
458
+ return memberIds?.has(r.agentId) ?? false;
461
459
  });
462
460
  return formatListing(filtered, self.addr, presenceOf);
463
461
  }
464
462
 
465
463
  /** Visible peer records (excluding self), e.g. for command completions. */
466
- async listPeers(): Promise<SessionRecord[]> {
464
+ async listPeers(): Promise<AgentRecord[]> {
467
465
  const self = this.requireSelf();
468
466
  const records = await listRecords(this.storage);
469
467
  const memberIds = await this.myGroupMemberIds();
470
- return records.filter((r) => r.addr !== self.addr && (memberIds?.has(r.sessionId) ?? false));
468
+ return records.filter((r) => r.addr !== self.addr && (memberIds?.has(r.agentId) ?? false));
471
469
  }
472
470
 
473
471
  /**
474
- * Mark a session as dead: set its offline flag and pin lastSeenAt to 0 so
472
+ * Mark an agent as dead: set its offline flag and pin lastSeenAt to 0 so
475
473
  * the next sweep reaps it (empty mailbox). Without a target, marks this
476
- * session — later writeSelf calls no longer refresh lastSeenAt, and the
474
+ * agent — later writeSelf calls no longer refresh lastSeenAt, and the
477
475
  * record stays offline for peers.
478
476
  */
479
477
  async markDead(target?: string): Promise<string> {
480
478
  if (!target) {
481
479
  this.dead = true;
482
480
  await this.writeSelf({ offline: true });
483
- return "Marked this session as dead.";
481
+ return "Marked this agent as dead.";
484
482
  }
485
483
  const resolved = await this.resolveTarget(target);
486
484
  if (!resolved.ok) return resolved.error;
@@ -494,7 +492,7 @@ export class TalkCore {
494
492
  for (const peer of peers) {
495
493
  await writeRecord(this.storage, { ...peer, lastSeenAt: 0, offline: true });
496
494
  }
497
- return `Marked ${peers.length} session(s) as dead.`;
495
+ return `Marked ${peers.length} agent(s) as dead.`;
498
496
  }
499
497
 
500
498
  // ── Groups ─────────────────────────────────────────────────────────────
@@ -502,9 +500,9 @@ export class TalkCore {
502
500
  /** Remove the caller from its current group, deleting the group when it empties. */
503
501
  private async leaveCurrentGroup(): Promise<boolean> {
504
502
  const self = this.requireSelf();
505
- const group = await groupForSession(this.storage, self.sessionId);
503
+ const group = await groupForAgent(this.storage, self.agentId);
506
504
  if (!group) return false;
507
- const others = group.members.filter((m) => m !== self.sessionId);
505
+ const others = group.members.filter((m) => m !== self.agentId);
508
506
  if (others.length === 0) {
509
507
  await deleteGroup(this.storage, group.id);
510
508
  } else {
@@ -517,36 +515,42 @@ export class TalkCore {
517
515
  * Join or create a group and join it. With no name a fresh uuid is
518
516
  * generated; with a name, the group is joined when it exists and created
519
517
  * otherwise. Leaving any current group first keeps the single-group
520
- * invariant.
518
+ * invariant. When `agentName` is given, the agent's display name (what
519
+ * peers see in talk-list-agents) is set to it — also when the agent is
520
+ * already in the group, so re-joining can rename.
521
521
  */
522
- async groupJoin(groupName?: string): Promise<string> {
522
+ async groupJoin(groupName?: string, agentName?: string): Promise<string> {
523
523
  const self = this.requireSelf();
524
524
  const name =
525
525
  groupName === undefined || groupName.trim() === "" ? newGroupId() : groupName.trim();
526
526
  if (!isValidGroupName(name)) {
527
527
  return `Invalid group name '${name}'. Allowed: letters, digits, '-' and '_' (max 64 chars).`;
528
528
  }
529
+ if (agentName !== undefined) {
530
+ await this.writeSelf({ name: agentName });
531
+ }
532
+ const nameNote = agentName === undefined ? "" : ` You are visible as "${agentName}".`;
529
533
  const existing = await readGroup(this.storage, name);
530
- if (existing?.members.includes(self.sessionId)) {
531
- return `Already in group ${name} (${existing.members.length} member(s)).`;
534
+ if (existing?.members.includes(self.agentId)) {
535
+ return `Already in group ${name} (${existing.members.length} member(s)).${nameNote}`;
532
536
  }
533
537
  await this.leaveCurrentGroup();
534
538
  if (existing) {
535
539
  await writeGroup(this.storage, {
536
540
  ...existing,
537
- members: [...existing.members, self.sessionId],
541
+ members: [...existing.members, self.agentId],
538
542
  updatedAt: this.now(),
539
543
  });
540
- return `Joined group ${name} (${existing.members.length + 1} member(s)). You now see only co-members.`;
544
+ return `Joined group ${name} (${existing.members.length + 1} member(s)). You now see only co-members.${nameNote}`;
541
545
  }
542
546
  const now = this.now();
543
547
  await writeGroup(this.storage, {
544
548
  id: name,
545
- members: [self.sessionId],
549
+ members: [self.agentId],
546
550
  createdAt: now,
547
551
  updatedAt: now,
548
552
  });
549
- return `Created group ${name}. Other sessions join it with /talk-group-join ${name}.`;
553
+ return `Created group ${name}.${nameNote} Other agents join it with /talk-group-join ${name}.`;
550
554
  }
551
555
 
552
556
  /** Join the most recently created group; no-op when already in it. */
@@ -560,9 +564,9 @@ export class TalkCore {
560
564
  /** Leave the current group; an emptied group is deleted. */
561
565
  async groupLeave(): Promise<string> {
562
566
  const self = this.requireSelf();
563
- const group = await groupForSession(this.storage, self.sessionId);
567
+ const group = await groupForAgent(this.storage, self.agentId);
564
568
  if (!group) return "Not in any group.";
565
- const others = group.members.filter((m) => m !== self.sessionId);
569
+ const others = group.members.filter((m) => m !== self.agentId);
566
570
  if (others.length === 0) {
567
571
  await deleteGroup(this.storage, group.id);
568
572
  return `Left group ${group.id} (deleted — it was empty).`;
@@ -581,7 +585,7 @@ export class TalkCore {
581
585
  return `Deleted group ${name} (${group.members.length} member(s)).`;
582
586
  }
583
587
 
584
- /** Delete every group; all sessions become ungrouped. */
588
+ /** Delete every group; all agents become ungrouped. */
585
589
  async groupClear(): Promise<string> {
586
590
  const groups = await listGroups(this.storage);
587
591
  for (const group of groups) await deleteGroup(this.storage, group.id);
@@ -597,17 +601,15 @@ export class TalkCore {
597
601
  const groups = await listGroups(this.storage);
598
602
  if (groups.length === 0) return "No groups.";
599
603
  const records = await listRecords(this.storage);
600
- const label = (sessionId: string): string => {
601
- const rec = records.find((r) => r.sessionId === sessionId);
602
- const id = sessionId.length > 8 ? `${sessionId.slice(0, 8)}…` : sessionId;
603
- return rec ? `${rec.name} (${id})` : `unknown session (${id})`;
604
+ const label = (agentId: string): string => {
605
+ const rec = records.find((r) => r.agentId === agentId);
606
+ const id = agentId.length > 8 ? `${agentId.slice(0, 8)}…` : agentId;
607
+ return rec ? `${rec.name} (${id})` : `unknown agent (${id})`;
604
608
  };
605
609
  const lines = groups
606
610
  .toSorted((a, b) => b.createdAt - a.createdAt)
607
611
  .map((g) => {
608
- const members = g.members.map((m) =>
609
- m === self.sessionId ? `${label(m)} ← you` : label(m),
610
- );
612
+ const members = g.members.map((m) => (m === self.agentId ? `${label(m)} ← you` : label(m)));
611
613
  return `- ${g.id} (created ${age(g.createdAt)}): ${members.join(", ")}`;
612
614
  });
613
615
  return `Groups (${groups.length}):\n${lines.join("\n")}`;
@@ -617,7 +619,7 @@ export class TalkCore {
617
619
  if (!to) return 'send requires "to".';
618
620
  if (!body) return 'send requires "message".';
619
621
  if (to === "*" || to === "cwd") {
620
- return "send requires a single session id; broadcasting is disabled.";
622
+ return "send requires a single agent id; broadcasting is disabled.";
621
623
  }
622
624
  const resolved = await this.resolveTarget(to);
623
625
  if (!resolved.ok) return resolved.error;
@@ -683,10 +685,10 @@ export class TalkCore {
683
685
  }
684
686
 
685
687
  /** Build a minimal record from a letter's sender when the peer record is gone. */
686
- private recordFromLetter(letter: Letter): SessionRecord {
688
+ private recordFromLetter(letter: Letter): AgentRecord {
687
689
  return {
688
690
  addr: letter.from.addr,
689
- sessionId: letter.from.sessionId,
691
+ agentId: letter.from.agentId,
690
692
  name: letter.from.name,
691
693
  cwd: letter.from.cwd,
692
694
  pid: 0,
@@ -4,10 +4,9 @@
4
4
  */
5
5
 
6
6
  import type { Letter } from "./mailbox.js";
7
- import type { Presence, SessionRecord } from "./registry.js";
7
+ import type { AgentRecord, Presence } from "./registry.js";
8
8
 
9
- export const BOUNDARY_PREAMBLE =
10
- "This came from another pi session (a peer agent), not from the user.";
9
+ export const BOUNDARY_PREAMBLE = "This came from another pi agent, not from the user.";
11
10
 
12
11
  export function shortAddr(addr: string): string {
13
12
  return addr.slice(0, 6);
@@ -21,20 +20,20 @@ export function age(ts: number, now: number = Date.now()): string {
21
20
  return `${Math.round(m / 60)}h ago`;
22
21
  }
23
22
 
24
- /** Delivery text injected into the receiving session's LLM context. */
23
+ /** Delivery text injected into the receiving agent's LLM context. */
25
24
  export function formatDelivery(letter: Letter, now: number = Date.now()): string {
26
25
  const from = letter.from;
27
- const header = `From pi session ${from.sessionId} (${from.cwd}) — "${from.name}"`;
26
+ const header = `From pi agent ${from.agentId} (${from.cwd}) — "${from.name}"`;
28
27
  const meta = `_id ${letter.id} · ${letter.kind} · sent ${age(letter.ts, now)}_`;
29
28
  const hint =
30
29
  letter.kind === "ask" ? `\n\nReply with the talk-reply tool, replyTo: "${letter.id}"` : "";
31
30
  return `${BOUNDARY_PREAMBLE}\n\n${header}:\n\n${letter.body}\n\n${meta}${hint}`;
32
31
  }
33
32
 
34
- /** One session as the model sees it in a listing. `id` is the stable pi
35
- * session uuid; `name` is the display name when one was set; `self` marks
36
- * the calling session itself. */
37
- export interface SessionListItem {
33
+ /** One agent as the model sees it in a listing. `id` is the stable pi
34
+ * agent uuid; `name` is the display name when one was set; `self` marks
35
+ * the calling agent itself. */
36
+ export interface AgentListItem {
38
37
  status: string;
39
38
  work_dir: string;
40
39
  id: string;
@@ -42,17 +41,17 @@ export interface SessionListItem {
42
41
  self?: boolean;
43
42
  }
44
43
 
45
- /** Machine-readable JSON listing of sessions (what the model sees). */
44
+ /** Machine-readable JSON listing of agents (what the model sees). */
46
45
  export function formatListing(
47
- records: SessionRecord[],
46
+ records: AgentRecord[],
48
47
  selfAddr: string,
49
- presence: (r: SessionRecord) => Presence,
48
+ presence: (r: AgentRecord) => Presence,
50
49
  ): string {
51
50
  if (records.length === 0) return "[]";
52
- const items: SessionListItem[] = records.map((r) => {
51
+ const items: AgentListItem[] = records.map((r) => {
53
52
  const p = presence(r);
54
53
  const status = p === "live" ? r.status : "offline";
55
- const item: SessionListItem = { status, work_dir: r.cwd, id: r.sessionId, name: r.name };
54
+ const item: AgentListItem = { status, work_dir: r.cwd, id: r.agentId, name: r.name };
56
55
  if (r.addr === selfAddr) item.self = true;
57
56
  return item;
58
57
  });
@@ -60,5 +59,5 @@ export function formatListing(
60
59
  }
61
60
 
62
61
  export function refusalUnknown(to: string): string {
63
- return `Unknown session id '${to}'. Get session ids with talk-list-sessions.`;
62
+ return `Unknown agent id '${to}'. Get agent ids with talk-list-agents.`;
64
63
  }
package/src/talk/group.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Session groups for the talk mailbox: an explicit, uuid-addressed set of
3
- * sessions that see only each other. Core layer — depends only on
2
+ * Agent groups for the talk mailbox: an explicit, uuid-addressed set of
3
+ * agents that see only each other. Core layer — depends only on
4
4
  * TalkStorage, never on pi.
5
5
  *
6
6
  * Rules:
7
- * - A session belongs to at most one group (single-group invariant).
8
- * - Groups are public: any session can join any group by its uuid, and a
7
+ * - An agent belongs to at most one group (single-group invariant).
8
+ * - Groups are public: any agent can join any group by its uuid, and a
9
9
  * member can leave freely. There is no owner.
10
- * - Visibility is fully group-driven: a grouped session sees only its
11
- * co-members; a session in no group sees only itself.
10
+ * - Visibility is fully group-driven: a grouped agent sees only its
11
+ * co-members; an agent in no group sees only itself.
12
12
  * - A group that empties is deleted; a one-member group is a normal state
13
13
  * (a creator waiting for peers to join).
14
14
  */
@@ -22,7 +22,7 @@ import type { TalkStorage } from "./storage.js";
22
22
 
23
23
  export const GroupSchema = Type.Object({
24
24
  id: Type.String(),
25
- /** pi session uuids of the members. */
25
+ /** pi agent uuids of the members. */
26
26
  members: Type.Array(Type.String()),
27
27
  createdAt: Type.Number(),
28
28
  updatedAt: Type.Number(),
@@ -79,16 +79,13 @@ export async function deleteGroup(storage: TalkStorage, id: string): Promise<boo
79
79
  }
80
80
 
81
81
  /**
82
- * The group a session currently belongs to. Under the single-group invariant
83
- * this is at most one; corrupted data that lists the session in several
82
+ * The group an agent currently belongs to. Under the single-group invariant
83
+ * this is at most one; corrupted data that lists the agent in several
84
84
  * groups resolves to the first match.
85
85
  */
86
- export async function groupForSession(
87
- storage: TalkStorage,
88
- sessionId: string,
89
- ): Promise<Group | null> {
86
+ export async function groupForAgent(storage: TalkStorage, agentId: string): Promise<Group | null> {
90
87
  for (const group of await listGroups(storage)) {
91
- if (group.members.includes(sessionId)) return group;
88
+ if (group.members.includes(agentId)) return group;
92
89
  }
93
90
  return null;
94
91
  }