@trim21/personal-pi-extensions 0.1.612 → 0.1.619
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 +1 -1
- package/src/aft/config.ts +9 -3
- package/src/aft/index.ts +6 -2
- package/src/aft/logger.ts +9 -3
- package/src/aft/tools.ts +3 -1
- package/src/bwrap/README.md +19 -10
- package/src/bwrap/approval-rules.ts +42 -14
- package/src/bwrap/approval-suggest.ts +12 -4
- package/src/bwrap/core.ts +39 -13
- package/src/bwrap/dcg-scan.ts +16 -6
- package/src/bwrap/holder.ts +3 -1
- package/src/bwrap/mihomo-config.ts +18 -6
- package/src/bwrap/network-stack.ts +69 -24
- package/src/bwrap/runtime.ts +30 -10
- package/src/bwrap/sandbox.ts +1 -1
- package/src/claude-code/common.ts +6 -2
- package/src/claude-code/edit-utils.ts +3 -1
- package/src/claude-code/files.ts +40 -14
- package/src/claude-code/glob.ts +12 -4
- package/src/claude-code/grep.ts +41 -14
- package/src/claude-code/session-tools.ts +18 -6
- package/src/claude-code/shell.ts +9 -3
- package/src/gh/base.ts +72 -24
- package/src/gh/tools/download-release-assets.ts +12 -4
- package/src/gh/tools/list-releases.ts +3 -1
- package/src/gh/tools/list-workflow-runs.ts +9 -3
- package/src/gh/tools/read-ci-logs.ts +21 -7
- package/src/gh/tools/read-repo.ts +3 -1
- package/src/lib/cli-args.ts +6 -2
- package/src/lib/cli.ts +48 -16
- package/src/lib/file-reads.ts +24 -8
- package/src/lib/github.ts +35 -12
- package/src/lib/lsp/adapter.ts +6 -2
- package/src/lib/lsp/bin.ts +12 -4
- package/src/lib/lsp/client.ts +205 -69
- package/src/lib/lsp/diagnostic.ts +6 -2
- package/src/lib/lsp/inspect-tool.ts +25 -9
- package/src/lib/lsp/lsp.ts +138 -46
- package/src/lib/lsp/rename-tool.ts +11 -4
- package/src/lib/lsp/rename.ts +25 -8
- package/src/lib/lsp/server-config.ts +41 -14
- package/src/lib/lsp/watcher.ts +51 -17
- package/src/lib/path.ts +18 -6
- package/src/lib/proc.ts +3 -1
- package/src/lib/proxy.ts +9 -3
- package/src/lib/request-policy.ts +3 -1
- package/src/lib/seq-state.ts +3 -1
- package/src/lib/ui.ts +23 -8
- package/src/lib/write-guard.ts +21 -7
- package/src/openai-cost/config.ts +3 -1
- package/src/openai-cost/cost.ts +54 -18
- package/src/openai-cost/index.ts +3 -1
- package/src/openai-cost/provider.ts +6 -2
- package/src/opencode/bash.ts +9 -3
- package/src/opencode/edit-engine.ts +24 -8
- package/src/opencode/files.ts +30 -10
- package/src/opencode/glob.ts +3 -1
- package/src/opencode/grep.ts +12 -4
- package/src/opencode/question.ts +3 -1
- package/src/opencode/ripgrep.ts +18 -6
- package/src/opencode/todo.ts +3 -1
- package/src/session-name.ts +39 -13
- package/src/spawn-agent-agents.ts +18 -6
- package/src/spawn-agent.ts +53 -18
- package/src/system-prompt/index.ts +9 -3
- package/src/talk/core.ts +143 -48
- package/src/talk/format.ts +12 -4
- package/src/talk/group.ts +9 -3
- package/src/talk/index.ts +30 -10
- package/src/talk/mailbox.ts +30 -10
- package/src/talk/policy.ts +6 -2
- package/src/talk/registry.ts +33 -11
- package/src/talk/storage.ts +3 -1
- package/src/vision-agent.ts +41 -14
- package/src/web/config.ts +3 -1
- package/src/web/fetch.ts +75 -25
- package/src/web/search.ts +30 -10
package/src/talk/core.ts
CHANGED
|
@@ -102,12 +102,20 @@ export const TALK_JOIN_ENTRY_TYPE = "talk:join";
|
|
|
102
102
|
export function restoreTalkAgentId(branchEntries: readonly unknown[]): string | undefined {
|
|
103
103
|
let agentId: string | undefined;
|
|
104
104
|
for (const entry of branchEntries) {
|
|
105
|
-
if (!isRecord(entry))
|
|
105
|
+
if (!isRecord(entry)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
106
108
|
const { type, customType, data } = entry;
|
|
107
|
-
if (type !== "custom" || customType !== TALK_JOIN_ENTRY_TYPE)
|
|
108
|
-
|
|
109
|
+
if (type !== "custom" || customType !== TALK_JOIN_ENTRY_TYPE) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!isRecord(data)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
109
115
|
const recorded = data.agentId;
|
|
110
|
-
if (typeof recorded === "string" && recorded.length > 0)
|
|
116
|
+
if (typeof recorded === "string" && recorded.length > 0) {
|
|
117
|
+
agentId = recorded;
|
|
118
|
+
}
|
|
111
119
|
}
|
|
112
120
|
return agentId;
|
|
113
121
|
}
|
|
@@ -161,7 +169,9 @@ export class TalkCore {
|
|
|
161
169
|
|
|
162
170
|
private requireSelf(): AgentRecord {
|
|
163
171
|
const self = this.self;
|
|
164
|
-
if (!self)
|
|
172
|
+
if (!self) {
|
|
173
|
+
throw new Error("Talk core is not started");
|
|
174
|
+
}
|
|
165
175
|
return self;
|
|
166
176
|
}
|
|
167
177
|
|
|
@@ -201,10 +211,18 @@ export class TalkCore {
|
|
|
201
211
|
}
|
|
202
212
|
|
|
203
213
|
async stop(): Promise<void> {
|
|
204
|
-
if (this.initialDrain)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (this.
|
|
214
|
+
if (this.initialDrain) {
|
|
215
|
+
clearTimeout(this.initialDrain);
|
|
216
|
+
}
|
|
217
|
+
if (this.watchPoller) {
|
|
218
|
+
clearInterval(this.watchPoller);
|
|
219
|
+
}
|
|
220
|
+
if (this.inboxPoll) {
|
|
221
|
+
clearInterval(this.inboxPoll);
|
|
222
|
+
}
|
|
223
|
+
if (this.sweeper) {
|
|
224
|
+
clearInterval(this.sweeper);
|
|
225
|
+
}
|
|
208
226
|
if (this.self) {
|
|
209
227
|
try {
|
|
210
228
|
await this.writeSelf({ status: "idle", offline: true });
|
|
@@ -232,7 +250,9 @@ export class TalkCore {
|
|
|
232
250
|
}
|
|
233
251
|
|
|
234
252
|
private async writeSelf(patch: Partial<AgentRecord>): Promise<void> {
|
|
235
|
-
if (!this.self)
|
|
253
|
+
if (!this.self) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
236
256
|
// A dead agent pins lastSeenAt to 0 so no later event re-freshens it.
|
|
237
257
|
this.self = { ...this.self, ...patch, lastSeenAt: this.dead ? 0 : this.now() };
|
|
238
258
|
try {
|
|
@@ -247,7 +267,9 @@ export class TalkCore {
|
|
|
247
267
|
* 该动作下次成功后再失败会重新上报。通知通道本身出错也不能逃逸。
|
|
248
268
|
*/
|
|
249
269
|
private notifyBackgroundFailure(action: string, error: unknown): void {
|
|
250
|
-
if (this.backgroundFailuresReported.has(action))
|
|
270
|
+
if (this.backgroundFailuresReported.has(action)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
251
273
|
this.backgroundFailuresReported.add(action);
|
|
252
274
|
const detail = error instanceof Error ? error.message : String(error);
|
|
253
275
|
try {
|
|
@@ -271,7 +293,9 @@ export class TalkCore {
|
|
|
271
293
|
}
|
|
272
294
|
|
|
273
295
|
private startInboxPoll(): void {
|
|
274
|
-
if (this.inboxPoll)
|
|
296
|
+
if (this.inboxPoll) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
275
299
|
this.inboxPoll = setInterval(() => {
|
|
276
300
|
this.runInBackground("inbox poll", () => this.checkInbox());
|
|
277
301
|
}, INBOX_POLL_MS);
|
|
@@ -283,11 +307,17 @@ export class TalkCore {
|
|
|
283
307
|
/** Drain the inbox and hand each letter to the adapter. Public so tests can drive it. */
|
|
284
308
|
async checkInbox(): Promise<void> {
|
|
285
309
|
const self = this.self;
|
|
286
|
-
if (!self)
|
|
287
|
-
|
|
310
|
+
if (!self) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (this.now() - this.lastDeliveryFailureAt < DELIVERY_BACKOFF_MS) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
288
316
|
// Refuse mode: never drain — letters stay queued (receipts honestly read
|
|
289
317
|
// 'queued') instead of being silently consumed and dropped.
|
|
290
|
-
if (!inboundAccepts())
|
|
318
|
+
if (!inboundAccepts()) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
291
321
|
const items = await listInbox(this.storage, self.addr);
|
|
292
322
|
for (const item of items) {
|
|
293
323
|
if (this.deliveredIds.has(item.letter.id)) {
|
|
@@ -388,7 +418,9 @@ export class TalkCore {
|
|
|
388
418
|
const memberIds = await this.myGroupMemberIds();
|
|
389
419
|
const others = records.filter((r) => r.addr !== self.addr && memberIds?.has(r.agentId));
|
|
390
420
|
const target = others.find((r) => r.agentId === to);
|
|
391
|
-
if (!target)
|
|
421
|
+
if (!target) {
|
|
422
|
+
return { ok: false, error: refusalUnknown(to) };
|
|
423
|
+
}
|
|
392
424
|
return { ok: true, record: target };
|
|
393
425
|
}
|
|
394
426
|
|
|
@@ -404,7 +436,9 @@ export class TalkCore {
|
|
|
404
436
|
? 0
|
|
405
437
|
: await unreadCount(this.storage, target.addr);
|
|
406
438
|
const verdict = this.policy.check(body, backlog, target.addr);
|
|
407
|
-
if (!verdict.ok)
|
|
439
|
+
if (!verdict.ok) {
|
|
440
|
+
return { ok: false, error: verdict.reason };
|
|
441
|
+
}
|
|
408
442
|
const letter: Letter = {
|
|
409
443
|
id: newMessageId(),
|
|
410
444
|
from: { addr: self.addr, name: self.name, cwd: self.cwd, agentId: self.agentId },
|
|
@@ -448,8 +482,11 @@ export class TalkCore {
|
|
|
448
482
|
);
|
|
449
483
|
const onAbort = () => settle({ reason: "aborted" });
|
|
450
484
|
this.askWaiters.set(askId, settle);
|
|
451
|
-
if (signal?.aborted)
|
|
452
|
-
|
|
485
|
+
if (signal?.aborted) {
|
|
486
|
+
onAbort();
|
|
487
|
+
} else {
|
|
488
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
489
|
+
}
|
|
453
490
|
});
|
|
454
491
|
}
|
|
455
492
|
|
|
@@ -458,7 +495,9 @@ export class TalkCore {
|
|
|
458
495
|
const self = this.requireSelf();
|
|
459
496
|
for (const askId of await outgoingAskIds(this.storage, self.addr)) {
|
|
460
497
|
const out = await readOutgoingAsk(this.storage, self.addr, askId);
|
|
461
|
-
if (out && out.toAddr === toAddr)
|
|
498
|
+
if (out && out.toAddr === toAddr) {
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
462
501
|
}
|
|
463
502
|
return undefined;
|
|
464
503
|
}
|
|
@@ -480,14 +519,20 @@ export class TalkCore {
|
|
|
480
519
|
private async resolveInterlock(letter: Letter): Promise<void> {
|
|
481
520
|
const self = this.requireSelf();
|
|
482
521
|
const myAsk = await this.findOutAskTo(letter.from.addr);
|
|
483
|
-
if (!myAsk)
|
|
522
|
+
if (!myAsk) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
484
525
|
const waiter = this.askWaiters.get(myAsk.askId);
|
|
485
|
-
if (!waiter)
|
|
526
|
+
if (!waiter) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
486
529
|
const peerFirst = peerAskedFirst(
|
|
487
530
|
{ ts: letter.ts, cwd: letter.from.cwd, agentId: letter.from.agentId },
|
|
488
531
|
{ ts: myAsk.ts, cwd: self.cwd, agentId: self.agentId },
|
|
489
532
|
);
|
|
490
|
-
if (!peerFirst)
|
|
533
|
+
if (!peerFirst) {
|
|
534
|
+
return; // we asked first; keep waiting — the peer will yield
|
|
535
|
+
}
|
|
491
536
|
this.askWaiters.delete(myAsk.askId);
|
|
492
537
|
waiter({
|
|
493
538
|
reason: `peer asked first (their ask id ${letter.id.slice(-8)}) — respond with a message before re-asking`,
|
|
@@ -508,7 +553,9 @@ export class TalkCore {
|
|
|
508
553
|
const all = await listRecords(this.storage);
|
|
509
554
|
const memberIds = await this.myGroupMemberIds();
|
|
510
555
|
const records = all.filter((r) => {
|
|
511
|
-
if (r.addr === self.addr)
|
|
556
|
+
if (r.addr === self.addr) {
|
|
557
|
+
return !this.dead;
|
|
558
|
+
}
|
|
512
559
|
return memberIds?.has(r.agentId) ?? false;
|
|
513
560
|
});
|
|
514
561
|
return formatListing(records, self.addr, presenceOf);
|
|
@@ -520,8 +567,12 @@ export class TalkCore {
|
|
|
520
567
|
const records = await listRecords(this.storage);
|
|
521
568
|
const memberIds = await this.myGroupMemberIds();
|
|
522
569
|
const filtered = records.filter((r) => {
|
|
523
|
-
if (r.cwd !== cwd)
|
|
524
|
-
|
|
570
|
+
if (r.cwd !== cwd) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
if (r.addr === self.addr) {
|
|
574
|
+
return !this.dead;
|
|
575
|
+
}
|
|
525
576
|
return memberIds?.has(r.agentId) ?? false;
|
|
526
577
|
});
|
|
527
578
|
return formatListing(filtered, self.addr, presenceOf);
|
|
@@ -548,7 +599,9 @@ export class TalkCore {
|
|
|
548
599
|
return "Marked this agent as dead.";
|
|
549
600
|
}
|
|
550
601
|
const resolved = await this.resolveTarget(target);
|
|
551
|
-
if (!resolved.ok)
|
|
602
|
+
if (!resolved.ok) {
|
|
603
|
+
return resolved.error;
|
|
604
|
+
}
|
|
552
605
|
await writeRecord(this.storage, { ...resolved.record, lastSeenAt: 0, offline: true });
|
|
553
606
|
return `Marked "${resolved.record.name}" as dead.`;
|
|
554
607
|
}
|
|
@@ -568,7 +621,9 @@ export class TalkCore {
|
|
|
568
621
|
private async leaveCurrentGroup(): Promise<boolean> {
|
|
569
622
|
const self = this.requireSelf();
|
|
570
623
|
const group = await groupForAgent(this.storage, self.agentId);
|
|
571
|
-
if (!group)
|
|
624
|
+
if (!group) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
572
627
|
const others = group.members.filter((m) => m !== self.agentId);
|
|
573
628
|
if (others.length === 0) {
|
|
574
629
|
await deleteGroup(this.storage, group.id);
|
|
@@ -636,7 +691,9 @@ export class TalkCore {
|
|
|
636
691
|
*/
|
|
637
692
|
async groupJoinLast(agentName?: string): Promise<string> {
|
|
638
693
|
const groups = await listGroups(this.storage);
|
|
639
|
-
if (groups.length === 0)
|
|
694
|
+
if (groups.length === 0) {
|
|
695
|
+
return "No groups. Create one with /talk-group-join.";
|
|
696
|
+
}
|
|
640
697
|
const latest = groups.reduce((a, b) => (b.createdAt > a.createdAt ? b : a));
|
|
641
698
|
return this.groupJoin(latest.id, agentName);
|
|
642
699
|
}
|
|
@@ -645,7 +702,9 @@ export class TalkCore {
|
|
|
645
702
|
async groupLeave(): Promise<string> {
|
|
646
703
|
const self = this.requireSelf();
|
|
647
704
|
const group = await groupForAgent(this.storage, self.agentId);
|
|
648
|
-
if (!group)
|
|
705
|
+
if (!group) {
|
|
706
|
+
return "Not in any group.";
|
|
707
|
+
}
|
|
649
708
|
const others = group.members.filter((m) => m !== self.agentId);
|
|
650
709
|
if (others.length === 0) {
|
|
651
710
|
await deleteGroup(this.storage, group.id);
|
|
@@ -660,9 +719,13 @@ export class TalkCore {
|
|
|
660
719
|
/** Delete a group by name; its members become ungrouped. */
|
|
661
720
|
async groupDelete(groupName: string): Promise<string> {
|
|
662
721
|
const name = groupName.trim();
|
|
663
|
-
if (!isValidGroupName(name))
|
|
722
|
+
if (!isValidGroupName(name)) {
|
|
723
|
+
return `Invalid group name '${name}'.`;
|
|
724
|
+
}
|
|
664
725
|
const group = await readGroup(this.storage, name);
|
|
665
|
-
if (!group)
|
|
726
|
+
if (!group) {
|
|
727
|
+
return `Unknown group '${name}'. Run /talk-group-list to see groups.`;
|
|
728
|
+
}
|
|
666
729
|
await deleteGroup(this.storage, name);
|
|
667
730
|
return `Deleted group ${name} (${group.members.length} member(s)).`;
|
|
668
731
|
}
|
|
@@ -670,7 +733,9 @@ export class TalkCore {
|
|
|
670
733
|
/** Delete every group; all agents become ungrouped. */
|
|
671
734
|
async groupClear(): Promise<string> {
|
|
672
735
|
const groups = await listGroups(this.storage);
|
|
673
|
-
for (const group of groups)
|
|
736
|
+
for (const group of groups) {
|
|
737
|
+
await deleteGroup(this.storage, group.id);
|
|
738
|
+
}
|
|
674
739
|
return groups.length === 0 ? "No groups." : `Deleted ${groups.length} group(s).`;
|
|
675
740
|
}
|
|
676
741
|
|
|
@@ -681,7 +746,9 @@ export class TalkCore {
|
|
|
681
746
|
async groupList(): Promise<string> {
|
|
682
747
|
const self = this.requireSelf();
|
|
683
748
|
const groups = await listGroups(this.storage);
|
|
684
|
-
if (groups.length === 0)
|
|
749
|
+
if (groups.length === 0) {
|
|
750
|
+
return "No groups.";
|
|
751
|
+
}
|
|
685
752
|
const records = await listRecords(this.storage);
|
|
686
753
|
const label = (agentId: string): string => {
|
|
687
754
|
const rec = records.find((r) => r.agentId === agentId);
|
|
@@ -719,32 +786,48 @@ export class TalkCore {
|
|
|
719
786
|
async groupStatus(): Promise<string | undefined> {
|
|
720
787
|
const self = this.requireSelf();
|
|
721
788
|
const group = await groupForAgent(this.storage, self.agentId);
|
|
722
|
-
if (!group)
|
|
789
|
+
if (!group) {
|
|
790
|
+
return undefined;
|
|
791
|
+
}
|
|
723
792
|
return self.alias ? `${self.alias}@${group.id}` : `@${group.id}`;
|
|
724
793
|
}
|
|
725
794
|
|
|
726
795
|
async send(to: string, body: string): Promise<string> {
|
|
727
|
-
if (!to)
|
|
728
|
-
|
|
796
|
+
if (!to) {
|
|
797
|
+
return 'send requires "to".';
|
|
798
|
+
}
|
|
799
|
+
if (!body) {
|
|
800
|
+
return 'send requires "message".';
|
|
801
|
+
}
|
|
729
802
|
if (to === "*" || to === "cwd") {
|
|
730
803
|
return "send requires a single agent id; broadcasting is disabled.";
|
|
731
804
|
}
|
|
732
805
|
const resolved = await this.resolveTarget(to);
|
|
733
|
-
if (!resolved.ok)
|
|
806
|
+
if (!resolved.ok) {
|
|
807
|
+
return resolved.error;
|
|
808
|
+
}
|
|
734
809
|
const sent = await this.sendLetter(resolved.record, "message", body);
|
|
735
|
-
if (!sent.ok)
|
|
810
|
+
if (!sent.ok) {
|
|
811
|
+
return sent.error;
|
|
812
|
+
}
|
|
736
813
|
return `Sent to "${resolved.record.name}" (${shortAddr(resolved.record.addr)}) [id ${sent.letter.id.slice(-8)}]: ${sent.verdict}.`;
|
|
737
814
|
}
|
|
738
815
|
|
|
739
816
|
async ask(to: string, body: string, timeoutMs: number, signal?: AbortSignal): Promise<string> {
|
|
740
|
-
if (!to)
|
|
741
|
-
|
|
817
|
+
if (!to) {
|
|
818
|
+
return 'ask requires "to".';
|
|
819
|
+
}
|
|
820
|
+
if (!body) {
|
|
821
|
+
return 'ask requires "message".';
|
|
822
|
+
}
|
|
742
823
|
if (to === "*" || to === "cwd") {
|
|
743
824
|
return "ask is 1:1 and cannot broadcast.";
|
|
744
825
|
}
|
|
745
826
|
const self = this.requireSelf();
|
|
746
827
|
const resolved = await this.resolveTarget(to);
|
|
747
|
-
if (!resolved.ok)
|
|
828
|
+
if (!resolved.ok) {
|
|
829
|
+
return resolved.error;
|
|
830
|
+
}
|
|
748
831
|
const record = resolved.record;
|
|
749
832
|
// Fast-path deadlock avoidance: if the target already sent us something,
|
|
750
833
|
// answer them first instead of blocking on a fresh ask. This closes the
|
|
@@ -755,7 +838,9 @@ export class TalkCore {
|
|
|
755
838
|
return `You have ${fromTarget.length} unread message(s) from "${record.name}" — respond to them before asking.`;
|
|
756
839
|
}
|
|
757
840
|
const sent = await this.sendLetter(record, "ask", body);
|
|
758
|
-
if (!sent.ok)
|
|
841
|
+
if (!sent.ok) {
|
|
842
|
+
return sent.error;
|
|
843
|
+
}
|
|
759
844
|
await trackOutgoingAsk(this.storage, self.addr, {
|
|
760
845
|
askId: sent.letter.id,
|
|
761
846
|
toAddr: record.addr,
|
|
@@ -776,16 +861,22 @@ export class TalkCore {
|
|
|
776
861
|
// ── Presence watch ─────────────────────────────────────────────────────
|
|
777
862
|
|
|
778
863
|
async watch(to: string): Promise<string> {
|
|
779
|
-
if (!to)
|
|
864
|
+
if (!to) {
|
|
865
|
+
return "watch requires 'to' (a peer name or address prefix).";
|
|
866
|
+
}
|
|
780
867
|
const resolved = await this.resolveTarget(to);
|
|
781
|
-
if (!resolved.ok)
|
|
868
|
+
if (!resolved.ok) {
|
|
869
|
+
return resolved.error;
|
|
870
|
+
}
|
|
782
871
|
this.watched.set(resolved.record.addr, presenceOf(resolved.record));
|
|
783
872
|
this.startWatchPoller();
|
|
784
873
|
return `Watching "${resolved.record.name}" (${shortAddr(resolved.record.addr)}) for presence transitions. Notifications arrive as talk messages.`;
|
|
785
874
|
}
|
|
786
875
|
|
|
787
876
|
private startWatchPoller(): void {
|
|
788
|
-
if (this.watchPoller)
|
|
877
|
+
if (this.watchPoller) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
789
880
|
this.watchPoller = setInterval(() => {
|
|
790
881
|
this.runInBackground("presence watch", () => this.pollWatched());
|
|
791
882
|
}, WATCH_POLL_MS);
|
|
@@ -793,11 +884,15 @@ export class TalkCore {
|
|
|
793
884
|
}
|
|
794
885
|
|
|
795
886
|
private async pollWatched(): Promise<void> {
|
|
796
|
-
if (!this.self || this.watched.size === 0)
|
|
887
|
+
if (!this.self || this.watched.size === 0) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
797
890
|
for (const [addr, prev] of this.watched) {
|
|
798
891
|
const rec = await readRecord(this.storage, addr);
|
|
799
892
|
const now: Presence = rec ? presenceOf(rec) : "offline";
|
|
800
|
-
if (now === prev)
|
|
893
|
+
if (now === prev) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
801
896
|
this.watched.set(addr, now);
|
|
802
897
|
const label = rec ? `"${rec.name}"` : shortAddr(addr);
|
|
803
898
|
const state = now === "live" ? (rec?.status ?? "idle") : "offline";
|
package/src/talk/format.ts
CHANGED
|
@@ -14,9 +14,13 @@ export function shortAddr(addr: string): string {
|
|
|
14
14
|
|
|
15
15
|
export function age(ts: number, now: number = Date.now()): string {
|
|
16
16
|
const s = Math.max(0, Math.round((now - ts) / 1000));
|
|
17
|
-
if (s < 60)
|
|
17
|
+
if (s < 60) {
|
|
18
|
+
return `${s}s ago`;
|
|
19
|
+
}
|
|
18
20
|
const m = Math.round(s / 60);
|
|
19
|
-
if (m < 60)
|
|
21
|
+
if (m < 60) {
|
|
22
|
+
return `${m}m ago`;
|
|
23
|
+
}
|
|
20
24
|
return `${Math.round(m / 60)}h ago`;
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -45,12 +49,16 @@ export function formatListing(
|
|
|
45
49
|
selfAddr: string,
|
|
46
50
|
presence: (r: AgentRecord) => Presence,
|
|
47
51
|
): string {
|
|
48
|
-
if (records.length === 0)
|
|
52
|
+
if (records.length === 0) {
|
|
53
|
+
return "[]";
|
|
54
|
+
}
|
|
49
55
|
const items: AgentListItem[] = records.map((r) => {
|
|
50
56
|
const p = presence(r);
|
|
51
57
|
const status = p === "live" ? r.status : "offline";
|
|
52
58
|
const item: AgentListItem = { status, work_dir: r.cwd, id: r.agentId, name: r.name };
|
|
53
|
-
if (r.addr === selfAddr)
|
|
59
|
+
if (r.addr === selfAddr) {
|
|
60
|
+
item.self = true;
|
|
61
|
+
}
|
|
54
62
|
return item;
|
|
55
63
|
});
|
|
56
64
|
return JSON.stringify(items, null, 2);
|
package/src/talk/group.ts
CHANGED
|
@@ -39,7 +39,9 @@ export const GROUPS_NS = "groups";
|
|
|
39
39
|
const GROUP_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
40
40
|
|
|
41
41
|
export function assertGroupId(id: string): void {
|
|
42
|
-
if (!GROUP_ID_PATTERN.test(id))
|
|
42
|
+
if (!GROUP_ID_PATTERN.test(id)) {
|
|
43
|
+
throw new TypeError(`Invalid group name: ${id}`);
|
|
44
|
+
}
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export function isValidGroupName(id: string): boolean {
|
|
@@ -60,7 +62,9 @@ export async function listGroups(storage: TalkStorage): Promise<Group[]> {
|
|
|
60
62
|
const out: Group[] = [];
|
|
61
63
|
for (const key of await storage.listKeys(GROUPS_NS)) {
|
|
62
64
|
const raw = await storage.readJson(GROUPS_NS, key);
|
|
63
|
-
if (Value.Check(GroupSchema, raw))
|
|
65
|
+
if (Value.Check(GroupSchema, raw)) {
|
|
66
|
+
out.push(raw);
|
|
67
|
+
}
|
|
64
68
|
}
|
|
65
69
|
return out.toSorted((a, b) => a.createdAt - b.createdAt);
|
|
66
70
|
}
|
|
@@ -85,7 +89,9 @@ export async function deleteGroup(storage: TalkStorage, id: string): Promise<boo
|
|
|
85
89
|
*/
|
|
86
90
|
export async function groupForAgent(storage: TalkStorage, agentId: string): Promise<Group | null> {
|
|
87
91
|
for (const group of await listGroups(storage)) {
|
|
88
|
-
if (group.members.includes(agentId))
|
|
92
|
+
if (group.members.includes(agentId)) {
|
|
93
|
+
return group;
|
|
94
|
+
}
|
|
89
95
|
}
|
|
90
96
|
return null;
|
|
91
97
|
}
|
package/src/talk/index.ts
CHANGED
|
@@ -86,7 +86,9 @@ function readTalkSettings(): { dbPath?: string; deliver?: "steer" | "queue" } {
|
|
|
86
86
|
const raw = fs.readFileSync(path.join(getAgentDir(), "settings.json"), "utf8");
|
|
87
87
|
const parsed = JSON.parse(raw) as { talk?: { db_path?: unknown; deliver?: unknown } };
|
|
88
88
|
const talk = parsed.talk;
|
|
89
|
-
if (!talk)
|
|
89
|
+
if (!talk) {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
90
92
|
const dbPath = typeof talk.db_path === "string" ? talk.db_path : undefined;
|
|
91
93
|
const deliver = talk.deliver === "steer" || talk.deliver === "queue" ? talk.deliver : undefined;
|
|
92
94
|
return { dbPath, deliver };
|
|
@@ -151,14 +153,18 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
151
153
|
});
|
|
152
154
|
|
|
153
155
|
function requireInit(): string | undefined {
|
|
154
|
-
if (!core.selfAddr)
|
|
156
|
+
if (!core.selfAddr) {
|
|
157
|
+
return "Talk is not initialized (no session_start yet).";
|
|
158
|
+
}
|
|
155
159
|
return undefined;
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
/** Refresh the talk footer/status-bar text: "alias@group", or "@group" with no explicit alias. */
|
|
159
163
|
function refreshGroupStatus(ui: ExtensionUIContext): void {
|
|
160
164
|
void (async () => {
|
|
161
|
-
if (!self)
|
|
165
|
+
if (!self) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
162
168
|
try {
|
|
163
169
|
const text = await core.groupStatus();
|
|
164
170
|
ui.setStatus("talk", text && ui.theme.fg("accent", text));
|
|
@@ -205,7 +211,9 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
205
211
|
pi.on("session_info_changed", () => {
|
|
206
212
|
// A name set explicitly via `--name` wins over pi's session title;
|
|
207
213
|
// otherwise follow pi's session name.
|
|
208
|
-
if (self)
|
|
214
|
+
if (self) {
|
|
215
|
+
core.setAgentName(explicitName ?? pi.getSessionName() ?? self.name);
|
|
216
|
+
}
|
|
209
217
|
});
|
|
210
218
|
pi.on("session_shutdown", () => {
|
|
211
219
|
void core.stop();
|
|
@@ -223,7 +231,9 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
223
231
|
}),
|
|
224
232
|
async execute(_toolCallId, params) {
|
|
225
233
|
const initError = requireInit();
|
|
226
|
-
if (initError)
|
|
234
|
+
if (initError) {
|
|
235
|
+
return toolResult(initError);
|
|
236
|
+
}
|
|
227
237
|
return toolResult(params.cwd ? await core.listCwd(params.cwd) : await core.list());
|
|
228
238
|
},
|
|
229
239
|
});
|
|
@@ -243,7 +253,9 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
243
253
|
}),
|
|
244
254
|
async execute(_toolCallId, params, signal) {
|
|
245
255
|
const initError = requireInit();
|
|
246
|
-
if (initError)
|
|
256
|
+
if (initError) {
|
|
257
|
+
return toolResult(initError);
|
|
258
|
+
}
|
|
247
259
|
return toolResult(
|
|
248
260
|
await core.ask(
|
|
249
261
|
params.to,
|
|
@@ -267,7 +279,9 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
267
279
|
}),
|
|
268
280
|
async execute(_toolCallId, params) {
|
|
269
281
|
const initError = requireInit();
|
|
270
|
-
if (initError)
|
|
282
|
+
if (initError) {
|
|
283
|
+
return toolResult(initError);
|
|
284
|
+
}
|
|
271
285
|
return toolResult(await core.send(params.to, params.message));
|
|
272
286
|
},
|
|
273
287
|
});
|
|
@@ -417,7 +431,9 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
417
431
|
ctx,
|
|
418
432
|
async (parsed) => {
|
|
419
433
|
const agentName = parsed.flags.name?.trim() || undefined;
|
|
420
|
-
if (agentName !== undefined)
|
|
434
|
+
if (agentName !== undefined) {
|
|
435
|
+
explicitName = agentName;
|
|
436
|
+
}
|
|
421
437
|
return core.groupJoin(parsed.args[0], agentName);
|
|
422
438
|
},
|
|
423
439
|
{ sendToContext: true },
|
|
@@ -433,7 +449,9 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
433
449
|
ctx,
|
|
434
450
|
async (parsed) => {
|
|
435
451
|
const agentName = parsed.flags.name?.trim() || undefined;
|
|
436
|
-
if (agentName !== undefined)
|
|
452
|
+
if (agentName !== undefined) {
|
|
453
|
+
explicitName = agentName;
|
|
454
|
+
}
|
|
437
455
|
return core.groupJoinLast(agentName);
|
|
438
456
|
},
|
|
439
457
|
{ sendToContext: true },
|
|
@@ -496,7 +514,9 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
496
514
|
for (const type of [LIST_TYPE, NOTIFY_TYPE]) {
|
|
497
515
|
pi.registerEntryRenderer<string>(type, (entry, _options, theme) => {
|
|
498
516
|
const text = typeof entry.data === "string" ? entry.data : "";
|
|
499
|
-
if (!text)
|
|
517
|
+
if (!text) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
500
520
|
return {
|
|
501
521
|
render: (width: number) =>
|
|
502
522
|
truncateToVisualLines(text, Infinity, width, 1).visualLines.map((line) =>
|
package/src/talk/mailbox.ts
CHANGED
|
@@ -49,11 +49,19 @@ export type LetterKind = Letter["kind"];
|
|
|
49
49
|
* unchanged; anything else returns null.
|
|
50
50
|
*/
|
|
51
51
|
export function normalizeLetter(value: unknown): Letter | null {
|
|
52
|
-
if (Value.Check(LetterSchema, value))
|
|
53
|
-
|
|
52
|
+
if (Value.Check(LetterSchema, value)) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
if (!isRecord(value)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
54
58
|
const from = value.from;
|
|
55
|
-
if (!isRecord(from))
|
|
56
|
-
|
|
59
|
+
if (!isRecord(from)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (typeof from.sessionId !== "string") {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
57
65
|
const migrated = {
|
|
58
66
|
...value,
|
|
59
67
|
from: { ...from, agentId: from.sessionId },
|
|
@@ -98,7 +106,9 @@ export function newMessageId(): string {
|
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
function assertMessageId(id: string): void {
|
|
101
|
-
if (!MESSAGE_ID_PATTERN.test(id))
|
|
109
|
+
if (!MESSAGE_ID_PATTERN.test(id)) {
|
|
110
|
+
throw new TypeError(`Invalid talk message id: ${id}`);
|
|
111
|
+
}
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
/** The storage key for a letter: `<ts>-<id>.json`, sorted oldest-first. */
|
|
@@ -112,7 +122,9 @@ export function letterFileName(letter: Letter): string {
|
|
|
112
122
|
|
|
113
123
|
/** Structural validation plus filename safety (id/timestamp shape). */
|
|
114
124
|
export function isValidLetter(value: unknown): value is Letter {
|
|
115
|
-
if (!Value.Check(LetterSchema, value))
|
|
125
|
+
if (!Value.Check(LetterSchema, value)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
116
128
|
try {
|
|
117
129
|
letterFileName(value);
|
|
118
130
|
return true;
|
|
@@ -153,7 +165,9 @@ export async function listInbox(storage: TalkStorage, addr: string): Promise<Inb
|
|
|
153
165
|
for (const fileName of await storage.listKeys(inboxNs(addr))) {
|
|
154
166
|
const raw = await storage.readJson(inboxNs(addr), fileName);
|
|
155
167
|
const letter = normalizeLetter(raw);
|
|
156
|
-
if (letter && isValidLetter(letter))
|
|
168
|
+
if (letter && isValidLetter(letter)) {
|
|
169
|
+
out.push({ fileName, letter });
|
|
170
|
+
}
|
|
157
171
|
// corrupt letters are skipped; the caller may remove them separately
|
|
158
172
|
}
|
|
159
173
|
return out; // keys are already sorted by listKeys
|
|
@@ -194,7 +208,9 @@ export async function awaitReceipt(
|
|
|
194
208
|
while (Date.now() < deadline) {
|
|
195
209
|
const inbox = await listInbox(storage, toAddr);
|
|
196
210
|
const stillThere = inbox.some((i) => i.letter.id === letter.id);
|
|
197
|
-
if (!stillThere)
|
|
211
|
+
if (!stillThere) {
|
|
212
|
+
return "delivered";
|
|
213
|
+
}
|
|
198
214
|
await sleep(100);
|
|
199
215
|
}
|
|
200
216
|
const inbox = await listInbox(storage, toAddr);
|
|
@@ -236,7 +252,9 @@ export async function clearAsk(storage: TalkStorage, addr: string, askId: string
|
|
|
236
252
|
export async function outgoingAskIds(storage: TalkStorage, addr: string): Promise<string[]> {
|
|
237
253
|
const out: string[] = [];
|
|
238
254
|
for (const key of await storage.listKeys(asksNs(addr))) {
|
|
239
|
-
if (!key.startsWith("out-") || !key.endsWith(".json"))
|
|
255
|
+
if (!key.startsWith("out-") || !key.endsWith(".json")) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
240
258
|
out.push(key.slice("out-".length, -".json".length));
|
|
241
259
|
}
|
|
242
260
|
return out;
|
|
@@ -271,7 +289,9 @@ export async function readAudit(storage: TalkStorage, limit = 50): Promise<Audit
|
|
|
271
289
|
for (const line of await storage.readLog(AUDIT_LOG)) {
|
|
272
290
|
try {
|
|
273
291
|
const parsed: unknown = JSON.parse(line);
|
|
274
|
-
if (Value.Check(AuditRecordSchema, parsed))
|
|
292
|
+
if (Value.Check(AuditRecordSchema, parsed)) {
|
|
293
|
+
out.push(parsed);
|
|
294
|
+
}
|
|
275
295
|
} catch {
|
|
276
296
|
// skip corrupt line — append-only, never fatal
|
|
277
297
|
}
|
package/src/talk/policy.ts
CHANGED
|
@@ -53,7 +53,9 @@ export class OutboundPolicy {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
const windowStart = now - RATE_LIMIT_WINDOW_MS;
|
|
56
|
-
while (this.sentAt.length > 0 && this.sentAt[0] < windowStart)
|
|
56
|
+
while (this.sentAt.length > 0 && this.sentAt[0] < windowStart) {
|
|
57
|
+
this.sentAt.shift();
|
|
58
|
+
}
|
|
57
59
|
if (this.sentAt.length >= RATE_LIMIT_MAX) {
|
|
58
60
|
return {
|
|
59
61
|
ok: false,
|
|
@@ -73,7 +75,9 @@ export class OutboundPolicy {
|
|
|
73
75
|
}
|
|
74
76
|
const cutoff = this.now() - DEDUPE_WINDOW_MS;
|
|
75
77
|
for (const [key, ts] of this.recentBodies) {
|
|
76
|
-
if (ts < cutoff)
|
|
78
|
+
if (ts < cutoff) {
|
|
79
|
+
this.recentBodies.delete(key);
|
|
80
|
+
}
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
}
|