/** * routes/occasions-composition.ts * * Builds the one occasions service this process has, attaches the `occasions.*` * verb handlers to it, and hands back a teardown. Same shape as * owner-profile-composition.ts: the registrar calls one function. * * ## It is composed over the owner-profile store, not beside it * * Occasions live in the owner's profile, so this takes the store that already * owns that file rather than opening it a second time. A second reader would be * a second projection of the same document, disagreeing with the first for as * long as one of them had not noticed a hand edit. * * ## Delivery is bound, not invented * * The nudge goes out over the channel delivery router, the same substrate the * proactive check-in uses, with the target parsed by the router's own parser. * That includes the `agent` destination: the router carries a strategy for it, * and the agent product registers the sender that lands the message in its * conversation (`channels/delivery/strategies-agent.ts`). Nothing about this * file has to know which of the configured destinations is a transport and which * is another product. * * When no router is wired the service still runs and still records what is * outstanding; `occasions.pending` is then the only way a nudge is seen. * * ## Something has to run the sweep * * A loop that only runs when a verb asks it to is not proactive, and proactive * is the whole feature. So this arms a repeating timer, re-read from config on * every tick so `occasions.sweepIntervalMinutes` is a live setting rather than a * restart-only one. * * The timer is deliberately dumb, because the SWEEP is where the judgement is: * a tick inside quiet hours raises nothing and reaps anyway, and a tick on a day * an occasion has already been raised finds its open item not yet due. So the * interval decides how soon the first nudge lands and nothing else, it cannot * make the system nag. It is `unref`'d so it never holds the process open, and * a tick that overlaps a still-running one is skipped rather than queued. */ import type { GatewayMethodCatalog } from '../method-catalog.js'; import type { ConfigManager } from '../../config/manager.js'; import type { ChannelDeliveryRouter } from '../../channels/delivery-router.js'; import type { OwnerProfileStore } from '../../owner-profile/index.js'; import { OccasionStateStore } from '../../occasions/state-store.js'; import { OccasionsService } from '../../occasions/service.js'; /** What the composition needs from the runtime graph. */ export interface OccasionsCompositionDeps { /** The store that already owns the owner-profile file. */ readonly ownerProfile: OwnerProfileStore; /** Reads the live `occasions.*` and `daemon.timezone` policy. */ readonly configManager: Pick; /** Absolute path of the machine-owned state file. */ readonly statePath: string; /** Where a nudge is delivered. Absent ⇒ pull-only, through `occasions.pending`. */ readonly channelDeliveryRouter?: Pick | undefined; } export interface OccasionsComposition { readonly service: OccasionsService; readonly state: OccasionStateStore; readonly dispose: () => Promise; } export declare function composeOccasions(catalog: GatewayMethodCatalog, deps: OccasionsCompositionDeps): OccasionsComposition; /** * What the gateway registrar has to hand when it installs this. * * Declared here rather than as an inline object at the call site so the * registrar's own file, which is at the repo's line-cap ceiling, carries one * line for this feature rather than a dozen, and so the reasoning about WHICH * pieces are needed lives beside the composition that needs them. */ export interface OccasionsInstallDeps { readonly configManager: Pick; readonly shellPaths: { resolveUserPath(...segments: string[]): string; }; /** Surface root the state file resolves under; required, never defaulted (control-plane-store-paths.ts). */ readonly surfaceRoot: string; readonly channelDeliveryRouter?: Pick | undefined; readonly disposal?: { add(label: string, dispose: () => void): void; } | undefined; } /** * Compose the occasions loop and register its teardown. * * Called wherever the owner profile is composed, because the profile store is * the one thing it cannot do without. Everything else is optional: with no * delivery router it still reads, still answers and still records what is * outstanding, which is how the agent surface receives a nudge in any case. */ export declare function installOccasions(catalog: GatewayMethodCatalog, ownerProfile: OwnerProfileStore, deps: OccasionsInstallDeps): OccasionsComposition; //# sourceMappingURL=occasions-composition.d.ts.map