@trim21/personal-pi-extensions 0.0.349 → 0.0.351

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.349",
3
+ "version": "0.0.351",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -642,8 +642,10 @@ export async function create(input: CreateInput): Promise<LspClient> {
642
642
 
643
643
  const document = files[resolvedPath];
644
644
  if (document !== undefined) {
645
- // didChange:不清空既有诊断(如 clangd 只在内容变化时重发),
646
- // 让服务器下一次 push/pull 自然覆盖。
645
+ // didChange:内容已变,旧诊断立即失效。清空缓存避免等待窗口内服务器
646
+ // 重算未完成时(大项目可远超窗口)聚合到过期诊断;新 push 到达即填充。
647
+ pushDiagnostics.delete(resolvedPath);
648
+ pullDiagnostics.delete(resolvedPath);
647
649
  await connection.sendNotification("workspace/didChangeWatchedFiles", {
648
650
  changes: [{ uri, type: FILE_CHANGE_CHANGED }],
649
651
  });
package/src/talk/core.ts CHANGED
@@ -65,6 +65,12 @@ export interface TalkCoreEvents {
65
65
  deliver(letter: Letter): boolean | Promise<boolean>;
66
66
  /** Surface a notification (e.g. a presence transition) without waking a busy agent. */
67
67
  notify(content: string): void;
68
+ /**
69
+ * The agent's talk identity (agentId) was committed to a group by join or
70
+ * leave. The adapter persists it onto the session branch so a fork/resume
71
+ * of this session keeps the same identity.
72
+ */
73
+ identityChange?(agentId: string): void;
68
74
  }
69
75
 
70
76
  export interface TalkCoreOptions {
@@ -79,6 +85,35 @@ const DELIVERY_BACKOFF_MS = 5000;
79
85
  const INITIAL_DRAIN_DELAY_MS = 1200;
80
86
  const SWEEP_INTERVAL_MS = 30 * 60 * 1000;
81
87
 
88
+ /**
89
+ * custom entry type that pins an agent's talk identity to the session branch.
90
+ * join/leave commit the agentId here; fork/branch/resume copy the entry, and
91
+ * restoreTalkAgentId recovers the identity on the next session_start.
92
+ */
93
+ export const TALK_JOIN_ENTRY_TYPE = "talk:join";
94
+
95
+ /**
96
+ * Recover the talk identity (agentId) pinned to a session branch by its most
97
+ * recent join/leave record, or undefined when the branch never joined a group
98
+ * (a fresh session or one rewound before its join). Mirrors how the file tools
99
+ * rebuild their reads state from the current branch's history.
100
+ */
101
+ export function restoreTalkAgentId(branchEntries: readonly unknown[]): string | undefined {
102
+ let agentId: string | undefined;
103
+ for (const entry of branchEntries) {
104
+ if (typeof entry !== "object" || entry === null) continue;
105
+ const { type, customType, data } = entry as {
106
+ type?: unknown;
107
+ customType?: unknown;
108
+ data?: unknown;
109
+ };
110
+ if (type !== "custom" || customType !== TALK_JOIN_ENTRY_TYPE) continue;
111
+ const recorded = (data as { agentId?: unknown } | undefined)?.agentId;
112
+ if (typeof recorded === "string" && recorded.length > 0) agentId = recorded;
113
+ }
114
+ return agentId;
115
+ }
116
+
82
117
  /**
83
118
  * Mutual-ask arbitration: true when the peer asked first. The `ts` fields of
84
119
  * the two ask letters are fixed values inside the letters, so both sides
@@ -528,31 +563,36 @@ export class TalkCore {
528
563
  }
529
564
  const nameNote = agentName === undefined ? "" : ` You are visible as "${agentName}".`;
530
565
  const existing = await readGroup(this.storage, name);
566
+ let text: string;
531
567
  if (existing?.members.includes(self.agentId)) {
532
- return `Already in group ${name} (${existing.members.length} member(s)). Members: ${await this.groupMemberNames(existing.members)}.${nameNote}`;
533
- }
534
- await this.leaveCurrentGroup();
535
- if (existing) {
536
- await writeGroup(this.storage, {
537
- ...existing,
538
- members: [...existing.members, self.agentId],
539
- updatedAt: this.now(),
540
- });
541
- return `Joined group ${name} (${
542
- existing.members.length + 1
543
- } member(s)). Members: ${await this.groupMemberNames([
544
- ...existing.members,
545
- self.agentId,
546
- ])}. You now see only co-members.${nameNote}`;
568
+ text = `Already in group ${name} (${existing.members.length} member(s)). Members: ${await this.groupMemberNames(existing.members)}.${nameNote}`;
569
+ } else {
570
+ await this.leaveCurrentGroup();
571
+ if (existing) {
572
+ await writeGroup(this.storage, {
573
+ ...existing,
574
+ members: [...existing.members, self.agentId],
575
+ updatedAt: this.now(),
576
+ });
577
+ text = `Joined group ${name} (${
578
+ existing.members.length + 1
579
+ } member(s)). Members: ${await this.groupMemberNames([
580
+ ...existing.members,
581
+ self.agentId,
582
+ ])}. You now see only co-members.${nameNote}`;
583
+ } else {
584
+ const now = this.now();
585
+ await writeGroup(this.storage, {
586
+ id: name,
587
+ members: [self.agentId],
588
+ createdAt: now,
589
+ updatedAt: now,
590
+ });
591
+ text = `Created group ${name}. Members: ${await this.groupMemberNames([self.agentId])}.${nameNote} Other agents join it with /talk-group-join ${name}.`;
592
+ }
547
593
  }
548
- const now = this.now();
549
- await writeGroup(this.storage, {
550
- id: name,
551
- members: [self.agentId],
552
- createdAt: now,
553
- updatedAt: now,
554
- });
555
- return `Created group ${name}. Members: ${await this.groupMemberNames([self.agentId])}.${nameNote} Other agents join it with /talk-group-join ${name}.`;
594
+ this.events.identityChange?.(self.agentId);
595
+ return text;
556
596
  }
557
597
 
558
598
  /**
@@ -574,9 +614,11 @@ export class TalkCore {
574
614
  const others = group.members.filter((m) => m !== self.agentId);
575
615
  if (others.length === 0) {
576
616
  await deleteGroup(this.storage, group.id);
617
+ this.events.identityChange?.(self.agentId);
577
618
  return `Left group ${group.id} (deleted — it was empty).`;
578
619
  }
579
620
  await writeGroup(this.storage, { ...group, members: others, updatedAt: this.now() });
621
+ this.events.identityChange?.(self.agentId);
580
622
  return `Left group ${group.id} (${others.length} member(s) remain).`;
581
623
  }
582
624
 
package/src/talk/index.ts CHANGED
@@ -24,7 +24,7 @@ import { type TObject, Type } from "typebox";
24
24
 
25
25
  import { type CommandResult, type CommandSpec, parseCommand } from "../lib/cli.js";
26
26
  import { resolveHomePath } from "../lib/path.js";
27
- import { TalkCore } from "./core.js";
27
+ import { restoreTalkAgentId, TALK_JOIN_ENTRY_TYPE, TalkCore } from "./core.js";
28
28
  import { formatDelivery } from "./format.js";
29
29
  import type { Letter } from "./mailbox.js";
30
30
  import { type AgentRecord, deriveAddr } from "./registry.js";
@@ -150,6 +150,11 @@ export default function talk(pi: ExtensionAPI) {
150
150
  // in LLM context.
151
151
  pi.appendEntry(NOTIFY_TYPE, content);
152
152
  },
153
+ identityChange(agentId) {
154
+ // Pin the identity to the session branch so a fork/resume of this
155
+ // session keeps the same talk address and group membership.
156
+ pi.appendEntry(TALK_JOIN_ENTRY_TYPE, { agentId, ts: Date.now() });
157
+ },
153
158
  },
154
159
  });
155
160
 
@@ -174,9 +179,12 @@ export default function talk(pi: ExtensionAPI) {
174
179
  // ── Lifecycle ──────────────────────────────────────────────────────────
175
180
 
176
181
  pi.on("session_start", (_event, ctx: ExtensionContext) => {
177
- const agentId = ctx.sessionManager.getSessionId();
178
182
  const cwd = ctx.sessionManager.getCwd() ?? ctx.cwd;
179
183
  const now = Date.now();
184
+ // A fork/branch/resume of a session that joined a group keeps that talk
185
+ // identity (agentId); a fresh session gets the new session id.
186
+ const agentId =
187
+ restoreTalkAgentId(ctx.sessionManager.getBranch()) ?? ctx.sessionManager.getSessionId();
180
188
  self = {
181
189
  addr: deriveAddr(cwd, agentId),
182
190
  agentId,