@namzu/sdk 38.1.0 → 38.2.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/manager/resident/agenda.d.ts +125 -0
  3. package/dist/manager/resident/agenda.d.ts.map +1 -0
  4. package/dist/manager/resident/agenda.js +551 -0
  5. package/dist/manager/resident/agenda.js.map +1 -0
  6. package/dist/manager/resident/delivery-window.d.ts +22 -0
  7. package/dist/manager/resident/delivery-window.d.ts.map +1 -0
  8. package/dist/manager/resident/delivery-window.js +78 -0
  9. package/dist/manager/resident/delivery-window.js.map +1 -0
  10. package/dist/manager/resident/host.d.ts +78 -0
  11. package/dist/manager/resident/host.d.ts.map +1 -0
  12. package/dist/manager/resident/host.js +252 -0
  13. package/dist/manager/resident/host.js.map +1 -0
  14. package/dist/manager/resident/initiative.d.ts +111 -0
  15. package/dist/manager/resident/initiative.d.ts.map +1 -0
  16. package/dist/manager/resident/initiative.js +121 -0
  17. package/dist/manager/resident/initiative.js.map +1 -0
  18. package/dist/manager/resident/learning.d.ts +483 -0
  19. package/dist/manager/resident/learning.d.ts.map +1 -0
  20. package/dist/manager/resident/learning.js +263 -0
  21. package/dist/manager/resident/learning.js.map +1 -0
  22. package/dist/manager/resident/loop.d.ts +31 -0
  23. package/dist/manager/resident/loop.d.ts.map +1 -0
  24. package/dist/manager/resident/loop.js +69 -0
  25. package/dist/manager/resident/loop.js.map +1 -0
  26. package/dist/manager/resident/outbox.d.ts +180 -0
  27. package/dist/manager/resident/outbox.d.ts.map +1 -0
  28. package/dist/manager/resident/outbox.js +214 -0
  29. package/dist/manager/resident/outbox.js.map +1 -0
  30. package/dist/manager/resident/proposal.d.ts +91 -0
  31. package/dist/manager/resident/proposal.d.ts.map +1 -0
  32. package/dist/manager/resident/proposal.js +75 -0
  33. package/dist/manager/resident/proposal.js.map +1 -0
  34. package/dist/manager/resident/store.d.ts +149 -0
  35. package/dist/manager/resident/store.d.ts.map +1 -0
  36. package/dist/manager/resident/store.js +175 -0
  37. package/dist/manager/resident/store.js.map +1 -0
  38. package/dist/prompt/contributions.d.ts +5 -2
  39. package/dist/prompt/contributions.d.ts.map +1 -1
  40. package/dist/prompt/contributions.js.map +1 -1
  41. package/dist/prompt/index.d.ts +2 -0
  42. package/dist/prompt/index.d.ts.map +1 -1
  43. package/dist/prompt/index.js +1 -0
  44. package/dist/prompt/index.js.map +1 -1
  45. package/dist/prompt/resident-step.d.ts +29 -0
  46. package/dist/prompt/resident-step.d.ts.map +1 -0
  47. package/dist/prompt/resident-step.js +74 -0
  48. package/dist/prompt/resident-step.js.map +1 -0
  49. package/dist/public-runtime.d.ts +10 -1
  50. package/dist/public-runtime.d.ts.map +1 -1
  51. package/dist/public-runtime.js +10 -1
  52. package/dist/public-runtime.js.map +1 -1
  53. package/dist/public-types.d.ts +16 -2
  54. package/dist/public-types.d.ts.map +1 -1
  55. package/dist/registry/index.d.ts +1 -1
  56. package/dist/registry/index.d.ts.map +1 -1
  57. package/dist/registry/tool/execute.d.ts +18 -0
  58. package/dist/registry/tool/execute.d.ts.map +1 -1
  59. package/dist/registry/tool/execute.js +43 -6
  60. package/dist/registry/tool/execute.js.map +1 -1
  61. package/dist/runtime/query/events.d.ts +6 -1
  62. package/dist/runtime/query/events.d.ts.map +1 -1
  63. package/dist/runtime/query/events.js +21 -12
  64. package/dist/runtime/query/events.js.map +1 -1
  65. package/dist/runtime/query/index.d.ts.map +1 -1
  66. package/dist/runtime/query/index.js +1 -1
  67. package/dist/runtime/query/index.js.map +1 -1
  68. package/dist/runtime/query/prompt-cache.d.ts.map +1 -1
  69. package/dist/runtime/query/prompt-cache.js +21 -42
  70. package/dist/runtime/query/prompt-cache.js.map +1 -1
  71. package/package.json +1 -1
  72. package/src/manager/resident/agenda.ts +853 -0
  73. package/src/manager/resident/delivery-window.ts +93 -0
  74. package/src/manager/resident/host.ts +356 -0
  75. package/src/manager/resident/initiative.ts +195 -0
  76. package/src/manager/resident/learning.ts +369 -0
  77. package/src/manager/resident/loop.ts +101 -0
  78. package/src/manager/resident/outbox.ts +307 -0
  79. package/src/manager/resident/proposal.ts +105 -0
  80. package/src/manager/resident/store.ts +229 -0
  81. package/src/prompt/contributions.ts +5 -2
  82. package/src/prompt/index.ts +2 -0
  83. package/src/prompt/resident-step.ts +96 -0
  84. package/src/public-runtime.ts +14 -0
  85. package/src/public-types.ts +67 -0
  86. package/src/registry/index.ts +1 -1
  87. package/src/registry/tool/execute.ts +56 -9
  88. package/src/runtime/query/events.ts +24 -14
  89. package/src/runtime/query/index.ts +1 -2
  90. package/src/runtime/query/prompt-cache.ts +24 -47
