@agent-marshal/marshal-prime-agent 0.54.1 → 0.55.1
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/daemon.ts +50 -16
- package/src/generated/Message.ts +8 -6
- package/src/generated/MessageRead.ts +18 -1
- package/src/generated/index.ts +245 -245
- package/src/inbox-pull.ts +86 -0
- package/src/index.ts +25 -15
package/package.json
CHANGED
package/src/daemon.ts
CHANGED
|
@@ -41,9 +41,11 @@ import {
|
|
|
41
41
|
type SessionNicknameItem,
|
|
42
42
|
} from "./entities.ts";
|
|
43
43
|
import type { Identity } from "./identity.ts";
|
|
44
|
+
import { InboxPuller, type InboxPull } from "./inbox-pull.ts";
|
|
44
45
|
|
|
45
46
|
const LIVENESS_INTERVAL_MS = 5_000;
|
|
46
47
|
const INBOX_PULL_LIMIT = 20;
|
|
48
|
+
const INBOX_COMMAND_TIMEOUT_MS = 1_500;
|
|
47
49
|
const SOURCE_ID = "marshal-prime-agent";
|
|
48
50
|
|
|
49
51
|
/** myko's CellServer serves its WebSocket at the `/myko` path. */
|
|
@@ -78,7 +80,11 @@ export class MarshalDaemon {
|
|
|
78
80
|
private roster: SessionItem[] = [];
|
|
79
81
|
private nicknames: SessionNicknameItem[] = [];
|
|
80
82
|
|
|
81
|
-
private readonly
|
|
83
|
+
private readonly inbox = new InboxPuller<MessageView>({
|
|
84
|
+
read: (sessionId) => this.readUnread(sessionId),
|
|
85
|
+
ack: async (sessionId, messageIds) => { await this.sendForInbox(ackMessages(sessionId, messageIds)); },
|
|
86
|
+
render: (messages) => this.renderInbox(messages),
|
|
87
|
+
});
|
|
82
88
|
private notifyHandler: NotifyHandler | null = null;
|
|
83
89
|
private livenessTimer: ReturnType<typeof setInterval> | null = null;
|
|
84
90
|
private started = false;
|
|
@@ -250,12 +256,14 @@ export class MarshalDaemon {
|
|
|
250
256
|
return `- [${m.messageId}] ${this.senderLabel(m.fromSessionId)} [response_required=${m.responseRequired ?? false}]: ${m.body}`;
|
|
251
257
|
}
|
|
252
258
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Read the unread inbox without acking it. Commit the pull once its text is
|
|
261
|
+
* in the conversation, or release it if it never gets there, so a message is
|
|
262
|
+
* never marked read without having been shown.
|
|
263
|
+
*/
|
|
264
|
+
pullInbox(sessionId: string): Promise<InboxPull | null> {
|
|
265
|
+
if (!this.connected) return Promise.resolve(null);
|
|
266
|
+
return this.inbox.pull(sessionId).catch(() => null);
|
|
259
267
|
}
|
|
260
268
|
|
|
261
269
|
nicknameFor(sessionId: string): string {
|
|
@@ -270,11 +278,11 @@ export class MarshalDaemon {
|
|
|
270
278
|
|
|
271
279
|
// ── Internals ──────────────────────────────────────────────────────────
|
|
272
280
|
|
|
273
|
-
private async
|
|
281
|
+
private async readUnread(sessionId: string): Promise<MessageView[] | null> {
|
|
274
282
|
let result: ReadMessagesResult | undefined;
|
|
275
283
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
276
284
|
try {
|
|
277
|
-
result = await this.
|
|
285
|
+
result = await this.sendForInbox(readMessages({
|
|
278
286
|
asSession: sessionId,
|
|
279
287
|
toSession: sessionId,
|
|
280
288
|
inbox: false,
|
|
@@ -288,13 +296,7 @@ export class MarshalDaemon {
|
|
|
288
296
|
if (result.messages && result.messages.length > 0) break;
|
|
289
297
|
if (attempt < 2) await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
|
|
290
298
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const block = this.renderInbox(result.messages);
|
|
294
|
-
try {
|
|
295
|
-
await this.send(ackMessages(sessionId, result.messages.map((m) => m.messageId)));
|
|
296
|
-
} catch { this.log("inbox ack failed"); }
|
|
297
|
-
return block;
|
|
299
|
+
return result?.messages?.length ? result.messages : null;
|
|
298
300
|
}
|
|
299
301
|
|
|
300
302
|
private renderInbox(messages: MessageView[]): string {
|
|
@@ -352,6 +354,38 @@ export class MarshalDaemon {
|
|
|
352
354
|
return this.client.sendCommand(command as never) as Promise<R>;
|
|
353
355
|
}
|
|
354
356
|
|
|
357
|
+
/** Bounded send for the inbox path: reject at once while the WS is down, and
|
|
358
|
+
* otherwise time out, so a read that never answers can't hold up every later
|
|
359
|
+
* pull for the session. */
|
|
360
|
+
private sendForInbox<R>(command: MarshalCommand<R>): Promise<R> {
|
|
361
|
+
if (!this.connected) return Promise.reject(new Error("marshal is disconnected"));
|
|
362
|
+
|
|
363
|
+
return new Promise<R>((resolve, reject) => {
|
|
364
|
+
let settled = false;
|
|
365
|
+
const timer = setTimeout(() => {
|
|
366
|
+
if (settled) return;
|
|
367
|
+
settled = true;
|
|
368
|
+
reject(new Error(`marshal inbox command timed out after ${INBOX_COMMAND_TIMEOUT_MS}ms`));
|
|
369
|
+
}, INBOX_COMMAND_TIMEOUT_MS);
|
|
370
|
+
if (typeof (timer as any).unref === "function") (timer as any).unref();
|
|
371
|
+
|
|
372
|
+
void this.send(command).then(
|
|
373
|
+
(value) => {
|
|
374
|
+
if (settled) return;
|
|
375
|
+
settled = true;
|
|
376
|
+
clearTimeout(timer);
|
|
377
|
+
resolve(value);
|
|
378
|
+
},
|
|
379
|
+
(error: unknown) => {
|
|
380
|
+
if (settled) return;
|
|
381
|
+
settled = true;
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
reject(error);
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
355
389
|
private watch<I>(query: MarshalQuery<I>) {
|
|
356
390
|
return this.client.watchQuery(query as never) as unknown as import("rxjs").Observable<I[]>;
|
|
357
391
|
}
|
package/src/generated/Message.ts
CHANGED
|
@@ -26,28 +26,30 @@ export type Message = {
|
|
|
26
26
|
* pull it. In the pull-via-hook model the recipient reads on its
|
|
27
27
|
* next turn — which can be after the sender's SessionEnd — so
|
|
28
28
|
* cascading on the sender would silently delete unread messages in
|
|
29
|
-
* that window.
|
|
30
|
-
* instead (the `to_session_id` / `to_room_id` cascades below).
|
|
29
|
+
* that window. No recipient-session cascade either (see `to_session_id`).
|
|
31
30
|
*/
|
|
32
31
|
fromSessionId: SessionId,
|
|
33
32
|
/**
|
|
34
33
|
* Direct recipient — set for 1:1 sends, `None` for broadcasts.
|
|
35
|
-
*
|
|
34
|
+
* NOT a `belongs_to(Session)` cascade: a recipient's session row is DEL'd
|
|
35
|
+
* and re-SET under the same id on a reload, a resume or a reaped pull
|
|
36
|
+
* session's next hook, and its direct messages must still be there. A
|
|
37
|
+
* message lives until the daemon's TTL sweep prunes it.
|
|
36
38
|
*/
|
|
37
39
|
toSessionId?: SessionId | null,
|
|
38
40
|
/**
|
|
39
41
|
* Broadcast recipient — set for room sends, `None` for direct
|
|
40
42
|
* sends. Cascade-DELs when the room is DEL'd. (Auto-rooms like
|
|
41
43
|
* `everyone` and `host:*` are never DEL'd, so broadcasts to them
|
|
42
|
-
*
|
|
44
|
+
* persist until the daemon's TTL sweep prunes them.)
|
|
43
45
|
*/
|
|
44
46
|
toRoomId?: RoomId | null,
|
|
45
47
|
/**
|
|
46
48
|
* When this direct message was addressed to a *human* — via their operator
|
|
47
49
|
* identity (the `op:`/`human:`/email tier of recipient resolution) rather
|
|
48
50
|
* than to one specific agent — this holds that operator string (e.g.
|
|
49
|
-
* `max@lucid.rocks`). The message is still routed to
|
|
50
|
-
*
|
|
51
|
+
* `max@lucid.rocks`). The message is still routed to the operator's
|
|
52
|
+
* most-active session via `to_session_id`; this marks it as
|
|
51
53
|
* human-addressed so the receiving agent surfaces it to its operator
|
|
52
54
|
* instead of treating it as ordinary peer chatter, and the UI console can
|
|
53
55
|
* toast it for that operator regardless of which agent it landed on. `None`
|
|
@@ -3,7 +3,24 @@ import type { MessageId } from "./MessageId";
|
|
|
3
3
|
import type { MessageReadId } from "./MessageReadId";
|
|
4
4
|
import type { SessionId } from "./SessionId";
|
|
5
5
|
|
|
6
|
-
export type MessageRead = {
|
|
6
|
+
export type MessageRead = {
|
|
7
|
+
/**
|
|
8
|
+
* Cascade on the *message*: an ack is meaningless once its message is gone,
|
|
9
|
+
* so it dies with the message. This is the ack's primary lifetime owner.
|
|
10
|
+
* The TTL prune deletes messages without cascading, so the daemon's
|
|
11
|
+
* cleanup also sweeps acks whose message is gone (`sweep_orphan_reads`).
|
|
12
|
+
*/
|
|
13
|
+
messageId: MessageId,
|
|
14
|
+
/**
|
|
15
|
+
* NOT a `belongs_to(Session)` cascade: an ack is a fact about (message,
|
|
16
|
+
* reader) and outlives the reader's session row. Harnesses re-register
|
|
17
|
+
* under the same session id after a reload or resume (marshal-pi across
|
|
18
|
+
* `/reload`, the Claude shim on `--resume`, a pull session repaired after
|
|
19
|
+
* the reap backstop), so a session DEL is often a reconnect. Cascading
|
|
20
|
+
* here made every re-registration see the direct messages it had already
|
|
21
|
+
* read as unread, and the inbox handed them over again.
|
|
22
|
+
*/
|
|
23
|
+
sessionId: SessionId,
|
|
7
24
|
/**
|
|
8
25
|
* Wall-clock millis when this session marked the message read.
|
|
9
26
|
*/
|
package/src/generated/index.ts
CHANGED
|
@@ -217,84 +217,6 @@ import type { SetSessionLastToolAt as _SetSessionLastToolAt } from "./SetSession
|
|
|
217
217
|
import type { SetSessionSessionName as _SetSessionSessionName } from "./SetSessionSessionName";
|
|
218
218
|
|
|
219
219
|
// Query classes
|
|
220
|
-
export class GetSessionsByQuery {
|
|
221
|
-
static readonly queryId = "GetSessionsByQuery" as const;
|
|
222
|
-
static readonly queryItemType = "Session" as const;
|
|
223
|
-
readonly queryId = "GetSessionsByQuery" as const;
|
|
224
|
-
readonly queryItemType = "Session" as const;
|
|
225
|
-
readonly query: Omit<_GetSessionsByQuery, "tx" | "createdAt">;
|
|
226
|
-
declare readonly $res: () => Session[];
|
|
227
|
-
|
|
228
|
-
constructor(args: Omit<_GetSessionsByQuery, "tx" | "createdAt">) {
|
|
229
|
-
this.query = args;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export class GetSessionsByIds {
|
|
234
|
-
static readonly queryId = "GetSessionsByIds" as const;
|
|
235
|
-
static readonly queryItemType = "Session" as const;
|
|
236
|
-
readonly queryId = "GetSessionsByIds" as const;
|
|
237
|
-
readonly queryItemType = "Session" as const;
|
|
238
|
-
readonly query: Omit<_GetSessionsByIds, "tx" | "createdAt">;
|
|
239
|
-
declare readonly $res: () => Session[];
|
|
240
|
-
|
|
241
|
-
constructor(args: Omit<_GetSessionsByIds, "tx" | "createdAt">) {
|
|
242
|
-
this.query = args;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export class GetAllSessions {
|
|
247
|
-
static readonly queryId = "GetAllSessions" as const;
|
|
248
|
-
static readonly queryItemType = "Session" as const;
|
|
249
|
-
readonly queryId = "GetAllSessions" as const;
|
|
250
|
-
readonly queryItemType = "Session" as const;
|
|
251
|
-
readonly query: Omit<_GetAllSessions, "tx" | "createdAt">;
|
|
252
|
-
declare readonly $res: () => Session[];
|
|
253
|
-
|
|
254
|
-
constructor(args: Omit<_GetAllSessions, "tx" | "createdAt">) {
|
|
255
|
-
this.query = args;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export class GetMessageReadsByQuery {
|
|
260
|
-
static readonly queryId = "GetMessageReadsByQuery" as const;
|
|
261
|
-
static readonly queryItemType = "MessageRead" as const;
|
|
262
|
-
readonly queryId = "GetMessageReadsByQuery" as const;
|
|
263
|
-
readonly queryItemType = "MessageRead" as const;
|
|
264
|
-
readonly query: Omit<_GetMessageReadsByQuery, "tx" | "createdAt">;
|
|
265
|
-
declare readonly $res: () => MessageRead[];
|
|
266
|
-
|
|
267
|
-
constructor(args: Omit<_GetMessageReadsByQuery, "tx" | "createdAt">) {
|
|
268
|
-
this.query = args;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export class GetMessageReadsByIds {
|
|
273
|
-
static readonly queryId = "GetMessageReadsByIds" as const;
|
|
274
|
-
static readonly queryItemType = "MessageRead" as const;
|
|
275
|
-
readonly queryId = "GetMessageReadsByIds" as const;
|
|
276
|
-
readonly queryItemType = "MessageRead" as const;
|
|
277
|
-
readonly query: Omit<_GetMessageReadsByIds, "tx" | "createdAt">;
|
|
278
|
-
declare readonly $res: () => MessageRead[];
|
|
279
|
-
|
|
280
|
-
constructor(args: Omit<_GetMessageReadsByIds, "tx" | "createdAt">) {
|
|
281
|
-
this.query = args;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export class GetAllMessageReads {
|
|
286
|
-
static readonly queryId = "GetAllMessageReads" as const;
|
|
287
|
-
static readonly queryItemType = "MessageRead" as const;
|
|
288
|
-
readonly queryId = "GetAllMessageReads" as const;
|
|
289
|
-
readonly queryItemType = "MessageRead" as const;
|
|
290
|
-
readonly query: Omit<_GetAllMessageReads, "tx" | "createdAt">;
|
|
291
|
-
declare readonly $res: () => MessageRead[];
|
|
292
|
-
|
|
293
|
-
constructor(args: Omit<_GetAllMessageReads, "tx" | "createdAt">) {
|
|
294
|
-
this.query = args;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
220
|
export class GetSessionNicknamesByQuery {
|
|
299
221
|
static readonly queryId = "GetSessionNicknamesByQuery" as const;
|
|
300
222
|
static readonly queryItemType = "SessionNickname" as const;
|
|
@@ -490,75 +412,87 @@ export class GetAllMessages {
|
|
|
490
412
|
}
|
|
491
413
|
}
|
|
492
414
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
readonly
|
|
499
|
-
readonly
|
|
500
|
-
declare readonly $res: () => Session | null;
|
|
415
|
+
export class GetSessionsByQuery {
|
|
416
|
+
static readonly queryId = "GetSessionsByQuery" as const;
|
|
417
|
+
static readonly queryItemType = "Session" as const;
|
|
418
|
+
readonly queryId = "GetSessionsByQuery" as const;
|
|
419
|
+
readonly queryItemType = "Session" as const;
|
|
420
|
+
readonly query: Omit<_GetSessionsByQuery, "tx" | "createdAt">;
|
|
421
|
+
declare readonly $res: () => Session[];
|
|
501
422
|
|
|
502
|
-
constructor(args: Omit<
|
|
503
|
-
this.
|
|
423
|
+
constructor(args: Omit<_GetSessionsByQuery, "tx" | "createdAt">) {
|
|
424
|
+
this.query = args;
|
|
504
425
|
}
|
|
505
426
|
}
|
|
506
427
|
|
|
507
|
-
export class
|
|
508
|
-
static readonly
|
|
509
|
-
readonly
|
|
510
|
-
readonly
|
|
511
|
-
|
|
428
|
+
export class GetSessionsByIds {
|
|
429
|
+
static readonly queryId = "GetSessionsByIds" as const;
|
|
430
|
+
static readonly queryItemType = "Session" as const;
|
|
431
|
+
readonly queryId = "GetSessionsByIds" as const;
|
|
432
|
+
readonly queryItemType = "Session" as const;
|
|
433
|
+
readonly query: Omit<_GetSessionsByIds, "tx" | "createdAt">;
|
|
434
|
+
declare readonly $res: () => Session[];
|
|
512
435
|
|
|
513
|
-
constructor(args: Omit<
|
|
514
|
-
this.
|
|
436
|
+
constructor(args: Omit<_GetSessionsByIds, "tx" | "createdAt">) {
|
|
437
|
+
this.query = args;
|
|
515
438
|
}
|
|
516
439
|
}
|
|
517
440
|
|
|
518
|
-
export class
|
|
519
|
-
static readonly
|
|
520
|
-
readonly
|
|
521
|
-
readonly
|
|
522
|
-
|
|
441
|
+
export class GetAllSessions {
|
|
442
|
+
static readonly queryId = "GetAllSessions" as const;
|
|
443
|
+
static readonly queryItemType = "Session" as const;
|
|
444
|
+
readonly queryId = "GetAllSessions" as const;
|
|
445
|
+
readonly queryItemType = "Session" as const;
|
|
446
|
+
readonly query: Omit<_GetAllSessions, "tx" | "createdAt">;
|
|
447
|
+
declare readonly $res: () => Session[];
|
|
523
448
|
|
|
524
|
-
constructor(args: Omit<
|
|
525
|
-
this.
|
|
449
|
+
constructor(args: Omit<_GetAllSessions, "tx" | "createdAt">) {
|
|
450
|
+
this.query = args;
|
|
526
451
|
}
|
|
527
452
|
}
|
|
528
453
|
|
|
529
|
-
export class
|
|
530
|
-
static readonly
|
|
531
|
-
readonly
|
|
532
|
-
readonly
|
|
533
|
-
|
|
454
|
+
export class GetMessageReadsByQuery {
|
|
455
|
+
static readonly queryId = "GetMessageReadsByQuery" as const;
|
|
456
|
+
static readonly queryItemType = "MessageRead" as const;
|
|
457
|
+
readonly queryId = "GetMessageReadsByQuery" as const;
|
|
458
|
+
readonly queryItemType = "MessageRead" as const;
|
|
459
|
+
readonly query: Omit<_GetMessageReadsByQuery, "tx" | "createdAt">;
|
|
460
|
+
declare readonly $res: () => MessageRead[];
|
|
534
461
|
|
|
535
|
-
constructor(args: Omit<
|
|
536
|
-
this.
|
|
462
|
+
constructor(args: Omit<_GetMessageReadsByQuery, "tx" | "createdAt">) {
|
|
463
|
+
this.query = args;
|
|
537
464
|
}
|
|
538
465
|
}
|
|
539
466
|
|
|
540
|
-
export class
|
|
541
|
-
static readonly
|
|
542
|
-
readonly
|
|
543
|
-
readonly
|
|
544
|
-
|
|
467
|
+
export class GetMessageReadsByIds {
|
|
468
|
+
static readonly queryId = "GetMessageReadsByIds" as const;
|
|
469
|
+
static readonly queryItemType = "MessageRead" as const;
|
|
470
|
+
readonly queryId = "GetMessageReadsByIds" as const;
|
|
471
|
+
readonly queryItemType = "MessageRead" as const;
|
|
472
|
+
readonly query: Omit<_GetMessageReadsByIds, "tx" | "createdAt">;
|
|
473
|
+
declare readonly $res: () => MessageRead[];
|
|
545
474
|
|
|
546
|
-
constructor(args: Omit<
|
|
547
|
-
this.
|
|
475
|
+
constructor(args: Omit<_GetMessageReadsByIds, "tx" | "createdAt">) {
|
|
476
|
+
this.query = args;
|
|
548
477
|
}
|
|
549
478
|
}
|
|
550
479
|
|
|
551
|
-
export class
|
|
552
|
-
static readonly
|
|
553
|
-
readonly
|
|
554
|
-
readonly
|
|
555
|
-
|
|
480
|
+
export class GetAllMessageReads {
|
|
481
|
+
static readonly queryId = "GetAllMessageReads" as const;
|
|
482
|
+
static readonly queryItemType = "MessageRead" as const;
|
|
483
|
+
readonly queryId = "GetAllMessageReads" as const;
|
|
484
|
+
readonly queryItemType = "MessageRead" as const;
|
|
485
|
+
readonly query: Omit<_GetAllMessageReads, "tx" | "createdAt">;
|
|
486
|
+
declare readonly $res: () => MessageRead[];
|
|
556
487
|
|
|
557
|
-
constructor(args: Omit<
|
|
558
|
-
this.
|
|
488
|
+
constructor(args: Omit<_GetAllMessageReads, "tx" | "createdAt">) {
|
|
489
|
+
this.query = args;
|
|
559
490
|
}
|
|
560
491
|
}
|
|
561
492
|
|
|
493
|
+
// View classes
|
|
494
|
+
|
|
495
|
+
// Report classes
|
|
562
496
|
export class GetSessionNicknameById {
|
|
563
497
|
static readonly reportId = "GetSessionNicknameById" as const;
|
|
564
498
|
readonly reportId = "GetSessionNicknameById" as const;
|
|
@@ -724,150 +658,73 @@ export class CountAllMessages {
|
|
|
724
658
|
}
|
|
725
659
|
}
|
|
726
660
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
readonly
|
|
731
|
-
readonly
|
|
732
|
-
declare readonly $res: () => BroadcastMessageResult;
|
|
733
|
-
|
|
734
|
-
constructor(args: Omit<_BroadcastMessage, "tx" | "createdAt">) {
|
|
735
|
-
this.command = args;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
export class SendMessage {
|
|
740
|
-
static readonly commandId = "SendMessage" as const;
|
|
741
|
-
readonly commandId = "SendMessage" as const;
|
|
742
|
-
readonly command: Omit<_SendMessage, "tx" | "createdAt">;
|
|
743
|
-
declare readonly $res: () => SendMessageResult;
|
|
744
|
-
|
|
745
|
-
constructor(args: Omit<_SendMessage, "tx" | "createdAt">) {
|
|
746
|
-
this.command = args;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
export class SetSessionLastToolAt {
|
|
751
|
-
static readonly commandId = "SetSessionLastToolAt" as const;
|
|
752
|
-
readonly commandId = "SetSessionLastToolAt" as const;
|
|
753
|
-
readonly command: Omit<_SetSessionLastToolAt, "tx" | "createdAt">;
|
|
754
|
-
declare readonly $res: () => void;
|
|
755
|
-
|
|
756
|
-
constructor(args: Omit<_SetSessionLastToolAt, "tx" | "createdAt">) {
|
|
757
|
-
this.command = args;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
export class SetSessionLastTool {
|
|
762
|
-
static readonly commandId = "SetSessionLastTool" as const;
|
|
763
|
-
readonly commandId = "SetSessionLastTool" as const;
|
|
764
|
-
readonly command: Omit<_SetSessionLastTool, "tx" | "createdAt">;
|
|
765
|
-
declare readonly $res: () => void;
|
|
766
|
-
|
|
767
|
-
constructor(args: Omit<_SetSessionLastTool, "tx" | "createdAt">) {
|
|
768
|
-
this.command = args;
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
export class SetSessionLastActivityAt {
|
|
773
|
-
static readonly commandId = "SetSessionLastActivityAt" as const;
|
|
774
|
-
readonly commandId = "SetSessionLastActivityAt" as const;
|
|
775
|
-
readonly command: Omit<_SetSessionLastActivityAt, "tx" | "createdAt">;
|
|
776
|
-
declare readonly $res: () => void;
|
|
777
|
-
|
|
778
|
-
constructor(args: Omit<_SetSessionLastActivityAt, "tx" | "createdAt">) {
|
|
779
|
-
this.command = args;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
export class SetSessionGitBranch {
|
|
784
|
-
static readonly commandId = "SetSessionGitBranch" as const;
|
|
785
|
-
readonly commandId = "SetSessionGitBranch" as const;
|
|
786
|
-
readonly command: Omit<_SetSessionGitBranch, "tx" | "createdAt">;
|
|
787
|
-
declare readonly $res: () => void;
|
|
788
|
-
|
|
789
|
-
constructor(args: Omit<_SetSessionGitBranch, "tx" | "createdAt">) {
|
|
790
|
-
this.command = args;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
export class DeleteSessions {
|
|
795
|
-
static readonly commandId = "DeleteSessions" as const;
|
|
796
|
-
readonly commandId = "DeleteSessions" as const;
|
|
797
|
-
readonly command: Omit<_DeleteSessions, "tx" | "createdAt">;
|
|
798
|
-
declare readonly $res: () => DeleteSessionsResult;
|
|
799
|
-
|
|
800
|
-
constructor(args: Omit<_DeleteSessions, "tx" | "createdAt">) {
|
|
801
|
-
this.command = args;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
export class SetSessionActivity {
|
|
806
|
-
static readonly commandId = "SetSessionActivity" as const;
|
|
807
|
-
readonly commandId = "SetSessionActivity" as const;
|
|
808
|
-
readonly command: Omit<_SetSessionActivity, "tx" | "createdAt">;
|
|
809
|
-
declare readonly $res: () => void;
|
|
661
|
+
export class GetSessionById {
|
|
662
|
+
static readonly reportId = "GetSessionById" as const;
|
|
663
|
+
readonly reportId = "GetSessionById" as const;
|
|
664
|
+
readonly report: Omit<_GetSessionById, "tx">;
|
|
665
|
+
declare readonly $res: () => Session | null;
|
|
810
666
|
|
|
811
|
-
constructor(args: Omit<
|
|
812
|
-
this.
|
|
667
|
+
constructor(args: Omit<_GetSessionById, "tx">) {
|
|
668
|
+
this.report = args;
|
|
813
669
|
}
|
|
814
670
|
}
|
|
815
671
|
|
|
816
|
-
export class
|
|
817
|
-
static readonly
|
|
818
|
-
readonly
|
|
819
|
-
readonly
|
|
820
|
-
declare readonly $res: () =>
|
|
672
|
+
export class CountSessions {
|
|
673
|
+
static readonly reportId = "CountSessions" as const;
|
|
674
|
+
readonly reportId = "CountSessions" as const;
|
|
675
|
+
readonly report: Omit<_CountSessions, "tx">;
|
|
676
|
+
declare readonly $res: () => SessionCount;
|
|
821
677
|
|
|
822
|
-
constructor(args: Omit<
|
|
823
|
-
this.
|
|
678
|
+
constructor(args: Omit<_CountSessions, "tx">) {
|
|
679
|
+
this.report = args;
|
|
824
680
|
}
|
|
825
681
|
}
|
|
826
682
|
|
|
827
|
-
export class
|
|
828
|
-
static readonly
|
|
829
|
-
readonly
|
|
830
|
-
readonly
|
|
831
|
-
declare readonly $res: () =>
|
|
683
|
+
export class CountAllSessions {
|
|
684
|
+
static readonly reportId = "CountAllSessions" as const;
|
|
685
|
+
readonly reportId = "CountAllSessions" as const;
|
|
686
|
+
readonly report: Omit<_CountAllSessions, "tx">;
|
|
687
|
+
declare readonly $res: () => SessionCount;
|
|
832
688
|
|
|
833
|
-
constructor(args: Omit<
|
|
834
|
-
this.
|
|
689
|
+
constructor(args: Omit<_CountAllSessions, "tx">) {
|
|
690
|
+
this.report = args;
|
|
835
691
|
}
|
|
836
692
|
}
|
|
837
693
|
|
|
838
|
-
export class
|
|
839
|
-
static readonly
|
|
840
|
-
readonly
|
|
841
|
-
readonly
|
|
842
|
-
declare readonly $res: () =>
|
|
694
|
+
export class GetMessageReadById {
|
|
695
|
+
static readonly reportId = "GetMessageReadById" as const;
|
|
696
|
+
readonly reportId = "GetMessageReadById" as const;
|
|
697
|
+
readonly report: Omit<_GetMessageReadById, "tx">;
|
|
698
|
+
declare readonly $res: () => MessageRead | null;
|
|
843
699
|
|
|
844
|
-
constructor(args: Omit<
|
|
845
|
-
this.
|
|
700
|
+
constructor(args: Omit<_GetMessageReadById, "tx">) {
|
|
701
|
+
this.report = args;
|
|
846
702
|
}
|
|
847
703
|
}
|
|
848
704
|
|
|
849
|
-
export class
|
|
850
|
-
static readonly
|
|
851
|
-
readonly
|
|
852
|
-
readonly
|
|
853
|
-
declare readonly $res: () =>
|
|
705
|
+
export class CountMessageReads {
|
|
706
|
+
static readonly reportId = "CountMessageReads" as const;
|
|
707
|
+
readonly reportId = "CountMessageReads" as const;
|
|
708
|
+
readonly report: Omit<_CountMessageReads, "tx">;
|
|
709
|
+
declare readonly $res: () => MessageReadCount;
|
|
854
710
|
|
|
855
|
-
constructor(args: Omit<
|
|
856
|
-
this.
|
|
711
|
+
constructor(args: Omit<_CountMessageReads, "tx">) {
|
|
712
|
+
this.report = args;
|
|
857
713
|
}
|
|
858
714
|
}
|
|
859
715
|
|
|
860
|
-
export class
|
|
861
|
-
static readonly
|
|
862
|
-
readonly
|
|
863
|
-
readonly
|
|
864
|
-
declare readonly $res: () =>
|
|
716
|
+
export class CountAllMessageReads {
|
|
717
|
+
static readonly reportId = "CountAllMessageReads" as const;
|
|
718
|
+
readonly reportId = "CountAllMessageReads" as const;
|
|
719
|
+
readonly report: Omit<_CountAllMessageReads, "tx">;
|
|
720
|
+
declare readonly $res: () => MessageReadCount;
|
|
865
721
|
|
|
866
|
-
constructor(args: Omit<
|
|
867
|
-
this.
|
|
722
|
+
constructor(args: Omit<_CountAllMessageReads, "tx">) {
|
|
723
|
+
this.report = args;
|
|
868
724
|
}
|
|
869
725
|
}
|
|
870
726
|
|
|
727
|
+
// Command classes
|
|
871
728
|
export class DeleteSessionNickname {
|
|
872
729
|
static readonly commandId = "DeleteSessionNickname" as const;
|
|
873
730
|
readonly commandId = "DeleteSessionNickname" as const;
|
|
@@ -945,6 +802,28 @@ export class JoinRoom {
|
|
|
945
802
|
}
|
|
946
803
|
}
|
|
947
804
|
|
|
805
|
+
export class BroadcastMessage {
|
|
806
|
+
static readonly commandId = "BroadcastMessage" as const;
|
|
807
|
+
readonly commandId = "BroadcastMessage" as const;
|
|
808
|
+
readonly command: Omit<_BroadcastMessage, "tx" | "createdAt">;
|
|
809
|
+
declare readonly $res: () => BroadcastMessageResult;
|
|
810
|
+
|
|
811
|
+
constructor(args: Omit<_BroadcastMessage, "tx" | "createdAt">) {
|
|
812
|
+
this.command = args;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export class SendMessage {
|
|
817
|
+
static readonly commandId = "SendMessage" as const;
|
|
818
|
+
readonly commandId = "SendMessage" as const;
|
|
819
|
+
readonly command: Omit<_SendMessage, "tx" | "createdAt">;
|
|
820
|
+
declare readonly $res: () => SendMessageResult;
|
|
821
|
+
|
|
822
|
+
constructor(args: Omit<_SendMessage, "tx" | "createdAt">) {
|
|
823
|
+
this.command = args;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
948
827
|
export class DeleteRoom {
|
|
949
828
|
static readonly commandId = "DeleteRoom" as const;
|
|
950
829
|
readonly commandId = "DeleteRoom" as const;
|
|
@@ -1066,15 +945,136 @@ export class NotifyChannel {
|
|
|
1066
945
|
}
|
|
1067
946
|
}
|
|
1068
947
|
|
|
948
|
+
export class SetSessionLastToolAt {
|
|
949
|
+
static readonly commandId = "SetSessionLastToolAt" as const;
|
|
950
|
+
readonly commandId = "SetSessionLastToolAt" as const;
|
|
951
|
+
readonly command: Omit<_SetSessionLastToolAt, "tx" | "createdAt">;
|
|
952
|
+
declare readonly $res: () => void;
|
|
953
|
+
|
|
954
|
+
constructor(args: Omit<_SetSessionLastToolAt, "tx" | "createdAt">) {
|
|
955
|
+
this.command = args;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export class SetSessionLastTool {
|
|
960
|
+
static readonly commandId = "SetSessionLastTool" as const;
|
|
961
|
+
readonly commandId = "SetSessionLastTool" as const;
|
|
962
|
+
readonly command: Omit<_SetSessionLastTool, "tx" | "createdAt">;
|
|
963
|
+
declare readonly $res: () => void;
|
|
964
|
+
|
|
965
|
+
constructor(args: Omit<_SetSessionLastTool, "tx" | "createdAt">) {
|
|
966
|
+
this.command = args;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export class SetSessionLastActivityAt {
|
|
971
|
+
static readonly commandId = "SetSessionLastActivityAt" as const;
|
|
972
|
+
readonly commandId = "SetSessionLastActivityAt" as const;
|
|
973
|
+
readonly command: Omit<_SetSessionLastActivityAt, "tx" | "createdAt">;
|
|
974
|
+
declare readonly $res: () => void;
|
|
975
|
+
|
|
976
|
+
constructor(args: Omit<_SetSessionLastActivityAt, "tx" | "createdAt">) {
|
|
977
|
+
this.command = args;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export class SetSessionGitBranch {
|
|
982
|
+
static readonly commandId = "SetSessionGitBranch" as const;
|
|
983
|
+
readonly commandId = "SetSessionGitBranch" as const;
|
|
984
|
+
readonly command: Omit<_SetSessionGitBranch, "tx" | "createdAt">;
|
|
985
|
+
declare readonly $res: () => void;
|
|
986
|
+
|
|
987
|
+
constructor(args: Omit<_SetSessionGitBranch, "tx" | "createdAt">) {
|
|
988
|
+
this.command = args;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
export class DeleteSessions {
|
|
993
|
+
static readonly commandId = "DeleteSessions" as const;
|
|
994
|
+
readonly commandId = "DeleteSessions" as const;
|
|
995
|
+
readonly command: Omit<_DeleteSessions, "tx" | "createdAt">;
|
|
996
|
+
declare readonly $res: () => DeleteSessionsResult;
|
|
997
|
+
|
|
998
|
+
constructor(args: Omit<_DeleteSessions, "tx" | "createdAt">) {
|
|
999
|
+
this.command = args;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export class SetSessionActivity {
|
|
1004
|
+
static readonly commandId = "SetSessionActivity" as const;
|
|
1005
|
+
readonly commandId = "SetSessionActivity" as const;
|
|
1006
|
+
readonly command: Omit<_SetSessionActivity, "tx" | "createdAt">;
|
|
1007
|
+
declare readonly $res: () => void;
|
|
1008
|
+
|
|
1009
|
+
constructor(args: Omit<_SetSessionActivity, "tx" | "createdAt">) {
|
|
1010
|
+
this.command = args;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export class SetSessionSessionName {
|
|
1015
|
+
static readonly commandId = "SetSessionSessionName" as const;
|
|
1016
|
+
readonly commandId = "SetSessionSessionName" as const;
|
|
1017
|
+
readonly command: Omit<_SetSessionSessionName, "tx" | "createdAt">;
|
|
1018
|
+
declare readonly $res: () => void;
|
|
1019
|
+
|
|
1020
|
+
constructor(args: Omit<_SetSessionSessionName, "tx" | "createdAt">) {
|
|
1021
|
+
this.command = args;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export class SetSessionCurrentTask {
|
|
1026
|
+
static readonly commandId = "SetSessionCurrentTask" as const;
|
|
1027
|
+
readonly commandId = "SetSessionCurrentTask" as const;
|
|
1028
|
+
readonly command: Omit<_SetSessionCurrentTask, "tx" | "createdAt">;
|
|
1029
|
+
declare readonly $res: () => void;
|
|
1030
|
+
|
|
1031
|
+
constructor(args: Omit<_SetSessionCurrentTask, "tx" | "createdAt">) {
|
|
1032
|
+
this.command = args;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
export class DeleteSession {
|
|
1037
|
+
static readonly commandId = "DeleteSession" as const;
|
|
1038
|
+
readonly commandId = "DeleteSession" as const;
|
|
1039
|
+
readonly command: Omit<_DeleteSession, "tx" | "createdAt">;
|
|
1040
|
+
declare readonly $res: () => DeleteSessionResult;
|
|
1041
|
+
|
|
1042
|
+
constructor(args: Omit<_DeleteSession, "tx" | "createdAt">) {
|
|
1043
|
+
this.command = args;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
export class DeleteMessageRead {
|
|
1048
|
+
static readonly commandId = "DeleteMessageRead" as const;
|
|
1049
|
+
readonly commandId = "DeleteMessageRead" as const;
|
|
1050
|
+
readonly command: Omit<_DeleteMessageRead, "tx" | "createdAt">;
|
|
1051
|
+
declare readonly $res: () => DeleteMessageReadResult;
|
|
1052
|
+
|
|
1053
|
+
constructor(args: Omit<_DeleteMessageRead, "tx" | "createdAt">) {
|
|
1054
|
+
this.command = args;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export class DeleteMessageReads {
|
|
1059
|
+
static readonly commandId = "DeleteMessageReads" as const;
|
|
1060
|
+
readonly commandId = "DeleteMessageReads" as const;
|
|
1061
|
+
readonly command: Omit<_DeleteMessageReads, "tx" | "createdAt">;
|
|
1062
|
+
declare readonly $res: () => DeleteMessageReadsResult;
|
|
1063
|
+
|
|
1064
|
+
constructor(args: Omit<_DeleteMessageReads, "tx" | "createdAt">) {
|
|
1065
|
+
this.command = args;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
1069
|
// Item constructors
|
|
1070
1070
|
export const items = {
|
|
1071
|
-
Session: (args: Session) => ({ item: args, itemType: "Session" }),
|
|
1072
|
-
MessageRead: (args: MessageRead) => ({ item: args, itemType: "MessageRead" }),
|
|
1073
1071
|
SessionNickname: (args: SessionNickname) => ({ item: args, itemType: "SessionNickname" }),
|
|
1074
1072
|
RoomMember: (args: RoomMember) => ({ item: args, itemType: "RoomMember" }),
|
|
1075
1073
|
Room: (args: Room) => ({ item: args, itemType: "Room" }),
|
|
1076
1074
|
SandboxRun: (args: SandboxRun) => ({ item: args, itemType: "SandboxRun" }),
|
|
1077
1075
|
Message: (args: Message) => ({ item: args, itemType: "Message" }),
|
|
1076
|
+
Session: (args: Session) => ({ item: args, itemType: "Session" }),
|
|
1077
|
+
MessageRead: (args: MessageRead) => ({ item: args, itemType: "MessageRead" }),
|
|
1078
1078
|
};
|
|
1079
1079
|
|
|
1080
1080
|
// Message events
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Inbox pulls that are acked only once their text has been shown.
|
|
2
|
+
//
|
|
3
|
+
// Acking in the same step as the read loses messages whenever the caller ends
|
|
4
|
+
// up not showing what it read: a per-turn pull that loses its timeout race, or
|
|
5
|
+
// an injection that fails. A pull here hands the messages back unacked; the
|
|
6
|
+
// caller commits it once the text is in the conversation, or releases it so
|
|
7
|
+
// the next pull hands the same messages out again.
|
|
8
|
+
|
|
9
|
+
/** Unread messages read but not yet acked. */
|
|
10
|
+
export interface InboxPull {
|
|
11
|
+
/** The rendered messages, ready to inject. */
|
|
12
|
+
readonly text: string;
|
|
13
|
+
/** Ack the messages. Call once `text` has been shown. */
|
|
14
|
+
commit(): Promise<void>;
|
|
15
|
+
/** Give the messages back unacked, so the next pull hands them out again. */
|
|
16
|
+
release(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Where a pull reads, acks and renders messages. */
|
|
20
|
+
export interface InboxSource<M extends { messageId: string }> {
|
|
21
|
+
read(sessionId: string): Promise<M[] | null>;
|
|
22
|
+
ack(sessionId: string, messageIds: string[]): Promise<void>;
|
|
23
|
+
render(messages: M[]): string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class InboxPuller<M extends { messageId: string }> {
|
|
27
|
+
/** Per session, the ids an outstanding pull holds: still unread, so a read returns them. */
|
|
28
|
+
private readonly held = new Map<string, Set<string>>();
|
|
29
|
+
/** Per session, the pull in flight. Pulls run one at a time, so each sees what the last one holds. */
|
|
30
|
+
private readonly pulling = new Map<string, Promise<unknown>>();
|
|
31
|
+
|
|
32
|
+
private readonly source: InboxSource<M>;
|
|
33
|
+
|
|
34
|
+
constructor(source: InboxSource<M>) {
|
|
35
|
+
this.source = source;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pull(sessionId: string): Promise<InboxPull | null> {
|
|
39
|
+
const previous = this.pulling.get(sessionId) ?? Promise.resolve();
|
|
40
|
+
const next = previous.catch(() => {}).then(() => this.pullNow(sessionId));
|
|
41
|
+
this.pulling.set(sessionId, next);
|
|
42
|
+
void next.catch(() => {}).finally(() => {
|
|
43
|
+
if (this.pulling.get(sessionId) === next) this.pulling.delete(sessionId);
|
|
44
|
+
});
|
|
45
|
+
return next;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async pullNow(sessionId: string): Promise<InboxPull | null> {
|
|
49
|
+
const held = this.held.get(sessionId) ?? new Set<string>();
|
|
50
|
+
const messages = (await this.source.read(sessionId))?.filter((m) => !held.has(m.messageId));
|
|
51
|
+
if (!messages?.length) return null;
|
|
52
|
+
// Render before holding anything, so a render that throws holds nothing.
|
|
53
|
+
const text = this.source.render(messages);
|
|
54
|
+
const ids = messages.map((m) => m.messageId);
|
|
55
|
+
for (const id of ids) held.add(id);
|
|
56
|
+
this.held.set(sessionId, held);
|
|
57
|
+
const settle = () => { for (const id of ids) held.delete(id); };
|
|
58
|
+
return {
|
|
59
|
+
text,
|
|
60
|
+
commit: async () => {
|
|
61
|
+
try {
|
|
62
|
+
await this.source.ack(sessionId, ids);
|
|
63
|
+
} finally {
|
|
64
|
+
settle();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
release: settle,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Pull for a turn, giving up after `timeoutMs` so the turn is never held up.
|
|
74
|
+
* A pull that lands after that is released, not acked: its messages were not
|
|
75
|
+
* shown, so they stay unread for the next turn.
|
|
76
|
+
*/
|
|
77
|
+
export async function pullForTurn(pull: () => Promise<InboxPull | null>, timeoutMs: number): Promise<InboxPull | null> {
|
|
78
|
+
const pending = pull().catch(() => null);
|
|
79
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
80
|
+
const deadline = new Promise<"late">((resolve) => { timer = setTimeout(() => resolve("late"), timeoutMs); });
|
|
81
|
+
const winner = await Promise.race([pending, deadline]);
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
if (winner !== "late") return winner;
|
|
84
|
+
void pending.then((landed) => landed?.release());
|
|
85
|
+
return null;
|
|
86
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
23
23
|
import { Type } from "typebox";
|
|
24
24
|
|
|
25
25
|
import { MarshalDaemon } from "./daemon.ts";
|
|
26
|
+
import { pullForTurn } from "./inbox-pull.ts";
|
|
26
27
|
import { resolveIdentity, type Identity } from "./identity.ts";
|
|
27
28
|
import type { NotifyChannelMeta, SessionItem } from "./entities.ts";
|
|
28
29
|
import { shouldRegisterPrimeAgentSession } from "./runtime.ts";
|
|
@@ -111,12 +112,19 @@ async function init(pi: ExtensionAPI) {
|
|
|
111
112
|
);
|
|
112
113
|
} else {
|
|
113
114
|
// Compatibility fallback for older daemons that omit body metadata.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
// The pull is acked only once it has been injected.
|
|
116
|
+
const pull = await daemon!.pullInbox(sid);
|
|
117
|
+
if (!pull) return;
|
|
118
|
+
try {
|
|
119
|
+
await pi.sendMessage(
|
|
120
|
+
{ customType: "marshal-channel", content: pull.text, display: true, details: meta },
|
|
121
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
122
|
+
);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
pull.release();
|
|
125
|
+
throw e;
|
|
126
|
+
}
|
|
127
|
+
await pull.commit().catch(() => log("inbox ack failed"));
|
|
120
128
|
}
|
|
121
129
|
} catch (e) {
|
|
122
130
|
log(`live push injection failed: ${String(e)}`);
|
|
@@ -328,19 +336,21 @@ async function init(pi: ExtensionAPI) {
|
|
|
328
336
|
// Add marshal system prompt context (once per turn, before the LLM call).
|
|
329
337
|
result.systemPrompt = event.systemPrompt + "\n\n" + SYSTEM_PROMPT_BLOCK;
|
|
330
338
|
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
+
// Pull any missed inbox messages — only if the daemon is connected. The
|
|
340
|
+
// pull is acked only once its text goes into this turn; one that misses
|
|
341
|
+
// the deadline is released, so its messages stay unread for the next turn.
|
|
342
|
+
const d = daemon;
|
|
343
|
+
const sid = sessionId;
|
|
344
|
+
if (d && sid && d.isConnected()) {
|
|
345
|
+
d.registerSession(sid); // self-heal a dropped roster row
|
|
346
|
+
const pull = await pullForTurn(() => d.pullInbox(sid), 2000);
|
|
347
|
+
if (pull) {
|
|
339
348
|
result.message = {
|
|
340
349
|
customType: "marshal-inbox",
|
|
341
|
-
content:
|
|
350
|
+
content: pull.text,
|
|
342
351
|
display: true,
|
|
343
352
|
};
|
|
353
|
+
void pull.commit().catch(() => log("inbox ack failed"));
|
|
344
354
|
}
|
|
345
355
|
}
|
|
346
356
|
|