@agfpd/iapeer 0.4.37 → 0.4.38
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/index.ts +7 -0
- package/src/daemon/main.ts +5 -0
- package/src/lifecycle/index.ts +33 -1
- package/src/transport/index.ts +47 -1
package/package.json
CHANGED
package/src/daemon/index.ts
CHANGED
|
@@ -192,6 +192,8 @@ export interface CallToolDeps {
|
|
|
192
192
|
* see transport.RouteDeps.noteLiveTopic. Injected so the daemon never imports
|
|
193
193
|
* lifecycle; the production main wires makeNoteLiveTopic. */
|
|
194
194
|
noteLiveTopic?: (identity: string, topic: string) => void
|
|
195
|
+
/** В7 — confirmed-delivery stamp → the target's idle-proxy floor (see transport.RouteDeps.noteDelivered). */
|
|
196
|
+
noteDelivered?: (identity: string) => void
|
|
195
197
|
/** Busy-human-composer queue — see transport.createComposerDeliveryQueue. */
|
|
196
198
|
composerQueue?: ComposerQueueRouteDeps
|
|
197
199
|
/** H4 launchd-managed detector — see transport.RouteDeps.isLaunchdManaged (the
|
|
@@ -219,6 +221,7 @@ export async function callTool(
|
|
|
219
221
|
wake: deps.wake,
|
|
220
222
|
ephemeral: deps.ephemeral,
|
|
221
223
|
noteLiveTopic: deps.noteLiveTopic,
|
|
224
|
+
noteDelivered: deps.noteDelivered,
|
|
222
225
|
composerQueue: deps.composerQueue,
|
|
223
226
|
isLaunchdManaged: deps.isLaunchdManaged,
|
|
224
227
|
})
|
|
@@ -390,6 +393,9 @@ export interface StartDaemonOptions extends StorageOptions {
|
|
|
390
393
|
* main wires the lifecycle marker write (makeNoteLiveTopic).
|
|
391
394
|
*/
|
|
392
395
|
noteLiveTopic?: (identity: string, topic: string) => void
|
|
396
|
+
/** В7 — confirmed-delivery stamp → the target's idle-proxy floor (setLastDelivered). OFF by default;
|
|
397
|
+
* the production main wires it. */
|
|
398
|
+
noteDelivered?: (identity: string) => void
|
|
393
399
|
/**
|
|
394
400
|
* Busy-human-composer queue — async acceptance for live local TUI delivery when
|
|
395
401
|
* an attached operator has non-dim text in the target composer. OFF by default
|
|
@@ -433,6 +439,7 @@ export async function startDaemon(opts: StartDaemonOptions = {}): Promise<Daemon
|
|
|
433
439
|
onDelivered: opts.onDelivered,
|
|
434
440
|
ephemeral: opts.ephemeral,
|
|
435
441
|
noteLiveTopic: opts.noteLiveTopic,
|
|
442
|
+
noteDelivered: opts.noteDelivered,
|
|
436
443
|
composerQueue: opts.composerQueue,
|
|
437
444
|
isLaunchdManaged: opts.isLaunchdManaged,
|
|
438
445
|
})
|
package/src/daemon/main.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
readTopic,
|
|
43
43
|
resolveWakeRuntime,
|
|
44
44
|
setEphemeralArmed,
|
|
45
|
+
setLastDelivered,
|
|
45
46
|
superviseTick,
|
|
46
47
|
wakeOrSpawn,
|
|
47
48
|
type LifecycleConfig,
|
|
@@ -222,6 +223,7 @@ export function makeComposerQueueRouteDeps(cfg: LifecycleConfig, env: NodeJS.Pro
|
|
|
222
223
|
// flip + the shadow observer. tmux targets ignore it.
|
|
223
224
|
logDir: cfg.logDir,
|
|
224
225
|
noteLiveTopic,
|
|
226
|
+
noteDelivered: identity => setLastDelivered(cfg, identity),
|
|
225
227
|
onDelivered,
|
|
226
228
|
notifyFailed: async (job, reason) => {
|
|
227
229
|
const index = readPeersIndex({ env })
|
|
@@ -330,6 +332,9 @@ export async function startConfiguredDaemon(opts: ConfiguredDaemonOptions = {}):
|
|
|
330
332
|
// the wake resolver's executor discriminator then compares against the topic
|
|
331
333
|
// the session last WORKED ON, not the one it woke with.
|
|
332
334
|
noteLiveTopic,
|
|
335
|
+
// В7 — a confirmed live delivery floors the target's idle proxy (setLastDelivered), so a
|
|
336
|
+
// just-delivered message is never reaped away before its turn record lands.
|
|
337
|
+
noteDelivered: identity => setLastDelivered(cfg, identity),
|
|
333
338
|
// Busy-human-composer queue: fast queued ack, async drain, fail all pending
|
|
334
339
|
// on daemon close/update so a queued envelope is never silently lost.
|
|
335
340
|
composerQueue,
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -292,6 +292,34 @@ export function clearIdleReaped(cfg: LifecycleConfig, identity: string): void {
|
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
function lastDeliveredPath(cfg: LifecycleConfig, identity: string): string {
|
|
296
|
+
return join(cfg.stateDir, `${identity}.last-delivered`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Stamp the moment a live delivery to this session was CONFIRMED (В7). superviseTick folds this into
|
|
300
|
+
* the idle proxy so a message delivered just as the session crosses idleSecs cannot be reaped out from
|
|
301
|
+
* under the just-delivered message before the runtime writes its turn record — the receiver's own
|
|
302
|
+
* process may not have appended the user-turn yet (codex socket-ack lands ok BEFORE ingest), so the
|
|
303
|
+
* transcript proxy alone would miss it and the reap would silently drop the message behind ok:true. */
|
|
304
|
+
export function setLastDelivered(cfg: LifecycleConfig, identity: string, nowMs: number = Date.now()): void {
|
|
305
|
+
mkdirSync(cfg.stateDir, { recursive: true, mode: 0o700 })
|
|
306
|
+
try {
|
|
307
|
+
writeFileSync(lastDeliveredPath(cfg, identity), String(nowMs), { mode: 0o600 })
|
|
308
|
+
} catch {
|
|
309
|
+
/* best-effort — the transcript proxy still governs; this only ADDS a floor, never removes one */
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Epoch-ms of the last confirmed live delivery to this session, or 0 when none/unreadable. */
|
|
314
|
+
export function readLastDelivered(cfg: LifecycleConfig, identity: string): number {
|
|
315
|
+
try {
|
|
316
|
+
const n = parseInt(readFileSync(lastDeliveredPath(cfg, identity), 'utf8').trim(), 10)
|
|
317
|
+
return Number.isFinite(n) && n > 0 ? n : 0
|
|
318
|
+
} catch {
|
|
319
|
+
return 0
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
295
323
|
function newEagerPath(cfg: LifecycleConfig, identity: string): string {
|
|
296
324
|
return join(cfg.stateDir, `${identity}.new-eager`)
|
|
297
325
|
}
|
|
@@ -1328,7 +1356,11 @@ function superviseOnePeer(s: SessionState, ctx: SuperviseCtx): SuperviseOutcome
|
|
|
1328
1356
|
const ephemeral = isEphemeralPeer(s.cwd)
|
|
1329
1357
|
let lt = 0
|
|
1330
1358
|
try { lt = lastTurnMt(s.runtime, s.cwd) ?? 0 } catch { /* unreadable → wokeAt floor */ }
|
|
1331
|
-
|
|
1359
|
+
// В7 — fold in the last CONFIRMED live delivery. A message delivered just as the session crosses
|
|
1360
|
+
// idleSecs (codex acks BEFORE it writes the turn record) would otherwise be reaped out from under the
|
|
1361
|
+
// just-delivered message before lt catches up — a silent loss behind ok:true. lastDelivered only ever
|
|
1362
|
+
// RAISES the floor (a delivery is activity), so it can never keep a genuinely idle session alive.
|
|
1363
|
+
const mt = Math.max(lt, s.wokeAt, readLastDelivered(cfg, s.identity))
|
|
1332
1364
|
const ageSecs = Math.floor((nowMs - mt) / 1000)
|
|
1333
1365
|
// wake_policy:ephemeral M2 — die-after-reply: an ARMED ephemeral session (the
|
|
1334
1366
|
// daemon routed its outbound reply) is reaped after a QUIET window, checked
|
package/src/transport/index.ts
CHANGED
|
@@ -217,13 +217,37 @@ export interface WarmDeliverSeam {
|
|
|
217
217
|
* via launchd KeepAlive).
|
|
218
218
|
* See `deliverViaHost`.
|
|
219
219
|
*/
|
|
220
|
+
// В6 — per-target delivery serialization. deliverToHost is paste → 300ms settle → CR over the pty; two
|
|
221
|
+
// CONCURRENT deliveries to the SAME hosted session interleave their frames (paste2 lands inside paste1's
|
|
222
|
+
// settle window → CR1 submits the SPLICED "envelope1envelope2" as one turn, CR2 submits an empty
|
|
223
|
+
// composer; for a router target the splice breaks the mate's per-envelope framing). Chain deliveries per
|
|
224
|
+
// address so each paste+settle+CR (and its confirm) completes as a UNIT before the next to that address
|
|
225
|
+
// starts. Different addresses stay parallel. Covers the DOMINANT path — every agent send_to_peer routes
|
|
226
|
+
// through the SINGLE daemon process; a rare CLI in-process `iapeer send` is a separate process this chain
|
|
227
|
+
// does not span (fully hermetic serialization would live in the supervisor as a per-session transaction).
|
|
228
|
+
const deliverTails = new Map<string, Promise<unknown>>()
|
|
229
|
+
|
|
220
230
|
export async function deliverWarm(
|
|
221
231
|
target: DeliveryTarget,
|
|
222
232
|
envelope: string,
|
|
223
233
|
cwd?: string,
|
|
224
234
|
seam: WarmDeliverSeam = {},
|
|
225
235
|
): Promise<Result<void>> {
|
|
226
|
-
|
|
236
|
+
const address = target.address
|
|
237
|
+
const prev = deliverTails.get(address) ?? Promise.resolve()
|
|
238
|
+
// run AFTER prev settles (success or failure) — a prior delivery's error must not stall the chain
|
|
239
|
+
const run = prev.then(
|
|
240
|
+
() => deliverViaHost(target, envelope, cwd, seam),
|
|
241
|
+
() => deliverViaHost(target, envelope, cwd, seam),
|
|
242
|
+
)
|
|
243
|
+
deliverTails.set(
|
|
244
|
+
address,
|
|
245
|
+
run.then(
|
|
246
|
+
() => {},
|
|
247
|
+
() => {},
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
return run
|
|
227
251
|
}
|
|
228
252
|
|
|
229
253
|
async function deliverViaHost(
|
|
@@ -890,6 +914,10 @@ export interface RouteDeps {
|
|
|
890
914
|
* imports lifecycle (layering — main.ts is where the two meet); call sites
|
|
891
915
|
* swallow hook errors (a hook must never fail a delivered message). */
|
|
892
916
|
noteLiveTopic?: (identity: string, topic: string) => void
|
|
917
|
+
/** В7 — stamp a CONFIRMED live delivery (target identity) so superviseTick's idle proxy floors on it
|
|
918
|
+
* and cannot reap the session before the just-delivered message's turn record lands. Injected (same
|
|
919
|
+
* layering as noteLiveTopic — lifecycle.setLastDelivered wired in main.ts). */
|
|
920
|
+
noteDelivered?: (identity: string) => void
|
|
893
921
|
/** H4 launchd-managed detector (injected so transport never imports lifecycle —
|
|
894
922
|
* main.ts wires the real `isLaunchdManaged`). A MISS on a launchd-managed target is
|
|
895
923
|
* almost always a TRANSIENT restart window (router restart on connect / `iapeer
|
|
@@ -913,6 +941,16 @@ function noteTopic(deps: RouteDeps, identity: string, topic: string | undefined)
|
|
|
913
941
|
}
|
|
914
942
|
}
|
|
915
943
|
|
|
944
|
+
/** Best-effort lastDelivered stamp (В7) — records a CONFIRMED live delivery so the idle-reap proxy
|
|
945
|
+
* cannot reap the session out from under a just-delivered message before its turn record lands. */
|
|
946
|
+
function noteDelivered(deps: RouteDeps, identity: string): void {
|
|
947
|
+
try {
|
|
948
|
+
deps.noteDelivered?.(identity)
|
|
949
|
+
} catch {
|
|
950
|
+
/* a post-delivery hook must never fail the delivery */
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
916
954
|
export interface ComposerDeliveryQueueOptions {
|
|
917
955
|
env?: NodeJS.ProcessEnv
|
|
918
956
|
pollMs?: number
|
|
@@ -938,6 +976,8 @@ export interface ComposerDeliveryQueueOptions {
|
|
|
938
976
|
deliver?: (target: DeliveryTarget, envelope: string, cwd?: string) => Result<void> | Promise<Result<void>>
|
|
939
977
|
notifyFailed: (job: ComposerQueuedDelivery, reason: string) => void | Promise<void>
|
|
940
978
|
noteLiveTopic?: (identity: string, topic: string) => void
|
|
979
|
+
/** В7 — stamp a confirmed queued delivery (same seam as RouteDeps.noteDelivered). */
|
|
980
|
+
noteDelivered?: (identity: string) => void
|
|
941
981
|
onDelivered?: (caller: ResolvedCaller) => void
|
|
942
982
|
}
|
|
943
983
|
|
|
@@ -1078,6 +1118,11 @@ export function createComposerDeliveryQueue(opts: ComposerDeliveryQueueOptions):
|
|
|
1078
1118
|
await notifyFailed(job, `queued delivery to ${job.target.personality} (${job.target.runtime}) failed: ${delivered.error.message}`)
|
|
1079
1119
|
} else {
|
|
1080
1120
|
noteTopic({ noteLiveTopic: opts.noteLiveTopic }, job.target.address, job.topic)
|
|
1121
|
+
try {
|
|
1122
|
+
opts.noteDelivered?.(job.target.address) // В7 — floor the idle proxy for the queued delivery too
|
|
1123
|
+
} catch {
|
|
1124
|
+
/* best-effort */
|
|
1125
|
+
}
|
|
1081
1126
|
try {
|
|
1082
1127
|
opts.onDelivered?.(job.caller)
|
|
1083
1128
|
} catch {
|
|
@@ -1279,6 +1324,7 @@ export async function routeSend(
|
|
|
1279
1324
|
const delivered = await deliverWarm(target.value, envelope, peer.cwd)
|
|
1280
1325
|
if (!delivered.ok) return delivered
|
|
1281
1326
|
noteTopic(deps, target.value.address, topic)
|
|
1327
|
+
noteDelivered(deps, target.value.address) // В7 — floor the idle proxy so this delivery isn't reaped away
|
|
1282
1328
|
return ok({
|
|
1283
1329
|
ok: true,
|
|
1284
1330
|
delivered_to: { personality: target.value.personality, runtime: target.value.runtime },
|