@@ -0,0 +1,369 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { z } from 'zod'
3
+ import {
4
+ type HarnessVerificationBatch,
5
+ reviewHarnessCandidate,
6
+ } from '../../eval/harness-verification.js'
7
+
8
+ const label = z.string().trim().min(1).max(256)
9
+ const name = z.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/)
10
+ const digest = z.string().regex(/^[a-f0-9]{64}$/)
11
+ export const residentLearningEvidenceSchema = z.object({
12
+ key: label,
13
+ source: label,
14
+ reason: z.string().trim().min(1).max(1_000),
15
+ })
16
+
17
+ /** @experimental Host provenance for a correction, evaluated activation or rollback. */
18
+ export type ResidentLearningEvidence = Readonly<z.infer<typeof residentLearningEvidenceSchema>>
19
+
20
+ const candidateSchema = z.object({
21
+ name,
22
+ description: z.string().trim().min(1).max(1_000),
23
+ body: z.string().trim().min(1).max(4_000),
24
+ })
25
+
26
+ /** @experimental Instructional guidance only; never executable assets or tool permissions. */
27
+ export type ResidentSkillCandidate = Readonly<z.infer<typeof candidateSchema>>
28
+
29
+ const learnedSkillSchema = candidateSchema.extend({
30
+ hash: digest,
31
+ evidence: residentLearningEvidenceSchema,
32
+ verification: z.object({
33
+ baselineHash: z.union([digest, z.literal('none')]),
34
+ candidateHash: digest,
35
+ evidenceDigest: digest,
36
+ verificationTasks: z.number().int().min(5).max(64),
37
+ confirmationTasks: z.number().int().min(5).max(64),
38
+ }),
39
+ })
40
+
41
+ /** @experimental Immutable evaluated content and evidence identity. */
42
+ export type ResidentLearnedSkill = Readonly<z.infer<typeof learnedSkillSchema>>
43
+
44
+ export const residentLearningSchema = z
45
+ .object({
46
+ revision: z.number().int().positive().safe(),
47
+ identity: z
48
+ .object({
49
+ text: z.string().trim().min(1).max(4_000),
50
+ evidence: residentLearningEvidenceSchema,
51
+ })
52
+ .optional(),
53
+ preferences: z
54
+ .array(
55
+ z.object({
56
+ key: name,
57
+ value: z.string().trim().min(1).max(1_000),
58
+ evidence: residentLearningEvidenceSchema,
59
+ }),
60
+ )
61
+ .max(32),
62
+ skills: z.array(learnedSkillSchema).max(16),
63
+ lastChange: residentLearningEvidenceSchema,
64
+ })
65
+ .superRefine((state, context) => {
66
+ if (
67
+ new Set(state.preferences.map((p) => p.key)).size !== state.preferences.length ||
68
+ new Set(state.skills.map((s) => s.name)).size !== state.skills.length ||
69
+ state.skills.some(
70
+ (skill) =>
71
+ skill.hash !== hashResidentSkill(skill) ||
72
+ skill.verification.candidateHash !== skill.hash,
73
+ )
74
+ )
75
+ context.addIssue({
76
+ code: z.ZodIssueCode.custom,
77
+ message: 'Invalid resident learning identity or content digest.',
78
+ })
79
+ })
80
+
81
+ /** @experimental Versioned behavioral overlay; the original resident mandate remains unchanged. */
82
+ export interface ResidentLearningState {
83
+ readonly revision: number
84
+ readonly identity?: { readonly text: string; readonly evidence: ResidentLearningEvidence }
85
+ readonly preferences: readonly {
86
+ readonly key: string
87
+ readonly value: string
88
+ readonly evidence: ResidentLearningEvidence
89
+ }[]
90
+ readonly skills: readonly ResidentLearnedSkill[]
91
+ readonly lastChange: ResidentLearningEvidence
92
+ }
93
+
94
+ /** @experimental Corrections name the current preference evidence they supersede. */
95
+ export interface ResidentProfileUpdate {
96
+ readonly identity?: string
97
+ readonly preferences?: readonly {
98
+ readonly key: string
99
+ readonly value: string
100
+ readonly supersedes: string | null
101
+ }[]
102
+ readonly evidence: ResidentLearningEvidence
103
+ }
104
+
105
+ /** @experimental Recorded paired runs; hosts own execution, scoring and independent attribution. */
106
+ export interface ResidentSkillEvaluation {
107
+ readonly verification: HarnessVerificationBatch
108
+ readonly confirmation?: HarnessVerificationBatch
109
+ }
110
+
111
+ /** Internal immutable snapshot used by storage and admitted context. */
112
+ export function freezeResidentLearning(input: ResidentLearningState): ResidentLearningState {
113
+ const state = residentLearningSchema.parse(input)
114
+ return Object.freeze({
115
+ ...state,
116
+ ...(state.identity
117
+ ? {
118
+ identity: Object.freeze({
119
+ ...state.identity,
120
+ evidence: Object.freeze(state.identity.evidence),
121
+ }),
122
+ }
123
+ : {}),
124
+ preferences: Object.freeze(
125
+ state.preferences.map((p) => Object.freeze({ ...p, evidence: Object.freeze(p.evidence) })),
126
+ ),
127
+ skills: Object.freeze(
128
+ state.skills.map((skill) =>
129
+ Object.freeze({
130
+ ...skill,
131
+ evidence: Object.freeze(skill.evidence),
132
+ verification: Object.freeze(skill.verification),
133
+ }),
134
+ ),
135
+ ),
136
+ lastChange: Object.freeze(state.lastChange),
137
+ })
138
+ }
139
+
140
+ function evidenceFor(
141
+ current: ResidentLearningState | undefined,
142
+ input: ResidentLearningEvidence,
143
+ ): ResidentLearningEvidence {
144
+ const evidence = residentLearningEvidenceSchema.parse(input)
145
+ const used = [
146
+ current?.lastChange,
147
+ current?.identity?.evidence,
148
+ ...(current?.preferences.map((p) => p.evidence) ?? []),
149
+ ...(current?.skills.map((s) => s.evidence) ?? []),
150
+ ]
151
+ if (used.some((old) => old?.key === evidence.key))
152
+ throw new Error('A learning change requires a fresh evidence key.')
153
+ return evidence
154
+ }
155
+
156
+ function base(
157
+ current: ResidentLearningState | undefined,
158
+ evidence: ResidentLearningEvidence,
159
+ ): ResidentLearningState {
160
+ const checked = current ? freezeResidentLearning(current) : undefined
161
+ return {
162
+ ...checked,
163
+ revision: (checked?.revision ?? 0) + 1,
164
+ preferences: checked?.preferences ?? [],
165
+ skills: checked?.skills ?? [],
166
+ lastChange: evidence,
167
+ }
168
+ }
169
+
170
+ /** Internal pure profile transition; storage owns exact agenda CAS and idle admission. */
171
+ export function reviseResidentProfile(
172
+ current: ResidentLearningState | undefined,
173
+ input: ResidentProfileUpdate,
174
+ ): ResidentLearningState {
175
+ const checked = z
176
+ .object({
177
+ identity: z.string().trim().min(1).max(4_000).optional(),
178
+ preferences: z
179
+ .array(
180
+ z.object({
181
+ key: name,
182
+ value: z.string().trim().min(1).max(1_000),
183
+ supersedes: label.nullable(),
184
+ }),
185
+ )
186
+ .max(32)
187
+ .optional(),
188
+ evidence: residentLearningEvidenceSchema,
189
+ })
190
+ .parse(input)
191
+ if (checked.identity === undefined && !checked.preferences?.length)
192
+ throw new Error('A profile change must change identity or preferences.')
193
+ if (
194
+ new Set(checked.preferences?.map((p) => p.key)).size !== checked.preferences?.length &&
195
+ checked.preferences !== undefined
196
+ )
197
+ throw new Error('Duplicate preference corrections are not allowed.')
198
+ const evidence = evidenceFor(current, checked.evidence)
199
+ const next = base(current, evidence)
200
+ const preferences = [...next.preferences]
201
+ for (const change of checked.preferences ?? []) {
202
+ const index = preferences.findIndex((p) => p.key === change.key)
203
+ const previous = index >= 0 ? preferences[index] : undefined
204
+ if (change.supersedes !== (previous?.evidence.key ?? null))
205
+ throw new Error('Preference correction does not supersede its current evidence.')
206
+ const updated = { key: change.key, value: change.value, evidence }
207
+ if (index < 0) preferences.push(updated)
208
+ else preferences[index] = updated
209
+ }
210
+ return freezeResidentLearning({
211
+ ...next,
212
+ preferences,
213
+ ...(checked.identity !== undefined ? { identity: { text: checked.identity, evidence } } : {}),
214
+ })
215
+ }
216
+
217
+ /** @experimental Bind evaluation to the exact normalized name, description and body. */
218
+ export function hashResidentSkill(input: ResidentSkillCandidate): string {
219
+ const candidate = candidateSchema.parse(input)
220
+ return createHash('sha256')
221
+ .update(JSON.stringify([candidate.name, candidate.description, candidate.body]))
222
+ .digest('hex')
223
+ }
224
+
225
+ /** Internal pure activation; reuse the kernel's conservative two-round verification gate. */
226
+ export function promoteResidentSkill(
227
+ current: ResidentLearningState | undefined,
228
+ input: ResidentSkillCandidate,
229
+ evaluation: ResidentSkillEvaluation,
230
+ inputEvidence: ResidentLearningEvidence,
231
+ ): ResidentLearningState {
232
+ const candidate = candidateSchema.parse(input)
233
+ const hash = hashResidentSkill(candidate)
234
+ const baselineHash = current?.skills.find((s) => s.name === candidate.name)?.hash ?? 'none'
235
+ const evidence = evidenceFor(current, inputEvidence)
236
+ for (const batch of [evaluation.verification, evaluation.confirmation]) {
237
+ if (!batch) continue
238
+ if (batch.baselineRevision !== baselineHash || batch.candidateRevision !== hash)
239
+ throw new Error('Skill evaluation does not match the active baseline and candidate digests.')
240
+ if (
241
+ batch.baseline.length > 128 ||
242
+ batch.candidate.length > 128 ||
243
+ batch.attributions.length > 128
244
+ )
245
+ throw new Error('Resident skill evaluation is bounded to 64 tasks per round.')
246
+ // Recorded evidence must consistently identify success and complete measurements.
247
+ for (const trial of [...batch.baseline, ...batch.candidate]) {
248
+ if (
249
+ trial.result.passed !== (trial.result.status === 'passed') ||
250
+ (trial.result.status === 'passed' && trial.result.failedGates?.length)
251
+ )
252
+ throw new Error('Inconsistent recorded skill evaluation outcome.')
253
+ }
254
+ }
255
+ const review = reviewHarnessCandidate(evaluation.verification, evaluation.confirmation)
256
+ if (review.decision !== 'accept')
257
+ throw new Error(`Resident skill promotion ${review.decision}: ${review.reason}`)
258
+ const skill: ResidentLearnedSkill = {
259
+ ...candidate,
260
+ hash,
261
+ evidence,
262
+ verification: {
263
+ baselineHash,
264
+ candidateHash: hash,
265
+ evidenceDigest: createHash('sha256').update(JSON.stringify(evaluation)).digest('hex'),
266
+ verificationTasks: review.verification.tasks.length,
267
+ confirmationTasks: review.confirmation?.tasks.length ?? 0,
268
+ },
269
+ }
270
+ const next = base(current, evidence)
271
+ return freezeResidentLearning({
272
+ ...next,
273
+ skills: [...next.skills.filter((s) => s.name !== candidate.name), skill],
274
+ })
275
+ }
276
+
277
+ /** Internal rollback appends a new learning version while preserving current profile values. */
278
+ export function restoreResidentSkill(
279
+ current: ResidentLearningState | undefined,
280
+ historical: ResidentLearningState | undefined,
281
+ skillName: string,
282
+ inputEvidence: ResidentLearningEvidence,
283
+ ): ResidentLearningState {
284
+ name.parse(skillName)
285
+ if (!current?.skills.some((s) => s.name === skillName))
286
+ throw new Error('No active resident skill to roll back.')
287
+ const next = base(current, evidenceFor(current, inputEvidence))
288
+ const previous = historical
289
+ ? freezeResidentLearning(historical).skills.find((s) => s.name === skillName)
290
+ : undefined
291
+ return freezeResidentLearning({
292
+ ...next,
293
+ skills: [...next.skills.filter((s) => s.name !== skillName), ...(previous ? [previous] : [])],
294
+ })
295
+ }
296
+
297
+ /** @experimental Exact skill selection and a character cap, independent of provider token budgets. */
298
+ export interface ResidentLearningProjectionOptions {
299
+ readonly maxChars: number
300
+ readonly skillNames: readonly string[]
301
+ }
302
+
303
+ /** @experimental Omitted entries are counted; no partial skill instruction is emitted. */
304
+ export interface ResidentLearningProjection {
305
+ readonly text: string
306
+ readonly revision: number | null
307
+ readonly includedSkills: readonly string[]
308
+ readonly omitted: number
309
+ }
310
+
311
+ /** @experimental Bounded context data; never registers tools, loads files or executes a skill. */
312
+ export function projectResidentLearning(
313
+ current: ResidentLearningState | undefined,
314
+ options: ResidentLearningProjectionOptions,
315
+ ): ResidentLearningProjection {
316
+ if (!Number.isSafeInteger(options.maxChars) || options.maxChars < 0 || options.maxChars > 64_000)
317
+ throw new TypeError('Resident learning context requires maxChars between 0 and 64000.')
318
+ const selected = z.array(name).max(16).parse(options.skillNames)
319
+ if (new Set(selected).size !== selected.length) throw new Error('Duplicate selected skill names.')
320
+ if (!current)
321
+ return Object.freeze({
322
+ text: '',
323
+ revision: null,
324
+ includedSkills: Object.freeze([]),
325
+ omitted: selected.length,
326
+ })
327
+ const state = freezeResidentLearning(current)
328
+ const pieces: string[] = []
329
+ const includedSkills: string[] = []
330
+ let omitted = 0
331
+ let length = 0
332
+ const add = (value: unknown): boolean => {
333
+ const part = JSON.stringify(value)
334
+ const size = part.length + (pieces.length ? 1 : 0)
335
+ if (length + size > options.maxChars) {
336
+ omitted++
337
+ return false
338
+ }
339
+ pieces.push(part)
340
+ length += size
341
+ return true
342
+ }
343
+ if (state.identity) add({ kind: 'self-description', ...state.identity })
344
+ for (const preference of state.preferences) add({ kind: 'preference', ...preference })
345
+ for (const skillName of selected) {
346
+ const skill = state.skills.find((s) => s.name === skillName)
347
+ if (!skill) {
348
+ omitted++
349
+ continue
350
+ }
351
+ if (
352
+ add({
353
+ kind: 'evaluated-guidance',
354
+ name: skill.name,
355
+ description: skill.description,
356
+ body: skill.body,
357
+ hash: skill.hash,
358
+ evidence: skill.evidence,
359
+ })
360
+ )
361
+ includedSkills.push(skill.name)
362
+ }
363
+ return Object.freeze({
364
+ text: pieces.join('\n'),
365
+ revision: state.revision,
366
+ includedSkills: Object.freeze(includedSkills),
367
+ omitted,
368
+ })
369
+ }
@@ -0,0 +1,101 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises'
2
+ import {
3
+ ResidentConflictError,
4
+ type ResidentDecision,
5
+ type ResidentExecutionStore,
6
+ type ResidentState,
7
+ } from './store.js'
8
+
9
+ /** @experimental Developer-owned step; bind providers, tools and budgets in the host. */
10
+ export type ResidentStep = (state: ResidentState, signal: AbortSignal) => Promise<ResidentDecision>
11
+
12
+ /** @experimental No model is invoked for an idle, terminal or unresolved resident. */
13
+ export type ResidentStepResult =
14
+ | { status: 'settled'; state: ResidentState }
15
+ | {
16
+ status: 'idle'
17
+ reason: 'not-due' | 'terminal' | 'unresolved' | 'contended'
18
+ state: ResidentState
19
+ }
20
+
21
+ /** @experimental A failed/aborted callback leaves its durable claim unresolved. */
22
+ export async function stepResident(
23
+ store: ResidentExecutionStore,
24
+ step: ResidentStep,
25
+ signal: AbortSignal,
26
+ now: () => number = Date.now,
27
+ ): Promise<ResidentStepResult> {
28
+ signal.throwIfAborted()
29
+ const state = await store.read()
30
+ signal.throwIfAborted()
31
+ if (!state) throw new Error('Create the resident before running it.')
32
+ if (state.phase === 'running') return { status: 'idle', reason: 'unresolved', state }
33
+ if (state.phase !== 'waiting') return { status: 'idle', reason: 'terminal', state }
34
+ if (state.wakeAt === null || state.wakeAt > now())
35
+ return { status: 'idle', reason: 'not-due', state }
36
+ let claim: ResidentState
37
+ try {
38
+ claim = await store.claim(state, now())
39
+ } catch (error) {
40
+ if (!(error instanceof ResidentConflictError)) throw error
41
+ return {
42
+ status: 'idle',
43
+ reason: 'contended',
44
+ state: (await store.read()) ?? state,
45
+ }
46
+ }
47
+ signal.throwIfAborted()
48
+ const decision = await step(claim, signal)
49
+ signal.throwIfAborted()
50
+ return {
51
+ status: 'settled',
52
+ state: await store.settle(claim, decision, now()),
53
+ }
54
+ }
55
+
56
+ /** @experimental Bounded local driver; not a daemon, process recovery service or notification transport. */
57
+ export interface ResidentLoopOptions {
58
+ readonly store: ResidentExecutionStore
59
+ readonly step: ResidentStep
60
+ readonly signal: AbortSignal
61
+ /** Required finite admission cap for this invocation, independent of provider token limits. */
62
+ readonly maxSteps: number
63
+ /** Maximum single idle wait, default 60 seconds. Longer waits return control to the host. */
64
+ readonly maxIdleMs?: number
65
+ }
66
+
67
+ /**
68
+ * @experimental Continue a pursuit without a new user message. Null wake times
69
+ * and terminal states stop immediately; cancellation also interrupts idle waits.
70
+ * An abort cannot undo callback effects or forcibly stop a callback ignoring its signal.
71
+ */
72
+ export async function runResident(options: ResidentLoopOptions): Promise<ResidentStepResult> {
73
+ const maxIdleMs = options.maxIdleMs ?? 60_000
74
+ if (
75
+ !Number.isSafeInteger(options.maxSteps) ||
76
+ options.maxSteps < 1 ||
77
+ !Number.isSafeInteger(maxIdleMs) ||
78
+ maxIdleMs < 0 ||
79
+ maxIdleMs > 2_147_483_647
80
+ ) {
81
+ throw new TypeError(
82
+ 'Resident loop requires positive finite maxSteps and bounded nonnegative maxIdleMs.',
83
+ )
84
+ }
85
+ let admitted = 0
86
+ while (true) {
87
+ const result = await stepResident(options.store, options.step, options.signal)
88
+ if (result.status === 'settled') admitted++
89
+ if (admitted >= options.maxSteps) return result
90
+ const { state } = result
91
+ if (
92
+ state.phase !== 'waiting' ||
93
+ state.wakeAt === null ||
94
+ (result.status === 'idle' && result.reason !== 'not-due')
95
+ )
96
+ return result
97
+ const delay = Math.max(0, state.wakeAt - Date.now())
98
+ if (delay > maxIdleMs) return result
99
+ await sleep(Math.max(1, delay), undefined, { signal: options.signal })
100
+ }
101
+ }