ftown 0.25.2 → 0.26.0
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/dist/command-rpc.d.ts +3 -0
- package/dist/command-rpc.js +41 -0
- package/dist/command-rpc.js.map +1 -1
- package/dist/coordination/board.d.ts +12 -0
- package/dist/coordination/board.js +240 -0
- package/dist/coordination/board.js.map +1 -0
- package/dist/coordination/card-store.d.ts +9 -0
- package/dist/coordination/card-store.js +188 -0
- package/dist/coordination/card-store.js.map +1 -0
- package/dist/coordination/contract.d.ts +236 -0
- package/dist/coordination/contract.js +63 -0
- package/dist/coordination/contract.js.map +1 -0
- package/dist/coordination/format.d.ts +5 -0
- package/dist/coordination/format.js +60 -0
- package/dist/coordination/format.js.map +1 -0
- package/dist/coordination/tree-events.d.ts +31 -0
- package/dist/coordination/tree-events.js +226 -0
- package/dist/coordination/tree-events.js.map +1 -0
- package/dist/coordination/wiring.d.ts +48 -0
- package/dist/coordination/wiring.js +143 -0
- package/dist/coordination/wiring.js.map +1 -0
- package/dist/coordination-cli.d.ts +34 -0
- package/dist/coordination-cli.js +460 -0
- package/dist/coordination-cli.js.map +1 -0
- package/dist/ftown-cli.js +12 -2
- package/dist/ftown-cli.js.map +1 -1
- package/dist/index.js +33 -7
- package/dist/index.js.map +1 -1
- package/dist/local-api-server.d.ts +6 -1
- package/dist/local-api-server.js +282 -5
- package/dist/local-api-server.js.map +1 -1
- package/dist/mail-delivery.d.ts +16 -2
- package/dist/mail-delivery.js +331 -77
- package/dist/mail-delivery.js.map +1 -1
- package/dist/mail-store.d.ts +7 -0
- package/dist/mail-store.js +71 -1
- package/dist/mail-store.js.map +1 -1
- package/dist/pull-request-events.d.ts +5 -0
- package/dist/pull-request-events.js +21 -1
- package/dist/pull-request-events.js.map +1 -1
- package/dist/quota/agy.d.ts +16 -0
- package/dist/quota/agy.js +287 -0
- package/dist/quota/agy.js.map +1 -0
- package/dist/quota/annotate.d.ts +5 -0
- package/dist/quota/annotate.js +56 -0
- package/dist/quota/annotate.js.map +1 -0
- package/dist/quota/claude.d.ts +27 -0
- package/dist/quota/claude.js +265 -0
- package/dist/quota/claude.js.map +1 -0
- package/dist/quota/codex.d.ts +25 -0
- package/dist/quota/codex.js +378 -0
- package/dist/quota/codex.js.map +1 -0
- package/dist/quota/format.d.ts +6 -0
- package/dist/quota/format.js +113 -0
- package/dist/quota/format.js.map +1 -0
- package/dist/quota/grok.d.ts +97 -0
- package/dist/quota/grok.js +563 -0
- package/dist/quota/grok.js.map +1 -0
- package/dist/quota/models-agy.d.ts +10 -0
- package/dist/quota/models-agy.js +144 -0
- package/dist/quota/models-agy.js.map +1 -0
- package/dist/quota/models-claude.d.ts +10 -0
- package/dist/quota/models-claude.js +227 -0
- package/dist/quota/models-claude.js.map +1 -0
- package/dist/quota/models-codex.d.ts +18 -0
- package/dist/quota/models-codex.js +305 -0
- package/dist/quota/models-codex.js.map +1 -0
- package/dist/quota/models-grok.d.ts +19 -0
- package/dist/quota/models-grok.js +272 -0
- package/dist/quota/models-grok.js.map +1 -0
- package/dist/quota/models-muse.d.ts +13 -0
- package/dist/quota/models-muse.js +200 -0
- package/dist/quota/models-muse.js.map +1 -0
- package/dist/quota/models-opencode.d.ts +51 -0
- package/dist/quota/models-opencode.js +400 -0
- package/dist/quota/models-opencode.js.map +1 -0
- package/dist/quota/muse.d.ts +38 -0
- package/dist/quota/muse.js +225 -0
- package/dist/quota/muse.js.map +1 -0
- package/dist/quota/opencode.d.ts +7 -0
- package/dist/quota/opencode.js +47 -0
- package/dist/quota/opencode.js.map +1 -0
- package/dist/quota/routing-config.d.ts +18 -0
- package/dist/quota/routing-config.js +438 -0
- package/dist/quota/routing-config.js.map +1 -0
- package/dist/quota/routing-service.d.ts +24 -0
- package/dist/quota/routing-service.js +118 -0
- package/dist/quota/routing-service.js.map +1 -0
- package/dist/quota/routing.d.ts +10 -0
- package/dist/quota/routing.js +180 -0
- package/dist/quota/routing.js.map +1 -0
- package/dist/quota/types.d.ts +304 -0
- package/dist/quota/types.js +45 -0
- package/dist/quota/types.js.map +1 -0
- package/dist/quota-cli.d.ts +80 -0
- package/dist/quota-cli.js +462 -0
- package/dist/quota-cli.js.map +1 -0
- package/dist/security/bridge/permissions.js +24 -2
- package/dist/security/bridge/permissions.js.map +1 -1
- package/dist/types.d.ts +7 -3
- package/dist/wire-types.d.ts +17 -1
- package/dist/wire-types.js +1 -1
- package/dist/wire-types.js.map +1 -1
- package/package.json +3 -2
- package/skills/ftown/SKILL.md +31 -5
- package/skills/ftown/references/orchestrator.md +17 -7
- package/skills/ftown/references/quota.md +177 -0
- package/skills/ftown/references/sessions.md +45 -3
package/dist/mail-delivery.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { createMailMessage } from './mail-store.js';
|
|
1
|
+
import { createMailMessage, createNudgeToken } from './mail-store.js';
|
|
2
2
|
import { HOOKED_SHELL_TYPES } from './harness-registry.js';
|
|
3
3
|
import { createNativeMailWaker } from './native-mail.js';
|
|
4
|
+
import { DIGEST_MAX, MAIL_TYPES_V2, MAX_WAKE_ATTEMPTS, NON_WAKING_MAIL_TYPES, } from './coordination/contract.js';
|
|
5
|
+
export { MAIL_TYPES_V2, MAX_WAKE_ATTEMPTS, NON_WAKING_MAIL_TYPES };
|
|
4
6
|
export const MAX_MAIL_BODY_LENGTH = 64 * 1024;
|
|
5
7
|
export const MAX_MAIL_WAIT_SECONDS = 30;
|
|
6
8
|
export const MAIL_NUDGE_DELAY_MS = 5_000;
|
|
@@ -40,7 +42,32 @@ export function parseNativeWakeRegistration(query) {
|
|
|
40
42
|
}
|
|
41
43
|
return { adapter, nativeSessionId };
|
|
42
44
|
}
|
|
43
|
-
export const MAIL_TYPES =
|
|
45
|
+
export const MAIL_TYPES = MAIL_TYPES_V2;
|
|
46
|
+
const NUDGE_COUNT_ORDER = ['task', 'escalation', 'result', 'message'];
|
|
47
|
+
export function countMailTypes(messages) {
|
|
48
|
+
const counts = {};
|
|
49
|
+
for (const msg of messages) {
|
|
50
|
+
if (NON_WAKING_MAIL_TYPES.has(msg.type))
|
|
51
|
+
continue;
|
|
52
|
+
counts[msg.type] = (counts[msg.type] ?? 0) + 1;
|
|
53
|
+
}
|
|
54
|
+
return counts;
|
|
55
|
+
}
|
|
56
|
+
export function formatNudgeText(counts, digest) {
|
|
57
|
+
const parts = [];
|
|
58
|
+
let total = 0;
|
|
59
|
+
for (const type of NUDGE_COUNT_ORDER) {
|
|
60
|
+
const count = counts[type] ?? 0;
|
|
61
|
+
if (count > 0) {
|
|
62
|
+
total += count;
|
|
63
|
+
parts.push(`${count} ${type}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const base = `[ftown] ${total} new mail (${parts.join(', ')}) — run: ftown harness mail read`;
|
|
67
|
+
const trimmedDigest = typeof digest === 'string' ? sanitizeMessageText(digest).slice(0, DIGEST_MAX) : '';
|
|
68
|
+
return trimmedDigest ? `${base} | ${trimmedDigest}` : base;
|
|
69
|
+
}
|
|
70
|
+
export const NUDGE_TEXT = formatNudgeText;
|
|
44
71
|
// Composer TUIs detect pastes by input arrival rate; the submit CR must come well
|
|
45
72
|
// after that window or it is treated as a pasted newline.
|
|
46
73
|
const COMPOSER_PASTE_SETTLE_MS = 600;
|
|
@@ -85,14 +112,17 @@ export class MailDeliveryService {
|
|
|
85
112
|
lastNudgeAt = new Map();
|
|
86
113
|
agentBusySince = new Map();
|
|
87
114
|
nativeLeases = new Map();
|
|
88
|
-
nativeBatches = new Map();
|
|
89
115
|
selectionLocks = new Map();
|
|
90
116
|
codexClaims = new Map();
|
|
91
117
|
codexRetryAt = new Map();
|
|
118
|
+
batchAttempts = new Map();
|
|
119
|
+
museNativeTried = new Map();
|
|
92
120
|
wakeLocks = new Map();
|
|
93
121
|
wakeControllers = new Map();
|
|
94
122
|
legacyReadyAt = new Map();
|
|
95
123
|
lastInjection = new Map();
|
|
124
|
+
digestProvider = null;
|
|
125
|
+
eventSink = null;
|
|
96
126
|
stopped = false;
|
|
97
127
|
dependencyEpoch = 0;
|
|
98
128
|
nativeWake;
|
|
@@ -120,11 +150,19 @@ export class MailDeliveryService {
|
|
|
120
150
|
this.invalidateRecovery();
|
|
121
151
|
this.runner = runner;
|
|
122
152
|
}
|
|
153
|
+
setDigestProvider(p) {
|
|
154
|
+
this.digestProvider = p;
|
|
155
|
+
}
|
|
156
|
+
setEventSink(s) {
|
|
157
|
+
this.eventSink = s;
|
|
158
|
+
}
|
|
123
159
|
invalidateRecovery() {
|
|
124
160
|
this.dependencyEpoch++;
|
|
125
161
|
for (const timer of this.nudgeTimers.values())
|
|
126
162
|
this.clearTimeoutFn(timer);
|
|
127
163
|
this.nudgeTimers.clear();
|
|
164
|
+
this.batchAttempts.clear();
|
|
165
|
+
this.museNativeTried.clear();
|
|
128
166
|
for (const controller of this.wakeControllers.values())
|
|
129
167
|
controller.abort();
|
|
130
168
|
}
|
|
@@ -140,7 +178,7 @@ export class MailDeliveryService {
|
|
|
140
178
|
if (!current() || !session || session.status !== 'running' || session.shellType === 'pi'
|
|
141
179
|
|| !runner.isRunning(sessionId))
|
|
142
180
|
return;
|
|
143
|
-
const pending = await mailStore.
|
|
181
|
+
const pending = await mailStore.listUnnudgedWaking(sessionId);
|
|
144
182
|
if (!current() || !pending.length || !runner.isRunning(sessionId))
|
|
145
183
|
return;
|
|
146
184
|
// Reads may have raced deletion/stop or an inbox drain. The wake path checks
|
|
@@ -167,9 +205,10 @@ export class MailDeliveryService {
|
|
|
167
205
|
waiter.deliver([]);
|
|
168
206
|
this.mailWaiters.clear();
|
|
169
207
|
this.nativeLeases.clear();
|
|
170
|
-
this.nativeBatches.clear();
|
|
171
208
|
this.legacyReadyAt.clear();
|
|
172
209
|
this.codexRetryAt.clear();
|
|
210
|
+
this.batchAttempts.clear();
|
|
211
|
+
this.museNativeTried.clear();
|
|
173
212
|
this.lastInjection.clear();
|
|
174
213
|
for (const controller of this.wakeControllers.values())
|
|
175
214
|
controller.abort();
|
|
@@ -218,7 +257,7 @@ export class MailDeliveryService {
|
|
|
218
257
|
}
|
|
219
258
|
available(sessionId, messages) {
|
|
220
259
|
const claimed = this.codexClaims.get(sessionId);
|
|
221
|
-
return claimed ? messages.filter(message => message.id
|
|
260
|
+
return claimed?.size ? messages.filter(message => !claimed.has(message.id)) : messages;
|
|
222
261
|
}
|
|
223
262
|
/** Hook activity (UserPromptSubmit/PreToolUse/PostToolUse): the agent is mid-turn. */
|
|
224
263
|
markAgentBusy(sessionId) {
|
|
@@ -240,8 +279,51 @@ export class MailDeliveryService {
|
|
|
240
279
|
this.lastInjection.set(sessionId, { text, at: this.now() });
|
|
241
280
|
}
|
|
242
281
|
/** Let the API server record an opencode/claude-mod plugin handoff (mail handed to the plugin). */
|
|
243
|
-
notePluginHandoff(sessionId) {
|
|
282
|
+
async notePluginHandoff(sessionId) {
|
|
244
283
|
this.recordInjection(sessionId, OPENCODE_MAIL_WAKE_TEXT);
|
|
284
|
+
const existingTimer = this.nudgeTimers.get(sessionId);
|
|
285
|
+
if (existingTimer) {
|
|
286
|
+
this.clearTimeoutFn(existingTimer);
|
|
287
|
+
this.nudgeTimers.delete(sessionId);
|
|
288
|
+
}
|
|
289
|
+
this.pendingNudgeFrom.delete(sessionId);
|
|
290
|
+
const mailStore = this.mailStore;
|
|
291
|
+
if (!mailStore)
|
|
292
|
+
return;
|
|
293
|
+
const token = createNudgeToken(this.now());
|
|
294
|
+
try {
|
|
295
|
+
const snapshot = await mailStore.listUnnudgedWaking(sessionId);
|
|
296
|
+
const undeliveredWaking = this.wakeLocks.has(sessionId)
|
|
297
|
+
? (await mailStore.listUndelivered(sessionId)).filter((m) => !NON_WAKING_MAIL_TYPES.has(m.type))
|
|
298
|
+
: snapshot;
|
|
299
|
+
const batchIds = Array.from(new Set([...snapshot.map((m) => m.id), ...undeliveredWaking.map((m) => m.id)]));
|
|
300
|
+
if (batchIds.length === 0)
|
|
301
|
+
return;
|
|
302
|
+
await this.withSelection(sessionId, async () => {
|
|
303
|
+
await mailStore.markNudged(sessionId, batchIds, token);
|
|
304
|
+
});
|
|
305
|
+
const inFlightWake = this.wakeLocks.get(sessionId);
|
|
306
|
+
if (inFlightWake) {
|
|
307
|
+
void inFlightWake
|
|
308
|
+
.then(async () => {
|
|
309
|
+
const retryTimer = this.nudgeTimers.get(sessionId);
|
|
310
|
+
if (retryTimer) {
|
|
311
|
+
this.clearTimeoutFn(retryTimer);
|
|
312
|
+
this.nudgeTimers.delete(sessionId);
|
|
313
|
+
}
|
|
314
|
+
this.pendingNudgeFrom.delete(sessionId);
|
|
315
|
+
await this.withSelection(sessionId, async () => {
|
|
316
|
+
await mailStore.markNudged(sessionId, batchIds, token);
|
|
317
|
+
});
|
|
318
|
+
})
|
|
319
|
+
.catch((err) => {
|
|
320
|
+
console.error('[MailDelivery] Plugin handoff stamp failed:', err instanceof Error ? err.message : String(err));
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
console.error('[MailDelivery] Plugin handoff stamp failed:', err instanceof Error ? err.message : String(err));
|
|
326
|
+
}
|
|
245
327
|
}
|
|
246
328
|
/**
|
|
247
329
|
* Whether a UserPromptSubmit hook payload was submitted by ftown itself (bridge or installed plugin).
|
|
@@ -288,6 +370,7 @@ export class MailDeliveryService {
|
|
|
288
370
|
this.recordInjection(session.id, line);
|
|
289
371
|
if (!this.runner.write(session.id, line))
|
|
290
372
|
return false;
|
|
373
|
+
options?.onFirstWrite?.();
|
|
291
374
|
// Composer TUIs detect pastes by input arrival rate; the submit CR must come well
|
|
292
375
|
// after that window or it is treated as a pasted newline.
|
|
293
376
|
await this.delay(COMPOSER_PASTE_SETTLE_MS);
|
|
@@ -333,6 +416,17 @@ export class MailDeliveryService {
|
|
|
333
416
|
const threadId = typeof body.threadId === 'string' && body.threadId.trim() ? body.threadId.trim() : undefined;
|
|
334
417
|
const msg = createMailMessage({ from, fromName, to: session.id, type, threadId, body: text });
|
|
335
418
|
await this.mailStore.append(msg);
|
|
419
|
+
if (this.eventSink) {
|
|
420
|
+
const sink = this.eventSink;
|
|
421
|
+
try {
|
|
422
|
+
void Promise.resolve(sink.onAccepted(msg, session)).catch((err) => {
|
|
423
|
+
console.error('[MailDelivery] Event sink failed:', err instanceof Error ? err.message : String(err));
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
console.error('[MailDelivery] Event sink failed:', err instanceof Error ? err.message : String(err));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
336
430
|
await this.notifyCommittedMail(session, fromName ?? from);
|
|
337
431
|
return { ok: true, id: msg.id };
|
|
338
432
|
}
|
|
@@ -358,7 +452,7 @@ export class MailDeliveryService {
|
|
|
358
452
|
waiter.deliver(marked);
|
|
359
453
|
}
|
|
360
454
|
}
|
|
361
|
-
pending = (await mailStore.
|
|
455
|
+
pending = (await mailStore.listUnnudgedWaking(session.id)).length > 0;
|
|
362
456
|
});
|
|
363
457
|
if (pending && (!hadWaiter || session.shellType === 'codex') && session.shellType !== 'pi') {
|
|
364
458
|
const legacyDelay = HOOKED_SHELL_TYPES.has(session.shellType ?? 'claude')
|
|
@@ -505,24 +599,57 @@ export class MailDeliveryService {
|
|
|
505
599
|
return;
|
|
506
600
|
}
|
|
507
601
|
if (!runner || !runner.isRunning(sessionId) || session.status !== 'running') {
|
|
508
|
-
this.
|
|
509
|
-
|
|
602
|
+
const pending = await this.withSelection(sessionId, async () => current() ? this.available(sessionId, await mailStore.listUnnudgedWaking(sessionId)) : []);
|
|
603
|
+
if (!current() || !pending.length) {
|
|
604
|
+
this.pendingNudgeFrom.delete(sessionId);
|
|
605
|
+
this.codexRetryAt.delete(sessionId);
|
|
606
|
+
this.batchAttempts.delete(sessionId);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const batchIds = pending.map((m) => m.id);
|
|
610
|
+
const batchKey = batchIds.slice().sort().join(',');
|
|
611
|
+
const prev = this.batchAttempts.get(sessionId);
|
|
612
|
+
const attempts = (prev?.key === batchKey ? prev.count : 0) + 1;
|
|
613
|
+
if (attempts >= MAX_WAKE_ATTEMPTS) {
|
|
614
|
+
const token = createNudgeToken(this.now());
|
|
615
|
+
await this.withSelection(sessionId, async () => {
|
|
616
|
+
if (!current())
|
|
617
|
+
return;
|
|
618
|
+
await mailStore.markNudged(sessionId, batchIds, token);
|
|
619
|
+
});
|
|
620
|
+
this.batchAttempts.delete(sessionId);
|
|
621
|
+
this.codexRetryAt.delete(sessionId);
|
|
622
|
+
this.pendingNudgeFrom.delete(sessionId);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
this.batchAttempts.set(sessionId, { key: batchKey, count: attempts });
|
|
626
|
+
this.codexRetryAt.set(sessionId, this.now() + MAIL_NUDGE_BUSY_RECHECK_MS);
|
|
627
|
+
this.scheduleMailNudge(sessionId, fromLabel, MAIL_NUDGE_BUSY_RECHECK_MS);
|
|
628
|
+
}
|
|
510
629
|
return;
|
|
511
630
|
}
|
|
512
|
-
|
|
631
|
+
let claimSet;
|
|
632
|
+
const batch = await this.withSelection(sessionId, async () => {
|
|
513
633
|
if (!current())
|
|
514
|
-
return
|
|
515
|
-
const pending = this.available(sessionId, await mailStore.
|
|
634
|
+
return [];
|
|
635
|
+
const pending = this.available(sessionId, await mailStore.listUnnudgedWaking(sessionId));
|
|
516
636
|
if (!current() || !pending.length)
|
|
517
|
-
return
|
|
518
|
-
|
|
519
|
-
|
|
637
|
+
return [];
|
|
638
|
+
claimSet = new Set(pending.map(m => m.id));
|
|
639
|
+
this.codexClaims.set(sessionId, claimSet);
|
|
640
|
+
return pending;
|
|
520
641
|
});
|
|
521
|
-
if (!
|
|
642
|
+
if (!batch.length || !claimSet) {
|
|
643
|
+
this.pendingNudgeFrom.delete(sessionId);
|
|
644
|
+
this.codexRetryAt.delete(sessionId);
|
|
645
|
+
this.batchAttempts.delete(sessionId);
|
|
522
646
|
return;
|
|
647
|
+
}
|
|
648
|
+
const batchIds = batch.map(m => m.id);
|
|
649
|
+
const batchKey = batchIds.slice().sort().join(',');
|
|
523
650
|
if (!current()) {
|
|
524
651
|
await this.withSelection(sessionId, async () => {
|
|
525
|
-
if (this.codexClaims.get(sessionId) ===
|
|
652
|
+
if (this.codexClaims.get(sessionId) === claimSet)
|
|
526
653
|
this.codexClaims.delete(sessionId);
|
|
527
654
|
});
|
|
528
655
|
return;
|
|
@@ -536,22 +663,25 @@ export class MailDeliveryService {
|
|
|
536
663
|
if (!current() || controller.signal.aborted)
|
|
537
664
|
return false;
|
|
538
665
|
const live = await store.loadSession(sessionId);
|
|
539
|
-
const pending = await mailStore.
|
|
666
|
+
const pending = await mailStore.listUnnudgedWaking(sessionId);
|
|
667
|
+
const pendingIds = new Set(pending.map(m => m.id));
|
|
540
668
|
return current() && !controller.signal.aborted && !!live && live.status === 'running'
|
|
541
669
|
&& live.shellType === 'codex' && nativeIdentity(live) === identity && runner.isRunning(sessionId)
|
|
542
|
-
&& this.codexClaims.get(sessionId) ===
|
|
670
|
+
&& this.codexClaims.get(sessionId) === claimSet && batchIds.some(id => pendingIds.has(id));
|
|
543
671
|
};
|
|
544
|
-
const wakeText = codexMailText(
|
|
672
|
+
const wakeText = batch.map(codexMailText).join('\n\n');
|
|
545
673
|
// Recorded BEFORE the wake call: the prompt hook can fire before it resolves.
|
|
546
674
|
this.recordInjection(sessionId, wakeText);
|
|
547
675
|
const result = await this.nativeWake(session, wakeText, controller.signal, isCurrent)
|
|
548
676
|
.catch(() => ({ status: 'unknown' }));
|
|
549
677
|
if (result.status === 'accepted' && current() && !controller.signal.aborted) {
|
|
678
|
+
const token = createNudgeToken(this.now());
|
|
550
679
|
accepted = await this.withSelection(sessionId, async () => {
|
|
551
|
-
if (!current() || controller.signal.aborted || this.codexClaims.get(sessionId) !==
|
|
680
|
+
if (!current() || controller.signal.aborted || this.codexClaims.get(sessionId) !== claimSet)
|
|
552
681
|
return false;
|
|
553
|
-
const marked = await mailStore.markDelivered(sessionId,
|
|
554
|
-
|
|
682
|
+
const marked = await mailStore.markDelivered(sessionId, batchIds, 'cli');
|
|
683
|
+
await mailStore.markNudged(sessionId, batchIds, token);
|
|
684
|
+
return marked.length > 0;
|
|
555
685
|
});
|
|
556
686
|
}
|
|
557
687
|
}
|
|
@@ -562,24 +692,44 @@ export class MailDeliveryService {
|
|
|
562
692
|
if (this.wakeControllers.get(sessionId) === controller)
|
|
563
693
|
this.wakeControllers.delete(sessionId);
|
|
564
694
|
await this.withSelection(sessionId, async () => {
|
|
565
|
-
if (this.codexClaims.get(sessionId) ===
|
|
695
|
+
if (this.codexClaims.get(sessionId) === claimSet)
|
|
566
696
|
this.codexClaims.delete(sessionId);
|
|
567
697
|
});
|
|
568
698
|
}
|
|
569
699
|
if (!current())
|
|
570
700
|
return;
|
|
571
|
-
if (
|
|
572
|
-
this.codexRetryAt.set(sessionId, this.now() + MAIL_NUDGE_BUSY_RECHECK_MS);
|
|
573
|
-
else
|
|
701
|
+
if (accepted) {
|
|
574
702
|
this.codexRetryAt.delete(sessionId);
|
|
575
|
-
|
|
703
|
+
this.batchAttempts.delete(sessionId);
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
const prev = this.batchAttempts.get(sessionId);
|
|
707
|
+
const attempts = (prev?.key === batchKey ? prev.count : 0) + 1;
|
|
708
|
+
if (attempts >= MAX_WAKE_ATTEMPTS) {
|
|
709
|
+
const token = createNudgeToken(this.now());
|
|
710
|
+
await this.withSelection(sessionId, async () => {
|
|
711
|
+
if (!current())
|
|
712
|
+
return;
|
|
713
|
+
await mailStore.markNudged(sessionId, batchIds, token);
|
|
714
|
+
});
|
|
715
|
+
this.batchAttempts.delete(sessionId);
|
|
716
|
+
this.codexRetryAt.delete(sessionId);
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
this.batchAttempts.set(sessionId, { key: batchKey, count: attempts });
|
|
720
|
+
this.codexRetryAt.set(sessionId, this.now() + MAIL_NUDGE_BUSY_RECHECK_MS);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const remaining = await mailStore.listUnnudgedWaking(sessionId);
|
|
576
724
|
if (!current())
|
|
577
725
|
return;
|
|
578
|
-
if (
|
|
726
|
+
if (remaining.length) {
|
|
579
727
|
this.scheduleMailNudge(sessionId, fromLabel, accepted ? MAIL_NUDGE_DELAY_MS : MAIL_NUDGE_BUSY_RECHECK_MS);
|
|
728
|
+
}
|
|
580
729
|
else {
|
|
581
730
|
this.pendingNudgeFrom.delete(sessionId);
|
|
582
731
|
this.codexRetryAt.delete(sessionId);
|
|
732
|
+
this.batchAttempts.delete(sessionId);
|
|
583
733
|
}
|
|
584
734
|
}
|
|
585
735
|
async wakeSession(sessionId) {
|
|
@@ -588,13 +738,13 @@ export class MailDeliveryService {
|
|
|
588
738
|
if (!current() || !store || !mailStore)
|
|
589
739
|
return;
|
|
590
740
|
const fromLabel = this.pendingNudgeFrom.get(sessionId) ?? 'ftown';
|
|
591
|
-
const undelivered = await mailStore.
|
|
741
|
+
const undelivered = await mailStore.listUnnudgedWaking(sessionId);
|
|
592
742
|
if (!current())
|
|
593
743
|
return;
|
|
594
744
|
if (undelivered.length === 0) {
|
|
595
745
|
this.pendingNudgeFrom.delete(sessionId);
|
|
596
|
-
this.nativeBatches.delete(sessionId);
|
|
597
746
|
this.legacyReadyAt.delete(sessionId);
|
|
747
|
+
this.batchAttempts.delete(sessionId);
|
|
598
748
|
return;
|
|
599
749
|
}
|
|
600
750
|
let session = await store.loadSession(sessionId);
|
|
@@ -617,55 +767,93 @@ export class MailDeliveryService {
|
|
|
617
767
|
this.scheduleMailNudge(sessionId, fromLabel, MAIL_NUDGE_BUSY_RECHECK_MS);
|
|
618
768
|
return;
|
|
619
769
|
}
|
|
770
|
+
let museStamped;
|
|
620
771
|
if (session.shellType === 'muse') {
|
|
621
|
-
let identity = nativeIdentity(session);
|
|
622
|
-
const prior = this.nativeBatches.get(sessionId);
|
|
623
|
-
if (prior?.identity === identity && undelivered.every((message) => prior.ids.has(message.id)))
|
|
624
|
-
return;
|
|
625
772
|
// A hook may have drained mail during the session lookup. Never wake for a stale snapshot.
|
|
626
773
|
session = await store.loadSession(sessionId);
|
|
627
|
-
const pending = await mailStore.
|
|
774
|
+
const pending = await mailStore.listUnnudgedWaking(sessionId);
|
|
628
775
|
if (!current() || !pending.length || !session || session.status !== 'running' || !runner.isRunning(sessionId))
|
|
629
776
|
return;
|
|
630
777
|
if (session.shellType !== 'muse') {
|
|
631
778
|
this.scheduleMailNudge(sessionId, fromLabel, MAIL_NUDGE_DELAY_MS);
|
|
632
779
|
return;
|
|
633
780
|
}
|
|
634
|
-
|
|
635
|
-
const
|
|
636
|
-
this.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
781
|
+
const batchIds = pending.map((message) => message.id);
|
|
782
|
+
const batchKey = batchIds.slice().sort().join(',');
|
|
783
|
+
if (this.museNativeTried.get(sessionId) !== batchKey) {
|
|
784
|
+
const museToken = createNudgeToken(this.now());
|
|
785
|
+
const stamped = await mailStore.markNudged(sessionId, batchIds, museToken);
|
|
786
|
+
if (!stamped.length) {
|
|
787
|
+
this.pendingNudgeFrom.delete(sessionId);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (!current()) {
|
|
791
|
+
await mailStore.unmarkNudged(sessionId, batchIds, museToken);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
this.museNativeTried.set(sessionId, batchKey);
|
|
795
|
+
const prev = this.batchAttempts.get(sessionId);
|
|
796
|
+
const museAttempts = (prev?.key === batchKey ? prev.count : 0) + 1;
|
|
797
|
+
this.batchAttempts.set(sessionId, { key: batchKey, count: museAttempts });
|
|
798
|
+
const identity = nativeIdentity(session);
|
|
799
|
+
const controller = new AbortController();
|
|
800
|
+
this.wakeControllers.set(sessionId, controller);
|
|
801
|
+
const isCurrent = async () => {
|
|
802
|
+
if (!current() || controller.signal.aborted)
|
|
803
|
+
return false;
|
|
804
|
+
const live = await store.loadSession(sessionId);
|
|
805
|
+
const currentPending = await mailStore.listUndelivered(sessionId);
|
|
806
|
+
return current() && !controller.signal.aborted && !!live && live.status === 'running'
|
|
807
|
+
&& nativeIdentity(live) === identity && !this.isAgentRecentlyBusy(sessionId)
|
|
808
|
+
&& runner.isRunning(sessionId) && currentPending.some(message => batchIds.includes(message.id));
|
|
809
|
+
};
|
|
810
|
+
let result;
|
|
811
|
+
// Recorded BEFORE the wake call: the prompt hook can fire before it resolves.
|
|
812
|
+
this.recordInjection(sessionId, MAIL_WAKE_TEXT);
|
|
813
|
+
try {
|
|
814
|
+
result = await this.nativeWake(session, MAIL_WAKE_TEXT, controller.signal, isCurrent).catch(() => ({ status: 'unknown' }));
|
|
815
|
+
}
|
|
816
|
+
finally {
|
|
817
|
+
if (this.wakeControllers.get(sessionId) === controller)
|
|
818
|
+
this.wakeControllers.delete(sessionId);
|
|
819
|
+
}
|
|
820
|
+
// Never unstamp after an accepted/unknown native wake, regardless of later busy/isCurrent checks.
|
|
821
|
+
if (result.status === 'accepted' || result.status === 'unknown') {
|
|
822
|
+
this.pendingNudgeFrom.delete(sessionId);
|
|
823
|
+
this.batchAttempts.delete(sessionId);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (museAttempts >= MAX_WAKE_ATTEMPTS) {
|
|
827
|
+
this.pendingNudgeFrom.delete(sessionId);
|
|
828
|
+
this.batchAttempts.delete(sessionId);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (!current() || !(await isCurrent())) {
|
|
832
|
+
await mailStore.unmarkNudged(sessionId, batchIds, museToken);
|
|
833
|
+
if (current() && runner.isRunning(sessionId)) {
|
|
834
|
+
const live = await store.loadSession(sessionId);
|
|
835
|
+
if (current() && live && live.status === 'running' && runner.isRunning(sessionId)) {
|
|
836
|
+
this.scheduleMailNudge(sessionId, fromLabel, MAIL_NUDGE_BUSY_RECHECK_MS);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
museStamped = { batch: stamped, batchIds: stamped.map((m) => m.id), token: museToken };
|
|
661
842
|
}
|
|
662
843
|
}
|
|
844
|
+
const unstampMuse = async () => {
|
|
845
|
+
if (museStamped?.batchIds.length) {
|
|
846
|
+
await mailStore.unmarkNudged(sessionId, museStamped.batchIds, museStamped.token);
|
|
847
|
+
}
|
|
848
|
+
};
|
|
663
849
|
const legacyWait = (this.legacyReadyAt.get(sessionId) ?? 0) - this.now();
|
|
664
850
|
if (legacyWait > 0) {
|
|
851
|
+
await unstampMuse();
|
|
665
852
|
this.scheduleMailNudge(sessionId, fromLabel, legacyWait);
|
|
666
853
|
return;
|
|
667
854
|
}
|
|
668
855
|
if (this.isAgentRecentlyBusy(sessionId)) {
|
|
856
|
+
await unstampMuse();
|
|
669
857
|
this.scheduleMailNudge(sessionId, fromLabel, MAIL_NUDGE_BUSY_RECHECK_MS);
|
|
670
858
|
return;
|
|
671
859
|
}
|
|
@@ -677,40 +865,106 @@ export class MailDeliveryService {
|
|
|
677
865
|
const sinceLast = this.now() - lastNudgeAt;
|
|
678
866
|
if (sinceLast < MAIL_NUDGE_MIN_INTERVAL_MS) {
|
|
679
867
|
// Rate-limited: retry once the window reopens (mail may still be unread).
|
|
868
|
+
await unstampMuse();
|
|
680
869
|
this.scheduleMailNudge(sessionId, fromLabel, MAIL_NUDGE_MIN_INTERVAL_MS - sinceLast);
|
|
681
870
|
return;
|
|
682
871
|
}
|
|
683
872
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
873
|
+
let digest = null;
|
|
874
|
+
if (this.digestProvider) {
|
|
875
|
+
try {
|
|
876
|
+
digest = await this.digestProvider.digestFor(sessionId);
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
digest = null;
|
|
880
|
+
}
|
|
881
|
+
if (!current()) {
|
|
882
|
+
await unstampMuse();
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
692
886
|
// Recheck after native probing/rejection and before each legacy side effect.
|
|
693
|
-
|
|
887
|
+
let pending;
|
|
888
|
+
if (museStamped) {
|
|
889
|
+
const stillUndelivered = new Set((await mailStore.listUndelivered(sessionId)).map((m) => m.id));
|
|
890
|
+
pending = museStamped.batch.filter((m) => stillUndelivered.has(m.id));
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
pending = await mailStore.listUnnudgedWaking(sessionId);
|
|
894
|
+
}
|
|
694
895
|
session = await store.loadSession(sessionId);
|
|
695
|
-
if (!current() || !pending.length || !session || session.status !== 'running' || !runner.isRunning(sessionId))
|
|
896
|
+
if (!current() || !pending.length || !session || session.status !== 'running' || !runner.isRunning(sessionId)) {
|
|
897
|
+
await unstampMuse();
|
|
696
898
|
return;
|
|
899
|
+
}
|
|
697
900
|
if (session.shellType === 'codex') {
|
|
901
|
+
await unstampMuse();
|
|
698
902
|
this.scheduleMailNudge(sessionId, fromLabel, MAIL_NUDGE_DELAY_MS);
|
|
699
903
|
return;
|
|
700
904
|
}
|
|
701
905
|
const refreshedLease = this.pluginLeaseRemaining(session);
|
|
702
906
|
if (refreshedLease > 0) {
|
|
907
|
+
await unstampMuse();
|
|
703
908
|
this.scheduleMailNudge(sessionId, fromLabel, refreshedLease);
|
|
704
909
|
return;
|
|
705
910
|
}
|
|
706
911
|
if (this.isAgentRecentlyBusy(sessionId)) {
|
|
912
|
+
await unstampMuse();
|
|
707
913
|
this.scheduleMailNudge(sessionId, fromLabel, MAIL_NUDGE_BUSY_RECHECK_MS);
|
|
708
914
|
return;
|
|
709
915
|
}
|
|
916
|
+
let activeBatch;
|
|
917
|
+
let batchIds;
|
|
918
|
+
let token;
|
|
919
|
+
if (museStamped) {
|
|
920
|
+
activeBatch = pending;
|
|
921
|
+
batchIds = museStamped.batchIds;
|
|
922
|
+
token = museStamped.token;
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
token = createNudgeToken(this.now());
|
|
926
|
+
const requestedIds = pending.map((m) => m.id);
|
|
927
|
+
const stamped = await mailStore.markNudged(sessionId, requestedIds, token);
|
|
928
|
+
if (!stamped.length) {
|
|
929
|
+
await mailStore.unmarkNudged(sessionId, requestedIds, token);
|
|
930
|
+
this.pendingNudgeFrom.delete(sessionId);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
activeBatch = stamped;
|
|
934
|
+
batchIds = stamped.map((m) => m.id);
|
|
935
|
+
}
|
|
936
|
+
if (!current()) {
|
|
937
|
+
await mailStore.unmarkNudged(sessionId, batchIds, token);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const batchKey = batchIds.slice().sort().join(',');
|
|
941
|
+
const prev = this.batchAttempts.get(sessionId);
|
|
942
|
+
const attempts = (prev?.key === batchKey ? prev.count : 0) + 1;
|
|
943
|
+
this.batchAttempts.set(sessionId, { key: batchKey, count: attempts });
|
|
944
|
+
const text = sanitizeMessageText(formatNudgeText(countMailTypes(activeBatch), digest));
|
|
945
|
+
let wroteToPty = false;
|
|
946
|
+
const injected = await this.injectPtyLine(session, 'ftown-mail', text, {
|
|
947
|
+
onFirstWrite: () => {
|
|
948
|
+
wroteToPty = true;
|
|
949
|
+
},
|
|
950
|
+
});
|
|
951
|
+
const deliveredToPty = wroteToPty || injected;
|
|
952
|
+
if (!deliveredToPty) {
|
|
953
|
+
if (attempts >= MAX_WAKE_ATTEMPTS) {
|
|
954
|
+
this.batchAttempts.delete(sessionId);
|
|
955
|
+
this.pendingNudgeFrom.delete(sessionId);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
await mailStore.unmarkNudged(sessionId, batchIds, token);
|
|
959
|
+
if (current())
|
|
960
|
+
this.scheduleMailNudge(sessionId, fromLabel, MAIL_NUDGE_DELAY_MS);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
this.batchAttempts.delete(sessionId);
|
|
710
964
|
this.pendingNudgeFrom.delete(sessionId);
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
965
|
+
if (current()) {
|
|
966
|
+
this.lastNudgeAt.set(sessionId, this.now());
|
|
967
|
+
}
|
|
714
968
|
}
|
|
715
969
|
delay(ms) {
|
|
716
970
|
return new Promise((resolve) => this.setTimeoutFn(resolve, ms));
|