@elizaos/plugin-scheduling 2.0.3-beta.5 → 2.0.3-beta.7
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/anchors/anchor-registry.d.ts +33 -0
- package/dist/anchors/anchor-registry.d.ts.map +1 -0
- package/dist/anchors/anchor-registry.js +129 -0
- package/dist/anchors/anchor-registry.js.map +1 -0
- package/dist/dispatch-types.d.ts +28 -0
- package/dist/dispatch-types.d.ts.map +1 -0
- package/dist/dispatch-types.js +1 -0
- package/dist/dispatch-types.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +17 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +8 -0
- package/dist/plugin.js.map +1 -0
- package/dist/scheduled-task/completion-check-registry.d.ts +19 -0
- package/dist/scheduled-task/completion-check-registry.d.ts.map +1 -0
- package/dist/scheduled-task/completion-check-registry.js +113 -0
- package/dist/scheduled-task/completion-check-registry.js.map +1 -0
- package/dist/scheduled-task/consolidation-policy.d.ts +51 -0
- package/dist/scheduled-task/consolidation-policy.d.ts.map +1 -0
- package/dist/scheduled-task/consolidation-policy.js +154 -0
- package/dist/scheduled-task/consolidation-policy.js.map +1 -0
- package/dist/scheduled-task/due.d.ts +19 -0
- package/dist/scheduled-task/due.d.ts.map +1 -0
- package/dist/scheduled-task/due.js +349 -0
- package/dist/scheduled-task/due.js.map +1 -0
- package/dist/scheduled-task/escalation.d.ts +55 -0
- package/dist/scheduled-task/escalation.d.ts.map +1 -0
- package/dist/scheduled-task/escalation.js +99 -0
- package/dist/scheduled-task/escalation.js.map +1 -0
- package/dist/scheduled-task/gate-registry.d.ts +18 -0
- package/dist/scheduled-task/gate-registry.d.ts.map +1 -0
- package/dist/scheduled-task/gate-registry.js +244 -0
- package/dist/scheduled-task/gate-registry.js.map +1 -0
- package/dist/scheduled-task/index.d.ts +20 -0
- package/dist/scheduled-task/index.d.ts.map +1 -0
- package/dist/scheduled-task/index.js +83 -0
- package/dist/scheduled-task/index.js.map +1 -0
- package/dist/scheduled-task/next-fire-at.d.ts +40 -0
- package/dist/scheduled-task/next-fire-at.d.ts.map +1 -0
- package/dist/scheduled-task/next-fire-at.js +202 -0
- package/dist/scheduled-task/next-fire-at.js.map +1 -0
- package/dist/scheduled-task/runner.d.ts +263 -0
- package/dist/scheduled-task/runner.d.ts.map +1 -0
- package/dist/scheduled-task/runner.js +721 -0
- package/dist/scheduled-task/runner.js.map +1 -0
- package/dist/scheduled-task/state-log.d.ts +56 -0
- package/dist/scheduled-task/state-log.d.ts.map +1 -0
- package/dist/scheduled-task/state-log.js +87 -0
- package/dist/scheduled-task/state-log.js.map +1 -0
- package/dist/scheduled-task/types.d.ts +368 -0
- package/dist/scheduled-task/types.d.ts.map +1 -0
- package/dist/scheduled-task/types.js +14 -0
- package/dist/scheduled-task/types.js.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/scheduled-task/runner.ts"],"sourcesContent":["/**\n * ScheduledTaskRunner.\n *\n * Cross-agent invariants enforced here:\n * - The runner does NOT pattern-match on `promptInstructions`.\n * - `acknowledged` is non-terminal; `pipeline.onComplete` only fires on\n * `completed`.\n * - Snooze RESETS the ladder.\n * - Global pause skips tasks with `respectsGlobalPause: true`.\n * - `shouldFire` is always an array; empty / missing arrays are treated as\n * \"no gates → allow\".\n * - `idempotencyKey` deduplicates schedules.\n * - `pipeline.onSkip` wins over `completionCheck.followupAfterMinutes` when\n * both are set.\n */\n\nimport type { DispatchResult } from \"../dispatch-types.js\";\nimport type { CompletionCheckRegistry } from \"./completion-check-registry.js\";\nimport type {\n AnchorRegistry,\n ConsolidationRegistry,\n} from \"./consolidation-policy.js\";\nimport {\n type EscalationLadderRegistry,\n resetLadderForSnooze,\n resolveEffectiveLadder,\n} from \"./escalation.js\";\nimport type { TaskGateRegistry } from \"./gate-registry.js\";\nimport { computeNextFireAt } from \"./next-fire-at.js\";\nimport { createStateLogger, type ScheduledTaskLogStore } from \"./state-log.js\";\nimport {\n type ActivitySignalBusView,\n APPROVAL_DEFAULT_FOLLOWUP_AFTER_MINUTES,\n type CompletionCheckContext,\n DEFAULT_TASK_EXECUTION_PROFILE,\n type GateDecision,\n type GateEvaluationContext,\n type GlobalPauseView,\n type OwnerFactsView,\n type ScheduledTask,\n type ScheduledTaskFilter,\n type ScheduledTaskRef,\n type ScheduledTaskRunner,\n type ScheduledTaskState,\n type ScheduledTaskVerb,\n type SubjectStoreView,\n TASK_EXECUTION_PROFILES,\n type TaskExecutionProfile,\n type TerminalState,\n} from \"./types.js\";\n\n/**\n * Typed error thrown by `runner.schedule()` when an `escalation.steps[].channelKey`\n * does not match a registered channel in the host runtime's `ChannelRegistry`.\n * The runner stays decoupled from the channel registry implementation; the\n * caller injects a `channelKeys()` lookup via {@link ScheduledTaskRunnerDeps}.\n */\nexport class ChannelKeyError extends Error {\n readonly code = \"channel_key_unknown\";\n constructor(\n readonly channelKey: string,\n readonly available: readonly string[],\n ) {\n super(\n `escalation.steps[].channelKey \"${channelKey}\" is not registered (registered: ${available.join(\", \") || \"<none>\"})`,\n );\n this.name = \"ChannelKeyError\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Store interface — DB-backed in production; in-memory in unit tests.\n// ---------------------------------------------------------------------------\n\n/**\n * Options the runner passes to `store.upsert` to keep the indexed\n * `next_fire_at` column in sync with the task's current trigger and state.\n *\n * The store does not compute this itself — the runner computes the value\n * using the active anchor / owner-facts / now references and forwards it\n * here. The repository writes a Postgres `timestamp with time zone`\n * (NULL for triggers without a wall-clock fire time).\n */\nexport interface ScheduledTaskUpsertOptions {\n nextFireAtIso: string | null;\n}\n\n/**\n * Outcome of the atomic fire-claim. Exactly one parallel call resolves to\n * `\"fired\"` for a given `(taskId, status=\"scheduled\")` row; concurrent\n * callers see `\"raced\"` because the UPDATE … WHERE status='scheduled' clause\n * matches zero rows after the first wins.\n *\n * `task` on the `\"fired\"` branch carries the post-claim state (status =\n * \"fired\", `firedAt` set to the claim instant, `nextFireAt` cleared so the\n * scheduler tick will not re-pick it up before the next mutation).\n */\nexport type ScheduledTaskClaimResult =\n | { kind: \"fired\"; task: ScheduledTask }\n | { kind: \"raced\" };\n\nexport interface ScheduledTaskStore {\n upsert(\n task: ScheduledTask,\n options?: ScheduledTaskUpsertOptions,\n ): Promise<void>;\n /**\n * Atomically transition a row from `state.status === \"scheduled\"` to\n * `\"fired\"`, returning the resulting row. Returns `{ kind: \"raced\" }`\n * when zero rows matched — either because the task is already past\n * `scheduled` (another tick claimed it) or the id no longer exists.\n *\n * The store is the only place where the read-mutate-write becomes\n * atomic; the runner's previous read-then-upsert pattern was racy\n * across parallel ticks. See `LifeOpsRepository.claimScheduledTaskForFire`.\n */\n claimForFire(args: {\n taskId: string;\n firedAtIso: string;\n }): Promise<ScheduledTaskClaimResult>;\n get(taskId: string): Promise<ScheduledTask | null>;\n findByIdempotencyKey(key: string): Promise<ScheduledTask | null>;\n list(filter?: ScheduledTaskFilter): Promise<ScheduledTask[]>;\n delete(taskId: string): Promise<void>;\n}\n\nexport function createInMemoryScheduledTaskStore(): ScheduledTaskStore {\n const map = new Map<string, ScheduledTask>();\n return {\n async upsert(task) {\n map.set(task.taskId, structuredClone(task));\n },\n async claimForFire({ taskId, firedAtIso }) {\n const existing = map.get(taskId);\n if (existing?.state.status !== \"scheduled\") {\n return { kind: \"raced\" };\n }\n const next: ScheduledTask = structuredClone(existing);\n next.state.status = \"fired\";\n next.state.firedAt = firedAtIso;\n map.set(taskId, next);\n return { kind: \"fired\", task: structuredClone(next) };\n },\n async get(taskId) {\n const found = map.get(taskId);\n return found ? structuredClone(found) : null;\n },\n async findByIdempotencyKey(key) {\n for (const t of map.values()) {\n if (t.idempotencyKey === key) {\n return structuredClone(t);\n }\n }\n return null;\n },\n async list(filter) {\n let view = Array.from(map.values()).map((t) => structuredClone(t));\n if (!filter) return view;\n if (filter.kind) view = view.filter((t) => t.kind === filter.kind);\n if (filter.status) {\n const allowed = Array.isArray(filter.status)\n ? new Set(filter.status)\n : new Set([filter.status]);\n view = view.filter((t) => allowed.has(t.state.status));\n }\n if (filter.subject) {\n view = view.filter(\n (t) =>\n t.subject?.kind === filter.subject?.kind &&\n t.subject?.id === filter.subject?.id,\n );\n }\n if (filter.source) view = view.filter((t) => t.source === filter.source);\n if (filter.firedSince) {\n view = view.filter(\n (t) =>\n typeof t.state.firedAt === \"string\" &&\n t.state.firedAt >= (filter.firedSince ?? \"\"),\n );\n }\n if (filter.ownerVisibleOnly) view = view.filter((t) => t.ownerVisible);\n return view;\n },\n async delete(taskId) {\n map.delete(taskId);\n },\n };\n}\n\nexport interface ScheduledTaskDispatchRecord {\n taskId: string;\n firedAtIso: string;\n channelKey: string;\n intensity?: \"soft\" | \"normal\" | \"urgent\";\n promptInstructions: string;\n contextRequest: ScheduledTask[\"contextRequest\"];\n consolidationBatchId?: string;\n output?: ScheduledTask[\"output\"];\n}\n\nexport interface ScheduledTaskDispatcher {\n dispatch(\n record: ScheduledTaskDispatchRecord,\n ): Promise<DispatchResult | undefined>;\n}\n\n/**\n * Test-only no-op dispatcher. Production code MUST inject\n * `createProductionScheduledTaskDispatcher` via runtime-wiring; the runner\n * factory requires a dispatcher and there is no silent fallback. Exported only\n * so tests can construct a runner without touching the channel layer.\n *\n * @internal\n */\nexport const TestNoopScheduledTaskDispatcher: ScheduledTaskDispatcher = {\n async dispatch() {\n /* intentional no-op for tests */\n },\n};\n\n// ---------------------------------------------------------------------------\n// Runner deps (factory)\n// ---------------------------------------------------------------------------\n\nexport interface ScheduledTaskRunnerDeps {\n agentId: string;\n store: ScheduledTaskStore;\n logStore: ScheduledTaskLogStore;\n gates: TaskGateRegistry;\n completionChecks: CompletionCheckRegistry;\n ladders: EscalationLadderRegistry;\n anchors: AnchorRegistry;\n consolidation: ConsolidationRegistry;\n ownerFacts: () => OwnerFactsView | Promise<OwnerFactsView>;\n globalPause: GlobalPauseView;\n activity: ActivitySignalBusView;\n subjectStore: SubjectStoreView;\n dispatcher: ScheduledTaskDispatcher;\n /**\n * Lookup of registered `ChannelRegistry` keys. When supplied, `schedule()`\n * validates each `escalation.steps[].channelKey` against this set and\n * throws {@link ChannelKeyError} on miss. Decoupled from the channels\n * module to keep the spine free of channel-layer dependencies.\n */\n channelKeys?: () => ReadonlySet<string>;\n /**\n * Returns the set of `TaskExecutionProfile` values the current host can\n * actually run. The runner consults this AFTER the atomic fire-claim but\n * BEFORE dispatch: if `task.executionProfile` is not in the set, dispatch\n * is rewritten to `notify-only` and a `\"substituted\"` state-log row is\n * recorded. Default (when not provided): all four profiles available —\n * appropriate for tests and Node desktop. Mobile / Capacitor callers\n * inject a real probe from\n * `@elizaos/app-core/services/local-inference/host-capabilities`.\n */\n hostCapabilities?: () => ReadonlySet<TaskExecutionProfile>;\n /** Override for tests. */\n newTaskId?: () => string;\n /** Override for tests. */\n now?: () => Date;\n}\n\n/**\n * Default capability probe — assumes a full host (test/Node). Mobile callers\n * inject a real probe so heavy tasks substitute to notify-only on incapable\n * hosts instead of silently failing under a 30s wake budget.\n */\nconst ALL_PROFILES_AVAILABLE: ReadonlySet<TaskExecutionProfile> = new Set(\n TASK_EXECUTION_PROFILES,\n);\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction defaultTaskIdGenerator(): string {\n // Stable enough across runtimes; the DB is authoritative for uniqueness.\n return `st_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction isTerminal(status: ScheduledTask[\"state\"][\"status\"]): boolean {\n return (\n status === \"completed\" ||\n status === \"skipped\" ||\n status === \"expired\" ||\n status === \"failed\" ||\n status === \"dismissed\"\n );\n}\n\nfunction isRecurringTrigger(trigger: ScheduledTask[\"trigger\"]): boolean {\n return (\n trigger.kind === \"cron\" ||\n trigger.kind === \"interval\" ||\n trigger.kind === \"relative_to_anchor\" ||\n trigger.kind === \"during_window\"\n );\n}\n\nfunction setEscalationCursor(\n task: ScheduledTask,\n cursor: { stepIndex: number; lastDispatchedAt: string },\n): void {\n task.metadata = {\n ...(task.metadata ?? {}),\n escalationCursor: { ...cursor },\n };\n}\n\nfunction clearEscalationCursor(task: ScheduledTask): void {\n if (task.metadata && \"escalationCursor\" in task.metadata) {\n const next = { ...task.metadata };\n delete (next as Record<string, unknown>).escalationCursor;\n task.metadata = next;\n }\n}\n\nfunction stripServerManaged(\n task: ScheduledTask,\n): Omit<ScheduledTask, \"taskId\" | \"state\"> {\n const { taskId: _id, state: _state, ...rest } = task;\n return rest;\n}\n\n// ---------------------------------------------------------------------------\n// Runner factory\n// ---------------------------------------------------------------------------\n\n/**\n * Public read view of `metadata.escalationCursor`.\n *\n * The cursor is the runner's persistence channel for the snooze-resets-ladder\n * rule. Consumers that need to surface \"currently on step N of escalation\"\n * read it through {@link ScheduledTaskRunnerExtras.getEscalationCursor} so\n * they don't reach into the metadata namespace directly.\n *\n * - `stepIndex` follows the {@link EscalationCursor} convention: `-1` means\n * the task was fired but no escalation step has been dispatched yet;\n * `0..n` is the index into the resolved ladder's `steps`.\n * - `lastFiredAt` is the ISO of the most recent dispatch (or the initial\n * task fire when `stepIndex === -1`).\n * - `channelKey` is resolved from the effective ladder. For `stepIndex === -1`\n * we surface the first step's channel when the ladder has steps, falling\n * back to `\"in_app\"` when the ladder is empty.\n */\nexport interface EscalationCursorView {\n stepIndex: number;\n lastFiredAt: string;\n channelKey: string;\n}\n\n/**\n * Strict result of a single `fire()` attempt. Callers should exhaustively\n * switch on `kind`.\n *\n * - `fired` — the task transitioned to `\"fired\"` (or was deferred via\n * `gate.defer`, reopened for a recurrence, etc.) and the dispatcher ran.\n * `task` is the post-mutation state.\n * - `raced` — another tick atomically claimed this task first. Caller drops\n * the attempt silently; the winning tick's dispatch is authoritative.\n * - `skipped` — the task was skipped without dispatch: global-pause active,\n * a gate denied, or the task was already terminal and not eligible for\n * recurrence refire.\n * - `dispatch_failed` — the atomic claim succeeded but the dispatcher threw.\n * The runner persists the row as `\"failed\"` and writes a failed state-log\n * entry so history does not strand the task as successfully fired.\n */\nexport type ScheduledTaskFireResult =\n | { kind: \"fired\"; task: ScheduledTask }\n | { kind: \"raced\"; taskId: string }\n | { kind: \"skipped\"; task: ScheduledTask; reason: string }\n | { kind: \"dispatch_failed\"; task: ScheduledTask; error: Error };\n\nexport interface ScheduledTaskRunnerExtras {\n /**\n * Convenience wrapper around {@link ScheduledTaskRunnerExtras.fireWithResult}\n * that flattens the discriminated union into a `ScheduledTask`. Returns\n * the post-fire task on `fired` / `skipped` / `dispatch_failed`, and the\n * still-`scheduled` task on `raced` (so legacy callers that re-read see\n * the unmodified row). The strict-fire callsite — `processDueScheduledTasks`\n * — uses `fireWithResult` directly.\n *\n * Exposed for tests so we can assert behavior deterministically without\n * waiting on a real timer, and for legacy actions that only want the\n * task back.\n */\n fire(\n taskId: string,\n args?: { eventPayload?: unknown; allowTerminalRefire?: boolean },\n ): Promise<ScheduledTask>;\n /**\n * Strict fire-attempt. Returns the {@link ScheduledTaskFireResult}\n * discriminated union; callers must exhaustively switch on `kind`. This\n * is the path the scheduler tick uses so the `raced` outcome (another\n * tick claimed the same row first) is observable instead of silently\n * collapsed into a \"fired\" return.\n */\n fireWithResult(\n taskId: string,\n args?: { eventPayload?: unknown; allowTerminalRefire?: boolean },\n ): Promise<ScheduledTaskFireResult>;\n /**\n * Re-evaluate completion for a fired task (e.g. user_replied_within\n * scenarios, late inbounds). The runner consults its registered\n * completion-check and may transition the task to `completed`.\n */\n evaluateCompletion(\n taskId: string,\n signal: {\n acknowledged?: boolean;\n repliedAtIso?: string;\n },\n ): Promise<ScheduledTask>;\n /**\n * Run the nightly rollup pass on the state-log. Default retention is 90\n * days.\n */\n rolloverStateLog(opts?: { retentionDays?: number }): Promise<{\n rolledUp: number;\n deletedRaw: number;\n }>;\n /**\n * Return all gates registered (for the dev-registries endpoint).\n */\n inspectRegistries(): {\n gates: string[];\n completionChecks: string[];\n ladders: string[];\n anchors: string[];\n consolidationPolicies: string[];\n };\n /**\n * Read the public view of `metadata.escalationCursor` for a task. Returns\n * `null` when the task is not found or has no cursor recorded yet.\n */\n getEscalationCursor(taskId: string): Promise<EscalationCursorView | null>;\n}\n\nexport interface ScheduledTaskRunnerHandle\n extends ScheduledTaskRunner,\n ScheduledTaskRunnerExtras {}\n\nexport function createScheduledTaskRunner(\n deps: ScheduledTaskRunnerDeps,\n): ScheduledTaskRunnerHandle {\n const newTaskId = deps.newTaskId ?? defaultTaskIdGenerator;\n const now = deps.now ?? (() => new Date());\n const dispatcher = deps.dispatcher;\n const logger = createStateLogger({\n store: deps.logStore,\n agentId: deps.agentId,\n now,\n });\n\n async function evaluateGates(\n task: ScheduledTask,\n ): Promise<{ decision: GateDecision; gateKind?: string }> {\n const compose = task.shouldFire?.compose ?? \"first_deny\";\n const gates = task.shouldFire?.gates ?? [];\n if (gates.length === 0) {\n return { decision: { kind: \"allow\" } };\n }\n\n const ownerFacts = await deps.ownerFacts();\n const ctx: GateEvaluationContext = {\n task,\n nowIso: now().toISOString(),\n ownerFacts,\n activity: deps.activity,\n subjectStore: deps.subjectStore,\n };\n\n const decisions: Array<{ gateKind: string; decision: GateDecision }> = [];\n for (const gateRef of gates) {\n const contrib = deps.gates.get(gateRef.kind);\n if (!contrib) {\n return {\n gateKind: gateRef.kind,\n decision: {\n kind: \"deny\",\n reason: `unknown gate kind: ${gateRef.kind}`,\n },\n };\n }\n const decision = await contrib.evaluate(task, ctx);\n decisions.push({ gateKind: gateRef.kind, decision });\n\n if (compose === \"first_deny\" && decision.kind !== \"allow\") {\n return { gateKind: gateRef.kind, decision };\n }\n if (compose === \"any\" && decision.kind === \"allow\") {\n return { gateKind: gateRef.kind, decision: { kind: \"allow\" } };\n }\n }\n\n if (compose === \"all\") {\n const denied = decisions.find((d) => d.decision.kind !== \"allow\");\n if (denied) return denied;\n return { decision: { kind: \"allow\" } };\n }\n if (compose === \"any\") {\n // No allow seen.\n const lastDeny = decisions\n .reverse()\n .find((d) => d.decision.kind === \"deny\");\n if (lastDeny) return lastDeny;\n const lastDefer = decisions.find((d) => d.decision.kind === \"defer\");\n if (lastDefer) return lastDefer;\n return {\n decision: { kind: \"deny\", reason: \"any: no gate allowed\" },\n };\n }\n // first_deny: no deny encountered → allow\n return { decision: { kind: \"allow\" } };\n }\n\n async function shouldDeferForGlobalPause(\n task: ScheduledTask,\n ): Promise<{ paused: boolean; reason?: string }> {\n if (task.respectsGlobalPause === false) return { paused: false };\n const pause = await deps.globalPause.current(now());\n if (!pause.active) return { paused: false };\n return {\n paused: true,\n reason: pause.reason ? `global_pause: ${pause.reason}` : \"global_pause\",\n };\n }\n\n async function persist(task: ScheduledTask): Promise<ScheduledTask> {\n const nextFireAtIso = await resolveNextFireAt(task);\n await deps.store.upsert(task, { nextFireAtIso });\n return structuredClone(task);\n }\n\n async function resolveNextFireAt(\n task: ScheduledTask,\n ): Promise<string | null> {\n // Terminal-state rows do not refire (except recurring triggers that get\n // explicitly reopened via `fire({ allowTerminalRefire: true })`). Storing\n // a stale `next_fire_at` would leave the row in the partial-index slice\n // until the next mutation; clearing it keeps the index slim.\n if (isTerminal(task.state.status)) return null;\n const ownerFacts = await deps.ownerFacts();\n return computeNextFireAt(task, {\n now: now(),\n ownerFacts,\n anchors: deps.anchors,\n });\n }\n\n async function schedule(\n input: Omit<ScheduledTask, \"taskId\" | \"state\">,\n ): Promise<ScheduledTask> {\n if (input.idempotencyKey) {\n const existing = await deps.store.findByIdempotencyKey(\n input.idempotencyKey,\n );\n if (existing) return existing;\n }\n\n // A11: channel-key validation against the runtime ChannelRegistry.\n if (deps.channelKeys && input.escalation?.steps) {\n const registered = deps.channelKeys();\n for (const step of input.escalation.steps) {\n if (!registered.has(step.channelKey)) {\n throw new ChannelKeyError(\n step.channelKey,\n Array.from(registered).sort(),\n );\n }\n }\n }\n\n // A7: default `completionCheck.followupAfterMinutes` for approval-kind\n // tasks when the curator did not set one explicitly and pipeline.onSkip\n // is empty (which would otherwise win per §7.4 resolution rule).\n const withApprovalDefaults = applyApprovalCompletionDefault(input);\n\n const initialState: ScheduledTaskState = {\n status: \"scheduled\",\n followupCount: 0,\n };\n const task: ScheduledTask = {\n taskId: newTaskId(),\n ...withApprovalDefaults,\n state: initialState,\n };\n await persist(task);\n await logger.log(task.taskId, \"scheduled\", {\n detail: {\n kind: task.kind,\n priority: task.priority,\n triggerKind: task.trigger.kind,\n },\n });\n if (\n task.completionCheck?.followupAfterMinutes &&\n task.pipeline?.onSkip &&\n task.pipeline.onSkip.length > 0\n ) {\n await logger.log(task.taskId, \"edited\", {\n reason:\n \"validation: pipeline.onSkip overrides completionCheck.followupAfterMinutes\",\n });\n }\n return task;\n }\n\n function applyApprovalCompletionDefault(\n input: Omit<ScheduledTask, \"taskId\" | \"state\">,\n ): Omit<ScheduledTask, \"taskId\" | \"state\"> {\n if (input.kind !== \"approval\") return input;\n const onSkipEmpty =\n !input.pipeline?.onSkip || input.pipeline.onSkip.length === 0;\n if (!onSkipEmpty) return input;\n if (input.completionCheck?.followupAfterMinutes !== undefined) return input;\n const baseCheck = input.completionCheck ?? { kind: \"user_acknowledged\" };\n return {\n ...input,\n completionCheck: {\n ...baseCheck,\n followupAfterMinutes: APPROVAL_DEFAULT_FOLLOWUP_AFTER_MINUTES,\n },\n };\n }\n\n async function list(filter?: ScheduledTaskFilter): Promise<ScheduledTask[]> {\n return deps.store.list(filter);\n }\n\n // -------------------------------------------------------------------------\n // Verb dispatch\n // -------------------------------------------------------------------------\n\n async function applySnooze(\n task: ScheduledTask,\n payload: { minutes?: number; untilIso?: string } | undefined,\n ): Promise<ScheduledTask> {\n const minutes = payload?.minutes;\n const untilIso = payload?.untilIso;\n let newFireAtIso: string;\n if (typeof untilIso === \"string\") {\n newFireAtIso = new Date(untilIso).toISOString();\n } else if (typeof minutes === \"number\" && minutes > 0) {\n newFireAtIso = new Date(now().getTime() + minutes * 60_000).toISOString();\n } else {\n throw new Error(\"snooze: provide minutes or untilIso\");\n }\n const reopenStatus: ScheduledTask[\"state\"][\"status\"] = \"scheduled\";\n task.state.status = reopenStatus;\n task.state.firedAt = newFireAtIso;\n task.state.lastDecisionLog = `snoozed until ${newFireAtIso} (ladder reset)`;\n setEscalationCursor(task, resetLadderForSnooze(newFireAtIso));\n await persist(task);\n await logger.log(task.taskId, \"snoozed\", {\n reason: `until ${newFireAtIso}`,\n detail: { newFireAtIso },\n });\n return task;\n }\n\n async function applySkip(\n task: ScheduledTask,\n payload: { reason?: string } | undefined,\n ): Promise<ScheduledTask> {\n task.state.status = \"skipped\";\n task.state.lastDecisionLog = payload?.reason ?? \"user skipped\";\n await persist(task);\n await logger.log(task.taskId, \"skipped\", {\n reason: payload?.reason ?? \"user skipped\",\n });\n await runPipeline(task, \"skipped\");\n return task;\n }\n\n async function applyComplete(\n task: ScheduledTask,\n payload: { reason?: string } | undefined,\n ): Promise<ScheduledTask> {\n task.state.status = \"completed\";\n task.state.completedAt = now().toISOString();\n task.state.lastDecisionLog = payload?.reason ?? \"completed\";\n await persist(task);\n await logger.log(task.taskId, \"completed\", { reason: payload?.reason });\n await runPipeline(task, \"completed\");\n return task;\n }\n\n async function applyDismiss(\n task: ScheduledTask,\n payload: { reason?: string } | undefined,\n ): Promise<ScheduledTask> {\n task.state.status = \"dismissed\";\n task.state.lastDecisionLog = payload?.reason ?? \"dismissed\";\n await persist(task);\n await logger.log(task.taskId, \"dismissed\", { reason: payload?.reason });\n return task;\n }\n\n async function applyEscalate(\n task: ScheduledTask,\n payload: { force?: boolean } | undefined,\n ): Promise<ScheduledTask> {\n // `escalate` is a manual nudge to the next ladder step. The dispatcher\n // transition is handled inside fire(); we simply mark the task as fired\n // with intensity escalation and write a log row. The actual channel\n // egress happens via the dispatcher when fire() runs.\n task.state.followupCount += 1;\n task.state.lastFollowupAt = now().toISOString();\n task.state.lastDecisionLog = \"escalated\";\n await persist(task);\n await logger.log(task.taskId, \"escalated\", {\n reason: payload?.force ? \"force=true\" : undefined,\n });\n return task;\n }\n\n async function applyAcknowledge(task: ScheduledTask): Promise<ScheduledTask> {\n // §7.6: acknowledged is non-terminal. Pipeline.onComplete does NOT fire.\n task.state.status = \"acknowledged\";\n task.state.acknowledgedAt = now().toISOString();\n task.state.lastDecisionLog = \"acknowledged\";\n await persist(task);\n await logger.log(task.taskId, \"acknowledged\");\n return task;\n }\n\n async function applyEdit(\n task: ScheduledTask,\n payload: Partial<Omit<ScheduledTask, \"taskId\" | \"state\">> | undefined,\n ): Promise<ScheduledTask> {\n if (!payload) return task;\n // Cannot edit through state — that's what verbs are for.\n const banned: Array<keyof ScheduledTask> = [\"taskId\", \"state\"];\n for (const key of banned) {\n if (key in (payload as Record<string, unknown>)) {\n throw new Error(`edit: ${String(key)} is read-only`);\n }\n }\n Object.assign(task, payload);\n await persist(task);\n await logger.log(task.taskId, \"edited\", {\n detail: { keys: Object.keys(payload) },\n });\n return task;\n }\n\n async function applyReopen(\n task: ScheduledTask,\n payload: { reason?: string } | undefined,\n ): Promise<ScheduledTask> {\n if (!isTerminal(task.state.status)) {\n throw new Error(\n `reopen: task ${task.taskId} is not in a terminal state (status=${task.state.status})`,\n );\n }\n // §8.12: late-inbound reopen window default 24h after lastFollowupAt;\n // configurable via metadata.reopenWindowHours.\n const windowHours = (() => {\n const raw = task.metadata?.reopenWindowHours;\n return typeof raw === \"number\" && raw > 0 ? raw : 24;\n })();\n const referenceIso =\n task.state.lastFollowupAt ??\n task.state.firedAt ??\n task.state.completedAt ??\n now().toISOString();\n const expiresMs =\n new Date(referenceIso).getTime() + windowHours * 60 * 60 * 1000;\n if (now().getTime() > expiresMs) {\n throw new Error(\n `reopen: window expired (>${windowHours}h since ${referenceIso})`,\n );\n }\n task.state.status = \"scheduled\";\n task.state.lastDecisionLog = payload?.reason ?? \"reopened\";\n clearEscalationCursor(task);\n await persist(task);\n await logger.log(task.taskId, \"reopened\", { reason: payload?.reason });\n return task;\n }\n\n async function apply(\n taskId: string,\n verb: ScheduledTaskVerb,\n payload?: unknown,\n ): Promise<ScheduledTask> {\n const task = await deps.store.get(taskId);\n if (!task) {\n throw new Error(`apply: task ${taskId} not found`);\n }\n switch (verb) {\n case \"snooze\":\n return applySnooze(\n task,\n payload as { minutes?: number; untilIso?: string },\n );\n case \"skip\":\n return applySkip(task, payload as { reason?: string });\n case \"complete\":\n return applyComplete(task, payload as { reason?: string });\n case \"dismiss\":\n return applyDismiss(task, payload as { reason?: string });\n case \"escalate\":\n return applyEscalate(task, payload as { force?: boolean });\n case \"acknowledge\":\n return applyAcknowledge(task);\n case \"edit\":\n return applyEdit(\n task,\n payload as Partial<Omit<ScheduledTask, \"taskId\" | \"state\">>,\n );\n case \"reopen\":\n return applyReopen(task, payload as { reason?: string });\n default: {\n const exhaustive: never = verb;\n throw new Error(`apply: unknown verb ${String(exhaustive)}`);\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Pipeline propagation\n // -------------------------------------------------------------------------\n\n async function runPipeline(\n parent: ScheduledTask,\n outcome: TerminalState,\n ): Promise<ScheduledTask[]> {\n const refs: ScheduledTaskRef[] | undefined = (() => {\n switch (outcome) {\n case \"completed\":\n return parent.pipeline?.onComplete;\n case \"skipped\":\n return parent.pipeline?.onSkip;\n case \"failed\":\n return parent.pipeline?.onFail;\n // expired / dismissed do not propagate; pipeline.onSkip captures\n // the user-skip case explicitly.\n default:\n return undefined;\n }\n })();\n if (!refs || refs.length === 0) return [];\n const created: ScheduledTask[] = [];\n for (const ref of refs) {\n if (typeof ref === \"string\") {\n const child = await deps.store.get(ref);\n if (child) {\n // Mark the parent linkage on the child for observability.\n child.state.pipelineParentId = parent.taskId;\n await persist(child);\n await logger.log(child.taskId, \"edited\", {\n reason: `pipeline.${outcomeToFieldName(outcome)} parent=${parent.taskId}`,\n });\n created.push(child);\n }\n continue;\n }\n const cloned = structuredClone(ref);\n // Strip server-managed fields if the caller passed a fully-shaped\n // `ScheduledTask`. `schedule()` regenerates them.\n const childInput = stripServerManaged(cloned);\n const fresh = await schedule(childInput);\n fresh.state.pipelineParentId = parent.taskId;\n await persist(fresh);\n created.push(fresh);\n }\n return created;\n }\n\n function outcomeToFieldName(outcome: TerminalState): string {\n switch (outcome) {\n case \"completed\":\n return \"onComplete\";\n case \"skipped\":\n return \"onSkip\";\n case \"failed\":\n return \"onFail\";\n default:\n return outcome;\n }\n }\n\n async function pipeline(\n taskId: string,\n outcome: TerminalState,\n ): Promise<ScheduledTask[]> {\n const task = await deps.store.get(taskId);\n if (!task) throw new Error(`pipeline: task ${taskId} not found`);\n // D12: when callers invoke pipeline(\"failed\") (or any terminal state the\n // runner has not recorded), bring the parent's terminal state into\n // alignment with the dispatched outcome before propagating to children.\n // `apply(\"complete\" | \"skip\")` already writes the matching status, so we\n // only flip when the parent is still live and the outcome differs.\n if (!isTerminal(task.state.status) && task.state.status !== outcome) {\n task.state.status = outcome;\n task.state.lastDecisionLog = `pipeline: ${outcome}`;\n if (outcome === \"completed\" && !task.state.completedAt) {\n task.state.completedAt = now().toISOString();\n }\n await persist(task);\n await logger.log(task.taskId, outcomeToLogTransition(outcome), {\n reason: `pipeline: ${outcome}`,\n });\n }\n return runPipeline(task, outcome);\n }\n\n function outcomeToLogTransition(\n outcome: TerminalState,\n ): \"completed\" | \"skipped\" | \"expired\" | \"failed\" | \"dismissed\" {\n return outcome;\n }\n\n // -------------------------------------------------------------------------\n // Fire / evaluate completion\n // -------------------------------------------------------------------------\n\n async function fire(\n taskId: string,\n args?: { eventPayload?: unknown; allowTerminalRefire?: boolean },\n ): Promise<ScheduledTask> {\n const result = await fireWithResult(taskId, args);\n switch (result.kind) {\n case \"fired\":\n case \"skipped\":\n case \"dispatch_failed\":\n return result.task;\n case \"raced\": {\n // The caller did not opt in to seeing race outcomes; re-read the\n // row the winning tick committed so observers still see a coherent\n // post-claim ScheduledTask instead of stale pre-claim state.\n const winner = await deps.store.get(result.taskId);\n if (winner) return winner;\n throw new Error(`fire: task ${result.taskId} not found after race`);\n }\n default: {\n const _exhaustive: never = result;\n throw new Error(\"fire: unreachable\");\n }\n }\n }\n\n async function fireWithResult(\n taskId: string,\n args?: { eventPayload?: unknown; allowTerminalRefire?: boolean },\n ): Promise<ScheduledTaskFireResult> {\n const task = await deps.store.get(taskId);\n if (!task) throw new Error(`fire: task ${taskId} not found`);\n if (isTerminal(task.state.status)) {\n const canRefire =\n args?.allowTerminalRefire === true &&\n task.state.status !== \"dismissed\" &&\n isRecurringTrigger(task.trigger);\n if (!canRefire) {\n // Idempotent — already settled; report skipped so callers do not\n // double-count this as a fresh fire.\n return {\n kind: \"skipped\",\n task,\n reason: `terminal:${task.state.status}`,\n };\n }\n task.state.status = \"scheduled\";\n delete task.state.acknowledgedAt;\n delete task.state.completedAt;\n task.state.lastDecisionLog = \"recurrence refire\";\n clearEscalationCursor(task);\n // Flip the row back to `scheduled` so the atomic claim below has\n // something to match. The claim writes `firedAt` itself.\n await persist(task);\n }\n\n await logger.log(task.taskId, \"fire_attempt\", {\n detail: { eventPayload: args?.eventPayload ? \"present\" : \"absent\" },\n });\n\n // Global-pause check.\n const pause = await shouldDeferForGlobalPause(task);\n if (pause.paused) {\n task.state.status = \"skipped\";\n task.state.lastDecisionLog = pause.reason ?? \"global_pause\";\n await persist(task);\n await logger.log(task.taskId, \"skipped\", {\n reason: pause.reason ?? \"global_pause\",\n });\n return {\n kind: \"skipped\",\n task,\n reason: pause.reason ?? \"global_pause\",\n };\n }\n\n // Gate check.\n const gateOutcome = await evaluateGates(task);\n if (gateOutcome.decision.kind === \"deny\") {\n task.state.status = \"skipped\";\n task.state.lastDecisionLog = `${gateOutcome.gateKind ?? \"gate\"}: ${gateOutcome.decision.reason}`;\n await persist(task);\n await logger.log(task.taskId, \"skipped\", {\n reason: task.state.lastDecisionLog,\n });\n await runPipeline(task, \"skipped\");\n return {\n kind: \"skipped\",\n task,\n reason: task.state.lastDecisionLog,\n };\n }\n if (gateOutcome.decision.kind === \"defer\") {\n const offset =\n \"offsetMinutes\" in gateOutcome.decision.until\n ? gateOutcome.decision.until.offsetMinutes\n : Math.max(\n 1,\n Math.round(\n (new Date(gateOutcome.decision.until.atIso).getTime() -\n now().getTime()) /\n 60_000,\n ),\n );\n task.state.lastDecisionLog = `${gateOutcome.gateKind ?? \"gate\"}: deferred ${offset}m (${gateOutcome.decision.reason})`;\n const newFireMs = now().getTime() + offset * 60_000;\n task.state.firedAt = new Date(newFireMs).toISOString();\n await persist(task);\n await logger.log(task.taskId, \"snoozed\", {\n reason: `gate-defer: ${gateOutcome.decision.reason}`,\n detail: { offsetMinutes: offset },\n });\n return {\n kind: \"skipped\",\n task,\n reason: `gate-defer:${gateOutcome.decision.reason}`,\n };\n }\n\n // Allow → atomic claim. The store does UPDATE … WHERE status='scheduled'\n // RETURNING * so exactly one parallel caller can transition `scheduled`\n // → `fired`. Concurrent ticks see `kind: \"raced\"` and bail.\n const fireAtIso = now().toISOString();\n const claim = await deps.store.claimForFire({\n taskId: task.taskId,\n firedAtIso: fireAtIso,\n });\n if (claim.kind === \"raced\") {\n return { kind: \"raced\", taskId: task.taskId };\n }\n const claimed = claim.task;\n claimed.state.lastDecisionLog = \"fired\";\n setEscalationCursor(claimed, {\n stepIndex: -1,\n lastDispatchedAt: fireAtIso,\n });\n // Persist the post-claim metadata (escalationCursor, lastDecisionLog).\n // `persist` recomputes `next_fire_at` from the now-`fired` row.\n await persist(claimed);\n await logger.log(claimed.taskId, \"fired\");\n\n // Host-capability gate. If the host can't satisfy the task's profile,\n // rewrite the dispatch channel to `in_app` (notify-only) and record a\n // \"substituted\" log row. The substitution does not change the task's\n // status — it merely shifts the wire-out mechanism so a `bg-heavy-fgs`\n // task on iOS becomes a banner the user can tap.\n const hostCaps = deps.hostCapabilities?.() ?? ALL_PROFILES_AVAILABLE;\n const taskProfile =\n claimed.executionProfile ?? DEFAULT_TASK_EXECUTION_PROFILE;\n const substituted = !hostCaps.has(taskProfile);\n const dispatchChannelKey = substituted ? \"in_app\" : pickChannelKey(claimed);\n if (substituted) {\n await logger.log(claimed.taskId, \"substituted\", {\n reason: \"host_incapable\",\n detail: {\n originalProfile: taskProfile,\n substituteProfile: \"notify-only\" satisfies TaskExecutionProfile,\n availableProfiles: Array.from(hostCaps),\n },\n });\n }\n\n let dispatchResult: DispatchResult | undefined;\n try {\n dispatchResult = await dispatcher.dispatch({\n taskId: claimed.taskId,\n firedAtIso: fireAtIso,\n channelKey: dispatchChannelKey,\n intensity: pickIntensity(claimed),\n promptInstructions: claimed.promptInstructions,\n contextRequest: claimed.contextRequest,\n output: claimed.output,\n });\n } catch (error) {\n const wrapped = error instanceof Error ? error : new Error(String(error));\n const reason = `dispatch_failed: ${wrapped.message}`;\n claimed.state.status = \"failed\";\n claimed.state.lastDecisionLog = reason;\n claimed.metadata = {\n ...(claimed.metadata ?? {}),\n lastDispatchError: {\n name: wrapped.name,\n message: wrapped.message,\n },\n };\n await persist(claimed);\n await logger.log(claimed.taskId, \"failed\", {\n reason,\n detail: {\n errorName: wrapped.name,\n message: wrapped.message,\n },\n });\n await runPipeline(claimed, \"failed\");\n return { kind: \"dispatch_failed\", task: claimed, error: wrapped };\n }\n if (dispatchResult) {\n claimed.metadata = {\n ...(claimed.metadata ?? {}),\n lastDispatchResult: dispatchResult,\n };\n await persist(claimed);\n }\n return { kind: \"fired\", task: claimed };\n }\n\n function pickChannelKey(task: ScheduledTask): string {\n if (\n task.output?.destination === \"channel\" &&\n typeof task.output.target === \"string\"\n ) {\n const [channelKey] = task.output.target.split(\":\", 1);\n if (channelKey) return channelKey;\n }\n if (task.escalation?.steps && task.escalation.steps.length > 0) {\n return task.escalation.steps[0]?.channelKey ?? \"in_app\";\n }\n // Priority does not currently influence default channel — the production\n // dispatcher always routes \"in_app\" through the event service. If\n // priority-based routing is added later, branch here.\n return \"in_app\";\n }\n\n function pickIntensity(task: ScheduledTask): \"soft\" | \"normal\" | \"urgent\" {\n if (task.priority === \"high\") return \"urgent\";\n if (task.priority === \"medium\") return \"normal\";\n return \"soft\";\n }\n\n async function evaluateCompletion(\n taskId: string,\n signal: { acknowledged?: boolean; repliedAtIso?: string },\n ): Promise<ScheduledTask> {\n const task = await deps.store.get(taskId);\n if (!task) throw new Error(`evaluateCompletion: task ${taskId} not found`);\n if (!task.completionCheck) return task;\n const contrib = deps.completionChecks.get(task.completionCheck.kind);\n if (!contrib) return task;\n const ownerFacts = await deps.ownerFacts();\n const ctx: CompletionCheckContext = {\n task,\n nowIso: now().toISOString(),\n ownerFacts,\n activity: deps.activity,\n subjectStore: deps.subjectStore,\n acknowledged: signal.acknowledged === true,\n repliedSinceFiredAt: signal.repliedAtIso\n ? { atIso: signal.repliedAtIso }\n : undefined,\n };\n const completed = await contrib.shouldComplete(task, ctx);\n if (!completed) return task;\n return applyComplete(task, { reason: `completion-check:${contrib.kind}` });\n }\n\n async function rolloverStateLog(opts?: { retentionDays?: number }) {\n const days = opts?.retentionDays ?? 90;\n const olderThanIso = new Date(\n now().getTime() - days * 24 * 60 * 60 * 1000,\n ).toISOString();\n return deps.logStore.rollupOlderThan({\n agentId: deps.agentId,\n olderThanIso,\n });\n }\n\n function inspectRegistries() {\n return {\n gates: deps.gates.list().map((g) => g.kind),\n completionChecks: deps.completionChecks.list().map((c) => c.kind),\n ladders: deps.ladders.list().map((l) => l.ladderKey),\n anchors: deps.anchors.list().map((a) => a.anchorKey),\n consolidationPolicies: deps.consolidation.list().map((p) => p.anchorKey),\n };\n }\n\n async function getEscalationCursor(\n taskId: string,\n ): Promise<EscalationCursorView | null> {\n const task = await deps.store.get(taskId);\n if (!task) return null;\n const raw = task.metadata?.escalationCursor;\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return null;\n const cursor = raw as { stepIndex?: unknown; lastDispatchedAt?: unknown };\n if (\n typeof cursor.stepIndex !== \"number\" ||\n typeof cursor.lastDispatchedAt !== \"string\"\n ) {\n return null;\n }\n const ladder = resolveEffectiveLadder(task, deps.ladders);\n const stepIndex = cursor.stepIndex;\n const channelKey =\n stepIndex >= 0 && stepIndex < ladder.steps.length\n ? (ladder.steps[stepIndex]?.channelKey ?? \"in_app\")\n : (ladder.steps[0]?.channelKey ?? \"in_app\");\n return {\n stepIndex,\n lastFiredAt: cursor.lastDispatchedAt,\n channelKey,\n };\n }\n\n return {\n schedule,\n list,\n apply,\n pipeline,\n fire,\n fireWithResult,\n evaluateCompletion,\n rolloverStateLog,\n inspectRegistries,\n getEscalationCursor,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State-log writer.
|
|
3
|
+
*
|
|
4
|
+
* The runner writes one row per state transition. The user-visible
|
|
5
|
+
* `GET /api/lifeops/scheduled-tasks/:id/history` endpoint reads from
|
|
6
|
+
* this log. Default 90-day retention with a nightly rollup pass that
|
|
7
|
+
* folds expired entries into a daily-summary row per task per
|
|
8
|
+
* transition kind.
|
|
9
|
+
*/
|
|
10
|
+
import type { ScheduledTaskLogEntry, ScheduledTaskLogTransition } from "./types.js";
|
|
11
|
+
export interface ScheduledTaskLogStore {
|
|
12
|
+
append(entry: ScheduledTaskLogEntry): Promise<void>;
|
|
13
|
+
list(args: {
|
|
14
|
+
agentId: string;
|
|
15
|
+
taskId: string;
|
|
16
|
+
/** Inclusive lower bound (ISO). */
|
|
17
|
+
sinceIso?: string;
|
|
18
|
+
/** Exclusive upper bound (ISO). */
|
|
19
|
+
untilIso?: string;
|
|
20
|
+
/** When true, omit rolled-up summary rows. Default false. */
|
|
21
|
+
excludeRollups?: boolean;
|
|
22
|
+
limit?: number;
|
|
23
|
+
}): Promise<ScheduledTaskLogEntry[]>;
|
|
24
|
+
rollupOlderThan(args: {
|
|
25
|
+
agentId: string;
|
|
26
|
+
olderThanIso: string;
|
|
27
|
+
}): Promise<{
|
|
28
|
+
rolledUp: number;
|
|
29
|
+
deletedRaw: number;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* In-memory store used for unit tests + as a fallback when no DB-backed
|
|
34
|
+
* store is wired in. The DB-backed store lives behind the route handlers
|
|
35
|
+
* via `LifeOpsRepository`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createInMemoryScheduledTaskLogStore(): ScheduledTaskLogStore;
|
|
38
|
+
export declare const STATE_LOG_DEFAULT_RETENTION_DAYS = 90;
|
|
39
|
+
/**
|
|
40
|
+
* Build a logger that writes to the given store and updates the task's
|
|
41
|
+
* `lastDecisionLog`. The runner uses this to wrap every state transition.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createStateLogger(args: {
|
|
44
|
+
store: ScheduledTaskLogStore;
|
|
45
|
+
agentId: string;
|
|
46
|
+
/** Generator override for tests (defaults to crypto.randomUUID). */
|
|
47
|
+
newLogId?: () => string;
|
|
48
|
+
/** Now override for tests. */
|
|
49
|
+
now?: () => Date;
|
|
50
|
+
}): {
|
|
51
|
+
log: (taskId: string, transition: ScheduledTaskLogTransition, args?: {
|
|
52
|
+
reason?: string;
|
|
53
|
+
detail?: Record<string, unknown>;
|
|
54
|
+
}) => Promise<ScheduledTaskLogEntry>;
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=state-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-log.d.ts","sourceRoot":"","sources":["../../src/scheduled-task/state-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,qBAAqB,EACrB,0BAA0B,EAC3B,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,CAAC,IAAI,EAAE;QACT,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,mCAAmC;QACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,mCAAmC;QACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,6DAA6D;QAC7D,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC;IACrC,eAAe,CAAC,IAAI,EAAE;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvD;AAED;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,qBAAqB,CA6E3E;AAED,eAAO,MAAM,gCAAgC,KAAK,CAAC;AAEnD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,KAAK,EAAE,qBAAqB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,MAAM,CAAC;IACxB,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB,GAAG;IACF,GAAG,EAAE,CACH,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,0BAA0B,EACtC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,KACzD,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACrC,CAoBA"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
function createInMemoryScheduledTaskLogStore() {
|
|
2
|
+
const rows = [];
|
|
3
|
+
return {
|
|
4
|
+
async append(entry) {
|
|
5
|
+
rows.push({ ...entry });
|
|
6
|
+
},
|
|
7
|
+
async list({ agentId, taskId, sinceIso, untilIso, excludeRollups, limit }) {
|
|
8
|
+
let view = rows.filter((r) => r.agentId === agentId && r.taskId === taskId).filter((r) => excludeRollups ? !r.rolledUp : true).filter((r) => sinceIso ? r.occurredAtIso >= sinceIso : true).filter((r) => untilIso ? r.occurredAtIso < untilIso : true).sort((a, b) => a.occurredAtIso < b.occurredAtIso ? -1 : 1);
|
|
9
|
+
if (typeof limit === "number" && limit > 0) {
|
|
10
|
+
view = view.slice(0, limit);
|
|
11
|
+
}
|
|
12
|
+
return view.map((r) => ({ ...r }));
|
|
13
|
+
},
|
|
14
|
+
async rollupOlderThan({ agentId, olderThanIso }) {
|
|
15
|
+
const expired = rows.filter(
|
|
16
|
+
(r) => r.agentId === agentId && !r.rolledUp && r.occurredAtIso < olderThanIso
|
|
17
|
+
);
|
|
18
|
+
if (expired.length === 0) {
|
|
19
|
+
return { rolledUp: 0, deletedRaw: 0 };
|
|
20
|
+
}
|
|
21
|
+
const summaryByKey = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const row of expired) {
|
|
23
|
+
const dayIso = row.occurredAtIso.slice(0, 10);
|
|
24
|
+
const key = `${row.taskId}::${dayIso}::${row.transition}`;
|
|
25
|
+
const existing = summaryByKey.get(key);
|
|
26
|
+
if (existing) {
|
|
27
|
+
existing.count += 1;
|
|
28
|
+
} else {
|
|
29
|
+
summaryByKey.set(key, {
|
|
30
|
+
taskId: row.taskId,
|
|
31
|
+
transition: row.transition,
|
|
32
|
+
dayIso,
|
|
33
|
+
count: 1,
|
|
34
|
+
firstReason: row.reason
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const summaries = [];
|
|
39
|
+
let counter = 0;
|
|
40
|
+
for (const [key, summary] of summaryByKey.entries()) {
|
|
41
|
+
counter += 1;
|
|
42
|
+
summaries.push({
|
|
43
|
+
logId: `rollup:${key}:${counter}`,
|
|
44
|
+
taskId: summary.taskId,
|
|
45
|
+
agentId,
|
|
46
|
+
occurredAtIso: `${summary.dayIso}T00:00:00.000Z`,
|
|
47
|
+
transition: summary.transition,
|
|
48
|
+
reason: summary.firstReason,
|
|
49
|
+
rolledUp: true,
|
|
50
|
+
detail: { rollupCount: summary.count }
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const expiredIds = new Set(expired.map((r) => r.logId));
|
|
54
|
+
const remaining = rows.filter((r) => !expiredIds.has(r.logId));
|
|
55
|
+
rows.length = 0;
|
|
56
|
+
rows.push(...remaining, ...summaries);
|
|
57
|
+
return { rolledUp: summaries.length, deletedRaw: expired.length };
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const STATE_LOG_DEFAULT_RETENTION_DAYS = 90;
|
|
62
|
+
function createStateLogger(args) {
|
|
63
|
+
const newLogId = args.newLogId ?? (() => `stl_${Math.random().toString(36).slice(2, 12)}`);
|
|
64
|
+
const now = args.now ?? (() => /* @__PURE__ */ new Date());
|
|
65
|
+
return {
|
|
66
|
+
async log(taskId, transition, opts) {
|
|
67
|
+
const entry = {
|
|
68
|
+
logId: newLogId(),
|
|
69
|
+
taskId,
|
|
70
|
+
agentId: args.agentId,
|
|
71
|
+
occurredAtIso: now().toISOString(),
|
|
72
|
+
transition,
|
|
73
|
+
reason: opts?.reason,
|
|
74
|
+
rolledUp: false,
|
|
75
|
+
detail: opts?.detail
|
|
76
|
+
};
|
|
77
|
+
await args.store.append(entry);
|
|
78
|
+
return entry;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
STATE_LOG_DEFAULT_RETENTION_DAYS,
|
|
84
|
+
createInMemoryScheduledTaskLogStore,
|
|
85
|
+
createStateLogger
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=state-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/scheduled-task/state-log.ts"],"sourcesContent":["/**\n * State-log writer.\n *\n * The runner writes one row per state transition. The user-visible\n * `GET /api/lifeops/scheduled-tasks/:id/history` endpoint reads from\n * this log. Default 90-day retention with a nightly rollup pass that\n * folds expired entries into a daily-summary row per task per\n * transition kind.\n */\n\nimport type {\n ScheduledTaskLogEntry,\n ScheduledTaskLogTransition,\n} from \"./types.js\";\n\nexport interface ScheduledTaskLogStore {\n append(entry: ScheduledTaskLogEntry): Promise<void>;\n list(args: {\n agentId: string;\n taskId: string;\n /** Inclusive lower bound (ISO). */\n sinceIso?: string;\n /** Exclusive upper bound (ISO). */\n untilIso?: string;\n /** When true, omit rolled-up summary rows. Default false. */\n excludeRollups?: boolean;\n limit?: number;\n }): Promise<ScheduledTaskLogEntry[]>;\n rollupOlderThan(args: {\n agentId: string;\n olderThanIso: string;\n }): Promise<{ rolledUp: number; deletedRaw: number }>;\n}\n\n/**\n * In-memory store used for unit tests + as a fallback when no DB-backed\n * store is wired in. The DB-backed store lives behind the route handlers\n * via `LifeOpsRepository`.\n */\nexport function createInMemoryScheduledTaskLogStore(): ScheduledTaskLogStore {\n const rows: ScheduledTaskLogEntry[] = [];\n return {\n async append(entry) {\n rows.push({ ...entry });\n },\n async list({ agentId, taskId, sinceIso, untilIso, excludeRollups, limit }) {\n let view = rows\n .filter((r) => r.agentId === agentId && r.taskId === taskId)\n .filter((r) => (excludeRollups ? !r.rolledUp : true))\n .filter((r) => (sinceIso ? r.occurredAtIso >= sinceIso : true))\n .filter((r) => (untilIso ? r.occurredAtIso < untilIso : true))\n .sort((a, b) => (a.occurredAtIso < b.occurredAtIso ? -1 : 1));\n if (typeof limit === \"number\" && limit > 0) {\n view = view.slice(0, limit);\n }\n return view.map((r) => ({ ...r }));\n },\n async rollupOlderThan({ agentId, olderThanIso }) {\n const expired = rows.filter(\n (r) =>\n r.agentId === agentId &&\n !r.rolledUp &&\n r.occurredAtIso < olderThanIso,\n );\n if (expired.length === 0) {\n return { rolledUp: 0, deletedRaw: 0 };\n }\n const summaryByKey = new Map<\n string,\n {\n taskId: string;\n transition: ScheduledTaskLogTransition;\n dayIso: string;\n count: number;\n firstReason?: string;\n }\n >();\n for (const row of expired) {\n const dayIso = row.occurredAtIso.slice(0, 10);\n const key = `${row.taskId}::${dayIso}::${row.transition}`;\n const existing = summaryByKey.get(key);\n if (existing) {\n existing.count += 1;\n } else {\n summaryByKey.set(key, {\n taskId: row.taskId,\n transition: row.transition,\n dayIso,\n count: 1,\n firstReason: row.reason,\n });\n }\n }\n const summaries: ScheduledTaskLogEntry[] = [];\n let counter = 0;\n for (const [key, summary] of summaryByKey.entries()) {\n counter += 1;\n summaries.push({\n logId: `rollup:${key}:${counter}`,\n taskId: summary.taskId,\n agentId,\n occurredAtIso: `${summary.dayIso}T00:00:00.000Z`,\n transition: summary.transition,\n reason: summary.firstReason,\n rolledUp: true,\n detail: { rollupCount: summary.count },\n });\n }\n // Replace the expired raw rows with the new rollup rows.\n const expiredIds = new Set(expired.map((r) => r.logId));\n const remaining = rows.filter((r) => !expiredIds.has(r.logId));\n rows.length = 0;\n rows.push(...remaining, ...summaries);\n return { rolledUp: summaries.length, deletedRaw: expired.length };\n },\n };\n}\n\nexport const STATE_LOG_DEFAULT_RETENTION_DAYS = 90;\n\n/**\n * Build a logger that writes to the given store and updates the task's\n * `lastDecisionLog`. The runner uses this to wrap every state transition.\n */\nexport function createStateLogger(args: {\n store: ScheduledTaskLogStore;\n agentId: string;\n /** Generator override for tests (defaults to crypto.randomUUID). */\n newLogId?: () => string;\n /** Now override for tests. */\n now?: () => Date;\n}): {\n log: (\n taskId: string,\n transition: ScheduledTaskLogTransition,\n args?: { reason?: string; detail?: Record<string, unknown> },\n ) => Promise<ScheduledTaskLogEntry>;\n} {\n const newLogId =\n args.newLogId ?? (() => `stl_${Math.random().toString(36).slice(2, 12)}`);\n const now = args.now ?? (() => new Date());\n return {\n async log(taskId, transition, opts) {\n const entry: ScheduledTaskLogEntry = {\n logId: newLogId(),\n taskId,\n agentId: args.agentId,\n occurredAtIso: now().toISOString(),\n transition,\n reason: opts?.reason,\n rolledUp: false,\n detail: opts?.detail,\n };\n await args.store.append(entry);\n return entry;\n },\n };\n}\n"],"mappings":"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","names":[]}
|