@agfpd/iapeer 0.4.127 → 0.4.129
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/docs/19-notices.md
CHANGED
|
@@ -336,7 +336,9 @@ nothing to spare). A sweep only opens files whose mtime moved since the last pas
|
|
|
336
336
|
|
|
337
337
|
Attribution does not derive the path from `cwd`. It reads the cwd each file states **about
|
|
338
338
|
itself** (claude: the `cwd` field on every line; codex: `session_meta.payload.cwd`) and matches
|
|
339
|
-
it against the registry, case-insensitively
|
|
339
|
+
it against the registry, case-insensitively — the peer whose cwd is that path or its NEAREST
|
|
340
|
+
ancestor (a peer that `cd`-ed into a subdirectory stays itself, 18.10.2026; ancestors shallower
|
|
341
|
+
than `/<a>/<b>/<c>` match only exactly). A file whose cwd belongs to no peer is ignored —
|
|
340
342
|
a human's own session never notifies.
|
|
341
343
|
|
|
342
344
|
The reason is that one rule must serve both runtimes: the claude slug is a naming convention we
|
|
@@ -456,7 +458,11 @@ five-hour wall at 04:55; claude painted «resets 8:30am», the daemon idle-reape
|
|
|
456
458
|
3. **Nudge.** After `resetsAt + 3 min`, if the session made no progress since the event (claude: a
|
|
457
459
|
real, non-`<synthetic>` assistant line after it; codex: a `task_complete` with a message whose
|
|
458
460
|
`completed_at` is after it), the core delivers ONE continuation message through the normal
|
|
459
|
-
route — wake-on-miss
|
|
461
|
+
route — wake-on-miss RESUMES a reaped session: the nudge carries an explicit continue-work
|
|
462
|
+
wake intent that satisfies the executor topic gate by itself (wake cause
|
|
463
|
+
`idle-reaped-continue`); it never outranks an owner `/new`, a soft-reload or a crash. Without
|
|
464
|
+
it the first live run (0.4.127) woke the peer FRESH (`idle-reaped-new-topic`) and the peer
|
|
465
|
+
answered «nothing to resume» — the very loss this section exists to prevent. Progress
|
|
460
466
|
already made → the park is dropped without a message (no duplicate turn). A failed delivery is
|
|
461
467
|
retried (5 min backoff, 3 attempts); an operator `stop` is never fought.
|
|
462
468
|
4. **Sender** = the core's system identity `iapeer-system` (runtime `core`, intelligence
|
package/package.json
CHANGED
|
@@ -74,7 +74,10 @@ export async function processLimitResumes(
|
|
|
74
74
|
}
|
|
75
75
|
let reason: string
|
|
76
76
|
try {
|
|
77
|
-
|
|
77
|
+
// Topic = the stopped session's own thread (captured at park time), so the topic set still
|
|
78
|
+
// follows the work; the RESUME itself rides the explicit continue-work intent, not the topic.
|
|
79
|
+
const topic = p.topic || 'limit-resume'
|
|
80
|
+
const r = await send(systemCaller(), { personality: p.personality, runtime: p.runtime, message: limitResumeMessage(p), topic }, { ...routeDeps, wakeContinueWork: true })
|
|
78
81
|
if (r.ok) {
|
|
79
82
|
clearLimitPark(cfg, p.identity)
|
|
80
83
|
log({ identity: p.identity, outcome: 'nudged', woke: String(r.value.woke), resets: new Date(p.resetsAtMs).toISOString() })
|
package/src/daemon/main.ts
CHANGED
|
@@ -140,7 +140,7 @@ const DAEMON_MAIN_PATH = fileURLToPath(import.meta.url)
|
|
|
140
140
|
export function makeWakeFn(cfg: LifecycleConfig, env: NodeJS.ProcessEnv): WakeFn {
|
|
141
141
|
return (req: WakeRequest): Promise<WakeOutcome> =>
|
|
142
142
|
wakeOrSpawn(
|
|
143
|
-
{ personality: req.personality, runtime: req.runtime, topic: req.topic, task: req.task },
|
|
143
|
+
{ personality: req.personality, runtime: req.runtime, topic: req.topic, task: req.task, ...(req.continueWork ? { continueWork: true } : {}) },
|
|
144
144
|
{ cfg, env },
|
|
145
145
|
)
|
|
146
146
|
}
|
|
@@ -493,6 +493,7 @@ export async function startConfiguredDaemon(opts: ConfiguredDaemonOptions = {}):
|
|
|
493
493
|
...(d.file ? { file: d.file } : {}),
|
|
494
494
|
...(d.sessionId ? { sessionId: d.sessionId } : {}),
|
|
495
495
|
errorType: d.errorType,
|
|
496
|
+
...(readTopic(cfg, identity) ? { topic: readTopic(cfg, identity) } : {}),
|
|
496
497
|
})
|
|
497
498
|
if (fresh) appendLifecycleEvent(cfg.eventLogDir, { ev: 'limit-park', identity, resets: new Date(d.resetsAtMs).toISOString(), error_type: d.errorType }, { env })
|
|
498
499
|
},
|
package/src/daemon/mutewatch.ts
CHANGED
|
@@ -557,6 +557,28 @@ export function muteWatchTick(deps: MuteWatchDeps, sinceMs: number): { raised: n
|
|
|
557
557
|
return { raised, sinceMs: startedMs }
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
/**
|
|
561
|
+
* personality ← a session's self-stated cwd: the peer whose registered cwd is that path OR its
|
|
562
|
+
* NEAREST ancestor. A session records the cwd it is IN at each line, and a peer that `cd`s into a
|
|
563
|
+
* subdirectory of its project (observed 18.10.2026: `…/oc-test-nemo/.iapeer`) would otherwise fall
|
|
564
|
+
* out of attribution — no notice, no limit-park. Longest prefix wins, so a peer nested inside
|
|
565
|
+
* another peer's tree is attributed to itself. Keys are normalized (trailing slash off, lowercase).
|
|
566
|
+
*/
|
|
567
|
+
export function peerByCwdPrefix(byCwd: Map<string, string>, cwd: string): string | undefined {
|
|
568
|
+
const exact = cwd.replace(/\/+$/, '').toLowerCase()
|
|
569
|
+
let p = exact
|
|
570
|
+
while (p) {
|
|
571
|
+
const hit = byCwd.get(p)
|
|
572
|
+
// An ANCESTOR hit counts only below /<a>/<b>/<c> depth: a peer whose cwd is $HOME (or shallower)
|
|
573
|
+
// must not swallow every session on the host — such a peer attributes by exact match only.
|
|
574
|
+
if (hit && (p === exact || p.split('/').length > 4)) return hit
|
|
575
|
+
const i = p.lastIndexOf('/')
|
|
576
|
+
if (i <= 0) return undefined
|
|
577
|
+
p = p.slice(0, i)
|
|
578
|
+
}
|
|
579
|
+
return undefined
|
|
580
|
+
}
|
|
581
|
+
|
|
560
582
|
export interface StartMuteWatchOptions {
|
|
561
583
|
env?: NodeJS.ProcessEnv
|
|
562
584
|
intervalMs?: number
|
|
@@ -604,7 +626,7 @@ export function startMuteWatch(cfg: LifecycleConfig, board: NoticeBoard, opts: S
|
|
|
604
626
|
now,
|
|
605
627
|
onError: opts.onError,
|
|
606
628
|
onLimit: opts.onLimit,
|
|
607
|
-
peerByCwd: (cwd: string) => byCwd
|
|
629
|
+
peerByCwd: (cwd: string) => peerByCwdPrefix(byCwd, cwd),
|
|
608
630
|
opencodeJournals: () =>
|
|
609
631
|
rows
|
|
610
632
|
.filter(r => (r.runtimes ?? []).includes('opencode'))
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -653,6 +653,10 @@ export function resolveWakeMode(
|
|
|
653
653
|
* next resume/fresh decision (a crash-looping binary used to eat .idle-reaped, and after the fix the
|
|
654
654
|
* peer came back FRESH, its context lost). Absent → consume immediately (legacy/test callers). */
|
|
655
655
|
defer?: (consume: () => void) => void,
|
|
656
|
+
/** The waking message is an explicit "continue THIS work" (the limit-resume nudge, docs/19 §9):
|
|
657
|
+
* in the idle-reaped executor branch it satisfies the topic gate by itself. Never outranks an
|
|
658
|
+
* owner's /new, a soft-reload, a folder-launch or a crash (those decide before 3b). */
|
|
659
|
+
continueWork?: boolean,
|
|
656
660
|
): WakeMode {
|
|
657
661
|
const done = defer ?? ((f: () => void) => f())
|
|
658
662
|
// 1. folder-launch → always fresh. Consume a standing .new-eager too — the mark's
|
|
@@ -725,6 +729,12 @@ export function resolveWakeMode(
|
|
|
725
729
|
// gap is not a request to resume specific work; defaulting it to resume revived stale
|
|
726
730
|
// context (e.g. a 3-day-old session). The sender opts INTO continuity by passing the topic.
|
|
727
731
|
const topic = incomingTopic?.trim() ?? ''
|
|
732
|
+
if (continueWork) {
|
|
733
|
+
const r = resolveResume(cwd)
|
|
734
|
+
return r.ok
|
|
735
|
+
? { resume: true, resumeRef: r.ref, cause: 'idle-reaped-continue' }
|
|
736
|
+
: { resume: false, cause: 'idle-reaped-no-resume' }
|
|
737
|
+
}
|
|
728
738
|
if (!topic) return { resume: false, cause: 'idle-reaped-no-topic' }
|
|
729
739
|
// RESUME if the incoming topic matches ANY thread this session lineage worked on — not
|
|
730
740
|
// just the last. Different threads accumulate in one session; a later ping on an earlier
|
|
@@ -967,6 +977,8 @@ export interface WakeArgs {
|
|
|
967
977
|
/** First message delivered to the woken session (the routed envelope). */
|
|
968
978
|
task: string
|
|
969
979
|
resume?: boolean
|
|
980
|
+
/** Explicit "continue THIS work" intent — see resolveWakeMode `continueWork`. */
|
|
981
|
+
continueWork?: boolean
|
|
970
982
|
}
|
|
971
983
|
|
|
972
984
|
export interface WakeResult {
|
|
@@ -1124,6 +1136,7 @@ export async function wakeOrSpawn(args: WakeArgs, deps: WakeDeps = {}): Promise<
|
|
|
1124
1136
|
c => adapter.resolveResume(c, { personality: args.personality, env }),
|
|
1125
1137
|
args.topic,
|
|
1126
1138
|
f => consumeOnUp.push(f),
|
|
1139
|
+
args.continueWork,
|
|
1127
1140
|
)
|
|
1128
1141
|
const applyConsumed = (): void => {
|
|
1129
1142
|
for (const f of consumeOnUp.splice(0)) {
|
package/src/transport/index.ts
CHANGED
|
@@ -1272,6 +1272,8 @@ export interface WakeRequest {
|
|
|
1272
1272
|
topic?: string
|
|
1273
1273
|
/** First message delivered to the woken session (the routed envelope). */
|
|
1274
1274
|
task: string
|
|
1275
|
+
/** Explicit "continue THIS work" intent (core-originated limit-resume only). */
|
|
1276
|
+
continueWork?: boolean
|
|
1275
1277
|
}
|
|
1276
1278
|
export interface WakeOutcome {
|
|
1277
1279
|
status: 'READY' | 'FAILED'
|
|
@@ -1347,6 +1349,9 @@ export interface ComposerQueueRouteDeps {
|
|
|
1347
1349
|
}
|
|
1348
1350
|
|
|
1349
1351
|
export interface RouteDeps {
|
|
1352
|
+
/** A wake triggered by THIS send carries the "continue THIS work" intent (resume an idle-reaped
|
|
1353
|
+
* session regardless of topic). Set only by the core's limit-resume pass — never from a request. */
|
|
1354
|
+
wakeContinueWork?: boolean
|
|
1350
1355
|
/** Б7 — injected env sandboxes ALL of routeSend's env-resolved reads (the registry, the supervisor
|
|
1351
1356
|
* run-dir liveness/delivery). Default process.env (prod: env === process.env). Closes the daemon:286
|
|
1352
1357
|
* isolation invariant — a library/test daemon with an injected root no longer reads the real registry
|
|
@@ -1970,7 +1975,7 @@ async function routeSendInner(
|
|
|
1970
1975
|
`(restart window outlasted the grace?) — message NOT delivered; retry`,
|
|
1971
1976
|
)
|
|
1972
1977
|
}
|
|
1973
|
-
const woke = await deps.wake({ personality, runtime, topic, task: envelope })
|
|
1978
|
+
const woke = await deps.wake({ personality, runtime, topic, task: envelope, ...(deps.wakeContinueWork ? { continueWork: true } : {}) })
|
|
1974
1979
|
if (woke.status === 'FAILED') {
|
|
1975
1980
|
// C1 — a durably STOPPED peer is a deliberate halt, not a transient miss:
|
|
1976
1981
|
// surface the explicit "stopped" reason (no "offline and wake failed" wrapping).
|