@trim21/personal-pi-extensions 0.0.348 → 0.0.349
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/README.md +3 -4
- package/package.json +1 -1
- package/src/talk/core.ts +15 -69
- package/src/talk/format.ts +1 -3
- package/src/talk/index.ts +2 -24
- package/src/talk/mailbox.ts +3 -54
- package/src/talk/skills/multi-agent-dev/SKILL.md +4 -5
package/README.md
CHANGED
|
@@ -263,9 +263,8 @@ index.ts —— pi adapter:把 core 接到 pi 的 sendMessage / 生命周
|
|
|
263
263
|
| 工具 | 作用 |
|
|
264
264
|
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
265
265
|
| `talk-list-sessions` | 列出会话,返回 JSON 数组(`status` / `work_dir` / `id` / `name`,自己带 `self: true`);只列出同组成员(未入组时只有自己),`status` 区分 live(`idle` / `working` / `waiting-talk-message`)与 `offline` |
|
|
266
|
-
| `talk-ask` | 向某个 session
|
|
266
|
+
| `talk-ask` | 向某个 session 提问并阻塞等待回应(默认 30 分钟超时);对方发来任何 `talk-send` 消息都会解除等待 |
|
|
267
267
|
| `talk-send` | 发送纯文本消息到单个 session(`to` 只接受明确的 session id,不支持广播) |
|
|
268
|
-
| `talk-reply` | 回复一个 ask(`replyTo` 为 ask id,显式关联、不推断) |
|
|
269
268
|
|
|
270
269
|
对端消息自动投递(无需主动拉取):投递方式由 `talk.deliver` 配置,`steer` 在模型工作过程中打断/唤醒,`queue` 排队到 session 下一轮自然 turn 时注入。
|
|
271
270
|
|
|
@@ -275,10 +274,10 @@ index.ts —— pi adapter:把 core 接到 pi 的 sendMessage / 生命周
|
|
|
275
274
|
|
|
276
275
|
### 关键设计
|
|
277
276
|
|
|
278
|
-
- **presence 不靠心跳**:presence 由 `offline` 标志 + 进程 pid 存活判定;pid 存活时还校验进程启动时间(`/proc/<pid>/stat`),排除 pid 回卷复用造成的误判。未标记 offline 且进程存活即 live,否则 offline;没有心跳,进程挂死(wedged)与健康空闲不可区分。`status` 在 live 时显示 `working` / `waiting-talk-message`(`talk-ask`
|
|
277
|
+
- **presence 不靠心跳**:presence 由 `offline` 标志 + 进程 pid 存活判定;pid 存活时还校验进程启动时间(`/proc/<pid>/stat`),排除 pid 回卷复用造成的误判。未标记 offline 且进程存活即 live,否则 offline;没有心跳,进程挂死(wedged)与健康空闲不可区分。`status` 在 live 时显示 `working` / `waiting-talk-message`(`talk-ask` 阻塞等待回应中)/ `idle`。
|
|
279
278
|
- **定期清理**:进程仍存活的记录永不回收;进程已死且最后活跃超过 24h 且无未投递 mail 的记录会被定期 sweep(30 分钟一次)回收;有 mail 的保留 30 天。resume 后 session 会自动重新注册,无 mail 即无损失。
|
|
280
279
|
- **投递成功才消费**:信件只在成功交给 `sendMessage` 后才从 inbox 删除,投递失败留在 inbox 下次重试——不会因 `sendMessage` 吞异常而静默丢信。
|
|
281
|
-
- **双向 ask 仲裁**:`talk-ask` 发起前先检查收件箱(有对方消息就先读/先回);阻塞等待期间若收到对方的 ask
|
|
280
|
+
- **双向 ask 仲裁**:`talk-ask` 发起前先检查收件箱(有对方消息就先读/先回);阻塞等待期间若收到对方的 ask,按两个 ask 的 `ts` 字段仲裁——先 ask 者主导继续等,后 ask 者让位并先回复对方。`ts` 是信件内固定字段,双方读到同一对值,结论天然对称;同毫秒碰撞用 `session dir + session id` 字符串比较兜底。
|
|
282
281
|
- **typebox runtime 验证**:所有从存储读出的值经 TypeBox schema 校验,损坏/伪造数据被拒绝,不做 `as T` 强转。
|
|
283
282
|
- **安全**:纯文本 ≤32KB;10s 去重 / 30s 限速 8 条 / 50 积压上限(防环);每条投递标注来源(来自另一个 pi session,非用户)。
|
|
284
283
|
- **group 可见性**:可见性完全由 group 决定——组内 session 只能看到同组成员,不在任何 group 的 session 只能看到自己。用 `/talk-group-*` 命令建组/入组,见下方「group 可见性」。
|
package/package.json
CHANGED
package/src/talk/core.ts
CHANGED
|
@@ -38,8 +38,6 @@ import {
|
|
|
38
38
|
previewBody,
|
|
39
39
|
readOutgoingAsk,
|
|
40
40
|
removeLetter,
|
|
41
|
-
resolveAskByRef,
|
|
42
|
-
trackIncomingAsk,
|
|
43
41
|
trackOutgoingAsk,
|
|
44
42
|
unreadCount,
|
|
45
43
|
} from "./mailbox.js";
|
|
@@ -56,8 +54,9 @@ import {
|
|
|
56
54
|
} from "./registry.js";
|
|
57
55
|
import type { TalkStorage } from "./storage.js";
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
interface AskOutcome {
|
|
58
|
+
reason: string;
|
|
59
|
+
}
|
|
61
60
|
type TargetResult = { ok: true; record: AgentRecord } | { ok: false; error: string };
|
|
62
61
|
type SendResult = { ok: true; letter: Letter; verdict: string } | { ok: false; error: string };
|
|
63
62
|
|
|
@@ -241,44 +240,28 @@ export class TalkCore {
|
|
|
241
240
|
}
|
|
242
241
|
|
|
243
242
|
/**
|
|
244
|
-
* Shared per-letter inbound handling:
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
243
|
+
* Shared per-letter inbound handling: run ask interlock arbitration on
|
|
244
|
+
* incoming asks, and break the wait of an outstanding ask to the peer when
|
|
245
|
+
* the peer sends a plain message (ask = send + wait; any peer message is a
|
|
246
|
+
* response). Returns false when the letter was fully handled here (routed to
|
|
247
|
+
* a waiter) and must not be handed to the model.
|
|
248
248
|
*/
|
|
249
249
|
private async processIncoming(letter: Letter): Promise<boolean> {
|
|
250
250
|
const self = this.requireSelf();
|
|
251
|
-
if ((letter.kind === "reply" || letter.kind === "cancel") && letter.replyTo) {
|
|
252
|
-
const waiter = this.askWaiters.get(letter.replyTo);
|
|
253
|
-
await clearAsk(this.storage, self.addr, letter.replyTo);
|
|
254
|
-
if (waiter) {
|
|
255
|
-
this.askWaiters.delete(letter.replyTo);
|
|
256
|
-
waiter(
|
|
257
|
-
letter.kind === "reply"
|
|
258
|
-
? { replied: true, body: letter.body, from: letter.from.name }
|
|
259
|
-
: { replied: false, reason: `cancelled by ${letter.from.name}` },
|
|
260
|
-
);
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
251
|
if (letter.kind === "ask") {
|
|
265
252
|
// Both sides asking each other: timestamp arbitration before delivering,
|
|
266
253
|
// so the later asker yields and answers the earlier ask instead of
|
|
267
254
|
// both timing out.
|
|
268
255
|
await this.resolveInterlock(letter);
|
|
269
|
-
await trackIncomingAsk(this.storage, self.addr, letter);
|
|
270
256
|
} else if (letter.kind === "message") {
|
|
271
257
|
// A plain message from a peer we are blocked asking breaks the wait: the
|
|
272
|
-
// peer is engaging, so do not keep the caller stuck waiting
|
|
258
|
+
// peer is engaging, so do not keep the caller stuck waiting.
|
|
273
259
|
const myAsk = await this.findOutAskTo(letter.from.addr);
|
|
274
260
|
if (myAsk) {
|
|
275
261
|
const waiter = this.askWaiters.get(myAsk.askId);
|
|
276
262
|
if (waiter) {
|
|
277
263
|
this.askWaiters.delete(myAsk.askId);
|
|
278
|
-
waiter({
|
|
279
|
-
replied: false,
|
|
280
|
-
reason: "peer sent a message instead of replying to your ask",
|
|
281
|
-
});
|
|
264
|
+
waiter({ reason: "peer sent a message in response" });
|
|
282
265
|
await clearAsk(this.storage, self.addr, myAsk.askId);
|
|
283
266
|
}
|
|
284
267
|
}
|
|
@@ -343,7 +326,6 @@ export class TalkCore {
|
|
|
343
326
|
target: AgentRecord,
|
|
344
327
|
kind: LetterKind,
|
|
345
328
|
body: string,
|
|
346
|
-
replyTo?: string,
|
|
347
329
|
): Promise<SendResult> {
|
|
348
330
|
const self = this.requireSelf();
|
|
349
331
|
const presence = presenceOf(target);
|
|
@@ -360,7 +342,6 @@ export class TalkCore {
|
|
|
360
342
|
body,
|
|
361
343
|
ts: this.now(),
|
|
362
344
|
};
|
|
363
|
-
if (replyTo !== undefined) letter.replyTo = replyTo;
|
|
364
345
|
await deposit(this.storage, target.addr, letter);
|
|
365
346
|
this.policy.recordSend(body, target.addr);
|
|
366
347
|
if (presence === "live") {
|
|
@@ -392,11 +373,10 @@ export class TalkCore {
|
|
|
392
373
|
resolve(outcome);
|
|
393
374
|
};
|
|
394
375
|
const timer = setTimeout(
|
|
395
|
-
() =>
|
|
396
|
-
settle({ replied: false, reason: `no reply within ${Math.round(timeoutMs / 1000)}s` }),
|
|
376
|
+
() => settle({ reason: `no response within ${Math.round(timeoutMs / 1000)}s` }),
|
|
397
377
|
timeoutMs,
|
|
398
378
|
);
|
|
399
|
-
const onAbort = () => settle({
|
|
379
|
+
const onAbort = () => settle({ reason: "aborted" });
|
|
400
380
|
this.askWaiters.set(askId, settle);
|
|
401
381
|
if (signal?.aborted) onAbort();
|
|
402
382
|
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -440,8 +420,7 @@ export class TalkCore {
|
|
|
440
420
|
if (!peerFirst) return; // we asked first; keep waiting — the peer will yield
|
|
441
421
|
this.askWaiters.delete(myAsk.askId);
|
|
442
422
|
waiter({
|
|
443
|
-
|
|
444
|
-
reason: `peer asked first (their ask id ${letter.id.slice(-8)}) — answer it with talk-reply before re-asking`,
|
|
423
|
+
reason: `peer asked first (their ask id ${letter.id.slice(-8)}) — respond with a message before re-asking`,
|
|
445
424
|
});
|
|
446
425
|
await clearAsk(this.storage, self.addr, myAsk.askId);
|
|
447
426
|
}
|
|
@@ -696,7 +675,7 @@ export class TalkCore {
|
|
|
696
675
|
const inbox = await listInbox(this.storage, self.addr);
|
|
697
676
|
const fromTarget = inbox.filter((item) => item.letter.from.addr === record.addr);
|
|
698
677
|
if (fromTarget.length > 0) {
|
|
699
|
-
return `You have ${fromTarget.length} unread message(s) from "${record.name}" —
|
|
678
|
+
return `You have ${fromTarget.length} unread message(s) from "${record.name}" — respond to them before asking.`;
|
|
700
679
|
}
|
|
701
680
|
const sent = await this.sendLetter(record, "ask", body);
|
|
702
681
|
if (!sent.ok) return sent.error;
|
|
@@ -710,46 +689,13 @@ export class TalkCore {
|
|
|
710
689
|
try {
|
|
711
690
|
const outcome = await this.waitForReply(sent.letter.id, Math.max(1000, timeoutMs), signal);
|
|
712
691
|
await clearAsk(this.storage, self.addr, sent.letter.id);
|
|
713
|
-
|
|
714
|
-
return `Ask ${sent.letter.id.slice(-8)} to "${record.name}": ${outcome.reason}.`;
|
|
715
|
-
return `"${record.name}" replied:\n\n${outcome.body}`;
|
|
692
|
+
return `Ask ${sent.letter.id.slice(-8)} to "${record.name}": ${outcome.reason}.`;
|
|
716
693
|
} finally {
|
|
717
694
|
// The ask tool call is still part of a running agent turn.
|
|
718
695
|
this.setWorking();
|
|
719
696
|
}
|
|
720
697
|
}
|
|
721
698
|
|
|
722
|
-
async reply(replyTo: string, body: string): Promise<string> {
|
|
723
|
-
if (!body) return "reply requires 'message'.";
|
|
724
|
-
if (!replyTo) {
|
|
725
|
-
return "reply requires 'replyTo' (the ask/message id, shown in the delivered message).";
|
|
726
|
-
}
|
|
727
|
-
const self = this.requireSelf();
|
|
728
|
-
const ask = await resolveAskByRef(this.storage, self.addr, replyTo);
|
|
729
|
-
if (!ask) return `No pending ask matches '${replyTo}'.`;
|
|
730
|
-
const records = await listRecords(this.storage);
|
|
731
|
-
const asker = records.find((r) => r.addr === ask.from.addr);
|
|
732
|
-
const target = asker ?? this.recordFromLetter(ask);
|
|
733
|
-
const sent = await this.sendLetter(target, "reply", body, ask.id);
|
|
734
|
-
if (!sent.ok) return sent.error;
|
|
735
|
-
await clearAsk(this.storage, self.addr, ask.id);
|
|
736
|
-
return `Replied to "${target.name}" (ask ${ask.id.slice(-8)}): ${sent.verdict}.`;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/** Build a minimal record from a letter's sender when the peer record is gone. */
|
|
740
|
-
private recordFromLetter(letter: Letter): AgentRecord {
|
|
741
|
-
return {
|
|
742
|
-
addr: letter.from.addr,
|
|
743
|
-
agentId: letter.from.agentId,
|
|
744
|
-
name: letter.from.name,
|
|
745
|
-
cwd: letter.from.cwd,
|
|
746
|
-
pid: 0,
|
|
747
|
-
startedAt: letter.ts,
|
|
748
|
-
lastSeenAt: letter.ts,
|
|
749
|
-
status: "idle",
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
|
|
753
699
|
// ── Presence watch ─────────────────────────────────────────────────────
|
|
754
700
|
|
|
755
701
|
async watch(to: string): Promise<string> {
|
package/src/talk/format.ts
CHANGED
|
@@ -25,9 +25,7 @@ export function formatDelivery(letter: Letter, now: number = Date.now()): string
|
|
|
25
25
|
const from = letter.from;
|
|
26
26
|
const header = `From pi agent ${from.agentId} (${from.cwd}) — "${from.name}"`;
|
|
27
27
|
const meta = `_id ${letter.id} · ${letter.kind} · sent ${age(letter.ts, now)}_`;
|
|
28
|
-
|
|
29
|
-
letter.kind === "ask" ? `\n\nReply with the talk-reply tool, replyTo: "${letter.id}"` : "";
|
|
30
|
-
return `${BOUNDARY_PREAMBLE}\n\n${header}:\n\n${letter.body}\n\n${meta}${hint}`;
|
|
28
|
+
return `${BOUNDARY_PREAMBLE}\n\n${header}:\n\n${letter.body}\n\n${meta}`;
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
/** One agent as the model sees it in a listing. `id` is the stable pi
|
package/src/talk/index.ts
CHANGED
|
@@ -83,7 +83,6 @@ interface DeliveryDetails {
|
|
|
83
83
|
from: Letter["from"];
|
|
84
84
|
ts: number;
|
|
85
85
|
body: string;
|
|
86
|
-
replyTo?: string;
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
/**
|
|
@@ -126,7 +125,6 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
126
125
|
from: letter.from,
|
|
127
126
|
ts: letter.ts,
|
|
128
127
|
body: letter.body,
|
|
129
|
-
...(letter.replyTo !== undefined && { replyTo: letter.replyTo }),
|
|
130
128
|
};
|
|
131
129
|
try {
|
|
132
130
|
// The core only removes the letter from the inbox after this returns
|
|
@@ -225,8 +223,8 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
225
223
|
name: "talk-ask",
|
|
226
224
|
label: "Ask Talk",
|
|
227
225
|
description:
|
|
228
|
-
"Ask another pi agent a question and block until it
|
|
229
|
-
promptSnippet: "Ask another pi agent a question and wait for
|
|
226
|
+
"Ask another pi agent a question and block until it responds (or times out). Before asking, it checks whether that agent already sent you something; if so, you are told to read and respond first instead of asking.",
|
|
227
|
+
promptSnippet: "Ask another pi agent a question and wait for a response",
|
|
230
228
|
parameters: Type.Object({
|
|
231
229
|
to: Type.String({ description: "Target agent (name/address/@alias)" }),
|
|
232
230
|
message: Type.String({ description: "The question" }),
|
|
@@ -265,23 +263,6 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
265
263
|
},
|
|
266
264
|
});
|
|
267
265
|
|
|
268
|
-
pi.registerTool({
|
|
269
|
-
name: "talk-reply",
|
|
270
|
-
label: "Reply Talk",
|
|
271
|
-
description:
|
|
272
|
-
"Reply to a received ask. `replyTo` is the ask/message id (shown in the delivered message).",
|
|
273
|
-
promptSnippet: "Reply to a talk ask",
|
|
274
|
-
parameters: Type.Object({
|
|
275
|
-
replyTo: Type.String({ description: "The ask/message id to reply to" }),
|
|
276
|
-
message: Type.String({ description: "The reply body" }),
|
|
277
|
-
}),
|
|
278
|
-
async execute(_toolCallId, params) {
|
|
279
|
-
const initError = requireInit();
|
|
280
|
-
if (initError) return toolResult(initError);
|
|
281
|
-
return toolResult(await core.reply(params.replyTo, params.message));
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
|
|
285
266
|
// ── /talk commands ────────────────────────────────────────────────────
|
|
286
267
|
|
|
287
268
|
type OkResult<TFlags extends TObject> = Extract<CommandResult<TFlags>, { kind: "ok" }>;
|
|
@@ -496,9 +477,6 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
496
477
|
const header = `${theme.fg("accent", theme.bold(displayName(d.from.name)))} ${theme.fg("dim", `(${shortCwd(d.from.cwd)})`)} ${chip}`;
|
|
497
478
|
const footer = theme.fg("dim", `id ${idTail} · ${d.kind} · ${relativeTime(d.ts)}`);
|
|
498
479
|
const out = [header, sanitizeTerminal(d.body), "", footer];
|
|
499
|
-
if (d.kind === "ask") {
|
|
500
|
-
out.push(theme.fg("dim", `└─ reply via talk-reply, replyTo: "${idTail}"`));
|
|
501
|
-
}
|
|
502
480
|
// plain 组件:不引入 pi-tui,直接输出带背景色的文本行
|
|
503
481
|
const text = out.join("\n");
|
|
504
482
|
return {
|
package/src/talk/mailbox.ts
CHANGED
|
@@ -202,28 +202,14 @@ export async function awaitReceipt(
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
// ── Ask tracking ─────────────────────────────────────────────────────────
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// time out.
|
|
208
|
-
|
|
209
|
-
function askKey(askId: string): string {
|
|
210
|
-
assertMessageId(askId);
|
|
211
|
-
return `${askId}.json`;
|
|
212
|
-
}
|
|
205
|
+
// Our outgoing asks live at asks/<addr>/out-<id>.json until a message from
|
|
206
|
+
// the peer breaks the wait, the peer answers first, or we time out.
|
|
213
207
|
|
|
214
208
|
function outAskKey(askId: string): string {
|
|
215
209
|
assertMessageId(askId);
|
|
216
210
|
return `out-${askId}.json`;
|
|
217
211
|
}
|
|
218
212
|
|
|
219
|
-
export async function trackIncomingAsk(
|
|
220
|
-
storage: TalkStorage,
|
|
221
|
-
addr: string,
|
|
222
|
-
letter: Letter,
|
|
223
|
-
): Promise<void> {
|
|
224
|
-
await storage.writeJson(asksNs(addr), askKey(letter.id), letter);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
213
|
export async function trackOutgoingAsk(
|
|
228
214
|
storage: TalkStorage,
|
|
229
215
|
addr: string,
|
|
@@ -232,15 +218,6 @@ export async function trackOutgoingAsk(
|
|
|
232
218
|
await storage.writeJson(asksNs(addr), outAskKey(out.askId), out);
|
|
233
219
|
}
|
|
234
220
|
|
|
235
|
-
export async function readIncomingAsk(
|
|
236
|
-
storage: TalkStorage,
|
|
237
|
-
addr: string,
|
|
238
|
-
askId: string,
|
|
239
|
-
): Promise<Letter | null> {
|
|
240
|
-
const raw = await storage.readJson(asksNs(addr), askKey(askId));
|
|
241
|
-
return normalizeLetter(raw);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
221
|
export async function readOutgoingAsk(
|
|
245
222
|
storage: TalkStorage,
|
|
246
223
|
addr: string,
|
|
@@ -250,24 +227,11 @@ export async function readOutgoingAsk(
|
|
|
250
227
|
return Value.Check(OutAskSchema, raw) ? raw : null;
|
|
251
228
|
}
|
|
252
229
|
|
|
253
|
-
/** Remove
|
|
230
|
+
/** Remove our outstanding ask. Idempotent. */
|
|
254
231
|
export async function clearAsk(storage: TalkStorage, addr: string, askId: string): Promise<void> {
|
|
255
|
-
await storage.removeKey(asksNs(addr), askKey(askId));
|
|
256
232
|
await storage.removeKey(asksNs(addr), outAskKey(askId));
|
|
257
233
|
}
|
|
258
234
|
|
|
259
|
-
/** Asks we have received and not yet answered, oldest first. */
|
|
260
|
-
export async function pendingAsks(storage: TalkStorage, addr: string): Promise<Letter[]> {
|
|
261
|
-
const out: Letter[] = [];
|
|
262
|
-
for (const key of await storage.listKeys(asksNs(addr))) {
|
|
263
|
-
if (key.startsWith("out-")) continue;
|
|
264
|
-
const raw = await storage.readJson(asksNs(addr), key);
|
|
265
|
-
const letter = normalizeLetter(raw);
|
|
266
|
-
if (letter) out.push(letter);
|
|
267
|
-
}
|
|
268
|
-
return out;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
235
|
/** Outgoing ask ids, used by the core for interlock arbitration. */
|
|
272
236
|
export async function outgoingAskIds(storage: TalkStorage, addr: string): Promise<string[]> {
|
|
273
237
|
const out: string[] = [];
|
|
@@ -278,21 +242,6 @@ export async function outgoingAskIds(storage: TalkStorage, addr: string): Promis
|
|
|
278
242
|
return out;
|
|
279
243
|
}
|
|
280
244
|
|
|
281
|
-
/**
|
|
282
|
-
* Resolve a pending ask by explicit replyTo id or unique suffix. No inference.
|
|
283
|
-
* Matching is on the suffix: the prefix of a time-ordered (v7-style) id is the
|
|
284
|
-
* timestamp part and collides easily, the suffix is the random part.
|
|
285
|
-
*/
|
|
286
|
-
export async function resolveAskByRef(
|
|
287
|
-
storage: TalkStorage,
|
|
288
|
-
addr: string,
|
|
289
|
-
replyTo: string,
|
|
290
|
-
): Promise<Letter | null> {
|
|
291
|
-
if (!replyTo) return null; // empty suffix matches every id — an explicit ref is required
|
|
292
|
-
const asks = await pendingAsks(storage, addr);
|
|
293
|
-
return asks.find((a) => a.id === replyTo || a.id.endsWith(replyTo)) ?? null;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
245
|
// ── Audit log ────────────────────────────────────────────────────────────
|
|
297
246
|
|
|
298
247
|
const AUDIT_LOG = "audit";
|
|
@@ -34,7 +34,7 @@ The core rule: **a peer only knows what you tell it.** Messages must be self-con
|
|
|
34
34
|
|
|
35
35
|
### Status
|
|
36
36
|
|
|
37
|
-
- `idle` / `working` (agent actively running) / `waiting-talk-message` (blocked in `talk-ask` waiting for a
|
|
37
|
+
- `idle` / `working` (agent actively running) / `waiting-talk-message` (blocked in `talk-ask` waiting for a response)
|
|
38
38
|
- `offline` (process exited or marked dead)
|
|
39
39
|
- `talk-list-agents` lists every visible agent — live or offline — with its current status.
|
|
40
40
|
|
|
@@ -51,8 +51,7 @@ The core rule: **a peer only knows what you tell it.** Messages must be self-con
|
|
|
51
51
|
| ------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
52
52
|
| `talk-list-agents` | List visible agents (`id` / `status` / `work_dir` / `name`); only group co-members (or only yourself when ungrouped) |
|
|
53
53
|
| `talk-send` | Send a plain message to a single agent id (async — the main collaboration primitive) |
|
|
54
|
-
| `talk-ask` | Ask a question and block for
|
|
55
|
-
| `talk-reply` | Reply to a received ask; `replyTo` is the ask id shown in the delivered message |
|
|
54
|
+
| `talk-ask` | Ask a question and block for a response (default 30 min timeout); any message from the peer breaks the wait |
|
|
56
55
|
|
|
57
56
|
Pairing into groups is a user action (`/talk-group-*` in the TUI); you only observe its effect through `talk-list-agents`.
|
|
58
57
|
|
|
@@ -68,7 +67,7 @@ Pairing into groups is a user action (`/talk-group-*` in the TUI); you only obse
|
|
|
68
67
|
### Synchronous question/answer (need the answer to continue)
|
|
69
68
|
|
|
70
69
|
- Use `talk-ask` when the next step depends on the peer's information and the peer is reachable.
|
|
71
|
-
- On receiving an ask,
|
|
70
|
+
- On receiving an ask, respond with `talk-send` — any message you send breaks the peer's wait, so a plain `talk-send` back is the reply.
|
|
72
71
|
- If two agents ask each other simultaneously: the later asker yields — answer the peer's ask first, then re-ask.
|
|
73
72
|
|
|
74
73
|
### Async notifications
|
|
@@ -93,5 +92,5 @@ Pairing into groups is a user action (`/talk-group-*` in the TUI); you only obse
|
|
|
93
92
|
|
|
94
93
|
- **Avoid message loops**: if the peer sent you something or is asking you, answer it before sending new ones. Two agents pinging each other deadlock.
|
|
95
94
|
- **Address from known ids**: only run `talk-list-agents` to discover agents or verify an id. If you already hold a valid id (e.g. from an incoming message or a previous listing), send directly — an unknown or invisible id is refused with `Unknown agent id`.
|
|
96
|
-
- **Respect status**: asking an offline agent blocks until the 30 min timeout. Prefer `talk-send` there — the message queues on disk and the peer receives it when it resumes.
|
|
95
|
+
- **Respect status**: asking an offline agent blocks until the 30 min timeout (nothing breaks the wait). Prefer `talk-send` there — the message queues on disk and the peer receives it when it resumes.
|
|
97
96
|
- **Visibility boundary**: you can only collaborate with agents that share your group; ungrouped agents and other groups' members are unreachable by design. Ask the user to pair agents before collaborating.
|