Navigácia
ELOWEN · Dokumentácia

Posledná aktualizácia: 20. októbra 2026

Prechádzať dokumentáciu · cronjob implementation

Tento článok zatiaľ nie je preložený. Je dostupný v angličtine.

Developer reference

cronjob implementation

Local platform lifetime

CronAdapter.connect({ signal, onConnectionState }) awaits initial journal pruning, then starts its daily maintenance and scheduler intervals before reporting one canonical state for cron:local: local-owner authority cron, unknown transport, ready application, fresh observation and no retry. Core's PlatformConnectionRuntime can then release interrupted-channel recovery. This is local scheduler readiness, not delivery-provider health. Host abort and disconnect() remove the abort listener, clear both intervals, stop future job dispatch and abort managed guard execution. Already-owned model turns retain their existing result-delivery ownership. Aborting during initial pruning prevents timers and readiness from being published when pruning settles. Initialization or report failure cleans up and rejects.

Project removal

This participant requires core 0.30.11 or later for durable removal acknowledgement and transaction-scoped active-Project admission. Update core before installing this plugin version.

JobStore.save requires active Projects for changed/new bound jobs via tx.host.projects.requireActive(id) and holds those locks through the final atomic JSON write in a short, nonretrying READ COMMITTED transaction. It performs no network I/O there and propagates file-write errors. Because admission yields, it rejects a changed file snapshot with cronjob.jobs_changed; a competing scheduler claim leaves that job for the winning generation or a later tick. Pure removals and unbound writes retain the existing synchronous owner boundary, so cleanup is not blocked by unchanged jobs in another deleting Project. Admission examines only stored job records; malformed rows retained by strict tool writes remain in the saved file without being interpreted as Project bindings. Journal settlement follows the file write and remains retryable.

The plugin declares provides.projectRemoval: "services". Its idempotent registerProjectRemoved callback removes jobs whose projectRef.projectId matches the deleting Project, preserving instance jobs and jobs bound elsewhere. It awaits store.save even when a retry finds no matching jobs, so a previous file write cannot hide unfinished journal settlement. Existing job deletion semantics retain claimed runs and history; core owns cancellation of Project work. Job creation, edits and execution reject lifecycle deleting; editing an unchanged Project binding still revalidates it.

Migration entry

The manifest entry exports migrate(db: PluginDb<never>): Promise<void> beside register. The daemon loader always awaits it before registration for this reads: ['db'] plugin; the DB CLI calls the same export without registering tools, routes, timers or services. The export runs only the existing transactional plugin SQL through db.migrate. A failed step rolls back its SQL and ledger entry and prevents registration. Repeated calls skip applied versions. Runtime store construction never invokes migrations. Tests that bypass the loader explicitly await this export before using their fixture.

Turn authority and conversation filing

Scheduling tools await the host's currentIdentity(), currentAccess() and isAdminSession() readers before deciding job ownership, visibility, privileged options and execution project binding. The shared access helpers and their callers are asynchronous through that boundary. Conversation origin capture and private-room disclosure await the same verified identity, so a promise cannot be mistaken for an anonymous caller or an absent project reference.

conversationLinks() awaits each immutable-key jobAssociation() lookup before filtering against the host-authorized conversation ids. Job ACL, execution routing and delivery remain separate from filing. The real-loader cron tool, ownership, wake-up and conversation-group suites cover these asynchronous host contracts, including private titles that must remain hidden in shared rooms.

Problem audiences

Guard command exits, timeouts and output limits stay local. Project removal, revoked access, unavailable environments and nonzero project-command exits carry the core typed instance-condition cause. Guard runner defects retain cronjob.check_failed; scheduler, delivery and exhausted turn retries apply the same cause distinction before publisher reporting. Retry limits, log text, journal receipts and owner alerts are unchanged. This requires the core typed instance-condition seam exposed through elowen-plugin-shared/errors.

The host's notify rejects with an AggregateError retaining each adapter failure as a native cause. An all-instance aggregate remains local; one software failure remains reportable. A platform that has not reported readiness is not automatically an instance condition: an adapter missing its readiness callback is a software defect. Failed sends retain the queued payload and release its delivery lease; only a successful send removes it.

Retry reporting

The adapter uses ConsecutiveFailures from elowen-plugin-shared/consecutiveFailures; the plugin declares core 0.30.11 and shared API 14. Recurring checks count per job; queued delivery counts per queued item; periodic tick and journal maintenance each have their own counter. They export their existing code only on failure three and reset after success. Delivery and tick classify typed instance conditions before incrementing: those failures retain their local diagnostic without consuming or resetting the software-failure threshold. For example, three Discord HTTP 503 failures followed by three HTTP 400 failures without a successful send report the third 400 and keep the payload queued. The tick applies the same rule when a journal claim cannot acquire a database connection and later fails with a software defect. An overlapping or stopped tick skipped by the scheduler guard does not reset the active episode. runCheck returns a typed failed flag so an empty successful check resets the episode. One-shot checks and boot maintenance remain immediate. Bounded turn retries stay local until the final failed attempt, which exports cronjob.turn_retry once and retains the existing owner alert. A relay rejection with core's native_checkpoint_conversion_failed code stops after the first attempt: the owner must repair history/account access before trying again. That refusal remains a local warning and an error run receipt, with the owner notified through the bell.

Owner alerts use ctx.alerts.raise with the required reporting: { kind: 'local-only' } classification. The scheduler's own diagnostics decide which failures to export; the bell does not report them again. A delivery failure is reported only for a successful non-quiet result or image that lacks host delivery confirmation. A failed turn has no successful result to deliver, and an empty or quiet reply needs no delivery. All keep their existing run-history and owner-alert behavior.

Bounded journal maintenance

Cronjob uses the asynchronous PluginDb from await ctx.db(). Its schema is the host-selected plugin_cronjob; SQL quotes that identifier and qualifies every plugin table. Migration 7 creates the final PostgreSQL runs and run_daily tables with explicit row_id identities. Installed ledgers already record it, so it is never renumbered.

Maintenance uses drainDbRows(db, { schema: db.schema, table, identity: 'row_id', ... }) from elowen-plugin-shared/dbBatches. Its awaited beforeDelete(tx, ids) aggregates the selected captured owners and deletes those exact identities on the same transaction connection. Selection locks rows in identity order before aggregation. Both drains use the shared 64-row / 256 KiB limits, process one oversized row alone and yield between committed batches. PostgreSQL octet_length measures UTF-8 text, including multibyte previews.

openRunJournal().prune(now) returns a promise and drains expired detail in transactions of at most 64 rows and 256 KiB of stored text plus row overhead. An individual oversized historical row is processed alone. The same transaction adds the selected runs to the owner-specific daily counters and deletes exactly those runs, so interruption or overlapping maintenance cannot double-count. Daily expiry removes at most 64 summaries and 256 KiB per transaction. Each nonempty batch yields through setImmediate; callers await completion or log a rejected maintenance promise. CronAdapter.connect() owns initial pruning and the daily timer. Retention horizons and account visibility remain unchanged.

removeUser(id) and removeMissingUsers(ids) return promises resolving to the combined number of deleted detail and aggregate rows. They first clear removed initiators in batches of at most 64 explicit identities, then drain owned details and daily summaries with the same deletion budgets. The existing initiator index is reused; null owners survive missing-account cleanup. Empty or unreadable account lists retain the existing skip behavior.

Both account-removal and boot callbacks await the journal cleanup. Boot awaits the account directory, then retires missing-account jobs with the synchronous file write before awaiting journal settlement. The host's 20-second reconcile budget logs failure and raises an admin alert without cancelling work, so cleanup may finish after scheduler startup. Once journal cleanup starts, it does not write the jobs file again and cannot overwrite a concurrent scheduler claim or newly saved job. Each batch commits independently: a failed batch rolls back and rejects, earlier batches remain durable, and repeating cleanup selects only rows still needing removal. No startup budget is increased. tests/cronCleanup.test.ts covers thousands of details and aggregates, yields, failed-batch rollback, resumption. tests/cronCleanupLifecycle.test.ts covers callback completion and the host deadline with delayed database transaction acquisition.

Run claims and concurrent readers

A slot claim is one INSERT ... ON CONFLICT (claim_key) DO NOTHING RETURNING id. The losing caller reads the committed winner and never dispatches it. Manual runs queued earlier reuse their durable id; the scheduler must still win UPDATE ... WHERE outcome='waiting' before checking or relaying. These database constraints apply equally to daemon and runner connections. Atomic JSON schedule stamps retain their existing crash semantics, while the database owns cross-process execution deduplication.

Closing a run is a conditional terminal update that computes duration from the row's actual start. Job-save settlement may close only waiting manual rows, so a save racing a started guard cannot overwrite its result. Async claims recheck the current job owner before dispatch. Only authoritative registration reconciles abandoned executions, and registration awaits that reconciliation.

Run and summary pagination use read-only repeatable-read transactions for their count and page. History and metadata keep their existing account ACL; substring searches use literal escaped ILIKE. Owner profiles, grants, projects and immutable conversation associations await the live host readers. No additional host capability is introduced.

Conversation-list history branch

The conversation-list mount requires core 0.30.7. The host mounts ConversationHistoryBranch inside a named branch region below its conversation list item, never inside a table. The unchanged parent/items/expanded/toggle/open props carry host-authorized job metadata and controlled expansion; the plugin adds no reads, polling, pagination or persistence. The disclosure uses the shared ghost Button and ChevronRight; its named ul contains one li per job with Clock and localized enabled/paused badges. aria-controls names only the mounted list, with a unique id per renderer.

A retained run calls open("session:" + encodeURIComponent(sessionId) + ":" + flag), where flag is 1 only for a continuable run and 0 otherwise. This requests navigation, not authority: the host still decides whether the transcript is continuable. Each job has a required nullable run, matching the core ConversationJobLink DTO. run: null means no readable retained run: the anchor keeps the exact host-provided job-editor href and other jobs remain visible. A missing run is invalid input, not an older DTO variant. Empty or invalid item arrays mount nothing; loading, unavailable and read-error presentation belong to the host's independent branch state. When no plugin renderer is available, the host provides its own list-compatible fallback.

The old table renderer is removed without a compatibility flag. Coordinate this plugin with the core 0.30.7 conversation-register cutover; older Cronjob browser bundles are not list-compatible. The separate Automation history table remains unchanged.

Verification uses the paired core candidate selected by ELOWEN_CORE_ROOT:

  • tests/cronjob-conversation-list.test.tsx checks source and committed bundle registration through the candidate UI kit and host runtime. It covers list semantics, all three locales, mounted disclosure targets, encoded continuation flags, mixed retained/null-run jobs and exact editor links. Empty and malformed input checks keep a missing run distinct from the valid run: null DTO.
  • tests/cronConversationGroups.test.ts checks the plugin-owned cron.conversationLinks control: authorized conversation IDs, ownership and run-target metadata. Core owns the later projection into ConversationJobLink, including whether a retained run is readable.
  • tests/coreCompatibilityGate.test.ts checks that Cronjob refuses the old table host (core 0.30.6), admits the coordinated Project-removal host (0.30.11) and requires a capable paired candidate. Release tooling assigns the candidate's version; the gate still rejects an older artifact version.
  • tests/releaseMetadata.test.mjs and tests/registryCatalog.test.mjs check manifest/catalog/README version and compatibility agreement. Rebuilding only Cronjob with npm run build:web -- cronjob must leave its committed web/index.js and web/index.css unchanged.

The host catalog and CSS contracts (tests/contract/pluginBundleI18nKeys.test.ts and tests/pluginWebCss.test.mjs) additionally require that candidate's completed web build; source-only UI tests cannot replace them. The integrated conversation-list browser flow must load the candidate bundle to verify real keyboard, touch and layout behavior.

UI invalidation

Maintainer note: creating, saving or deleting a job refreshes the jobs list, calendar, run history and host conversation links through the plugin's useInvalidateJobs hook. Run now from a job drawer, calendar card or failed-run result uses the same hook after the server accepts the request. The drawer owns its refresh, including an already-queued response; its callers do not repeat it. Calendar state labels and the History and result badges share web-src/runStatus.ts; calendar dots keep their own CSS tones. Detailed run receipts are retained for seven days, daily aggregates for ninety days, and result previews for up to 2,000 characters. These journal limits are fixed; the journal's optional clock is used by tests.

Browser settings contract

The job rail and creation dialogs remain available from both the Automation page and the plugin settings deck. They share ScheduleField, ActiveHoursField and the existing conversation/destination adapters. Simple schedule, interval-unit, active-hours, owner, preset and suggested-time choices use shared dropdowns; the calendar's day/week navigation remains segmented. Presets fill an editable draft rather than binding a job to a template, and custom times remain visible. For one-shot creation, daily and weekly presets may fill a blank time through the existing schedule parser; interval presets leave the time for the user to choose.

Forms use the host's SettingsDocument, SettingsGroup and SettingsRow. Schedule and Advanced groups start closed and persist through the host's folding mechanism. Saved-job keys are cronjob:job:<id>:schedule and cronjob:job:<id>:advanced; creation keys are cronjob:create:<lifecycle>:schedule and cronjob:create:<lifecycle>:advanced. Both entry points share the same saved-job keys. Untitled leading fields and read-only run information do not become disclosures. Groups stay mounted when closed, so folding does not reset a draft or change autosave.

Browser UI API 62 is required. Every single choice in the job forms (schedule mode, interval unit, weekday, active hours, owner and execution Project) is the host ChoiceField, typed as ElowenUiRuntime['components']['ChoiceField'] and always the anchored dropdown: the consumers pass no presentation prop, and because the published onChange receives a plain string, each schedule or owner callback narrows it to its own union where the options are declared. Saved values the options no longer list stay visible and selectable through the host's stored-unknown-value row.

The host's Input.unit displays interval amounts as min or h, and active-hour clock values as h, using the host's shared numeric band. Schema millisecond limits display ms and the shortest interval displays min, without a divisor or storage conversion. Count meaning belongs in the localized label. Native input names, bounds and steps remain intact. Advanced cron validation stays on the server; unrepresented schedules and stored active-hours expressions remain unchanged until edited. Job edits retain revision-checked debounced autosave; creation retains explicit idempotent submission.

See the Scheduling Plugin page in the Elowen user manual.

Server module ownership

index.mjs composes the existing registrations in their original order. It opens the three atomic JSON stores and the SQL run journal, registers account cleanup/reconciliation, connects the concrete readers and writers below, then registers the platform adapter and shipped skills.

  • lib/scheduler.mjs owns CronAdapter, guard execution, due/manual claims, relay, owner alerts and leased pending deliveries. Its jobRunLocation also supplies calendar/navigation/retention reads. The host still injects the live timezone and owner-permission readers; these required inputs have no constructor defaults. The adapter keeps one sequential tick per generation and stops future work at teardown.
  • lib/stores.mjs owns jobs, creation receipts and pending-delivery persistence, strict write reads, model-pair validation and the draft/creation rules shared by HTTP and tools. Job saves write atomic JSON before awaiting pending journal settlement; all callers await completion. Creation receipts and delivery leases retain their existing file format and ordering. Creation writes its receipt through the synchronous after-write callback before journal settlement yields, preserving retry deduplication. lib/runJournal.mjs remains the SQL authority for claims, run receipts and retention.
  • lib/access.mjs owns account grants, owner limits, explicit tool scope and the actor visibility rule. Each execution rechecks permissions. Missing permission reads retain their existing fail-closed behavior.
  • lib/filing.mjs owns immutable conversation associations, metadata-only conversation discovery and the unchanged cron control methods, retainedSessionIds and conversationLinks. Filing never changes execution or delivery. Missing and unavailable associations remain distinct.
  • lib/routes.mjs registers the complete jobs GET/PUT/POST/DELETE patterns. Authorization still precedes conflict payloads. After awaited validation and filing, a fresh strict read rechecks the target before the file write. Manual queueing also rechecks ownership after its awaited journal claim.
  • lib/tools.mjs registers the same five tools through the shared access, filing and write readers.
  • lib/calendar.mjs owns job-response enrichment, the live engine-input reader and week/history/preview routes. Timezone, generation time, tick and lookback are read at request time; intentional preview overrides remain at their callers. Empty results and unreadable jobs still have separate responses.
  • schedule.mjs owns parsing, due/slot decisions, wall-clock resolution, labels and schedule defaults. projection.mjs owns bounded forward expansion and arithmetic day summaries, importing that same parser, cron-day predicate, DST identity and active-hours gate. Projection is pure and performs no storage or network work; its existing candidate and day-slot caps remain unchanged.
  • lib/constants.mjs holds only the shared id generator and JSON response constructor.

For example, a job-list GET filters with the access reader, enriches filing and next-occurrence data through the calendar reader, and projects from the same schedule authority used by the next scheduler tick. No additional host seam or public control/route contract is introduced.

Shared time-zone helpers

The server schedule engine and the scheduler import zonedParts, zonedTimeToMs and WEEKDAYS from elowen-plugin-shared/zonedTime. The shared module owns the formatter cache, local wall-clock fields, UTC conversion and Sunday-first weekday order. No local copies or re-exports remain in schedule.mjs. The cron parser, slot identity, due logic and previews stay in this plugin and use those helpers. This keeps the same time-zone and DST behavior while letting meeting-confirm use the same conversion.

The helpers do no network or storage work. An invalid zone retains the existing machine-zone behavior; schedules, catch-up and duplicate DST slots are unchanged. The manifest declares core 0.30.11 and shared API 14.

Authenticated API patterns

The plugin declares core 0.30.11 and shared API 14, supplied by the core's shared package 0.12.0. Each method registers its complete pattern with its own catalog summary. The manifest declares all 11 unique patterns, including the conversation reader in lib/filing.mjs.

Method Full path Behavior
GET /plugins/cronjob/jobs List visible jobs.
POST /plugins/cronjob/jobs Create a job with an idempotent request ID.
PUT /plugins/cronjob/jobs/:jobId Upsert one job with revision checks.
DELETE /plugins/cronjob/jobs/:jobId Delete an authorized job idempotently.
POST /plugins/cronjob/jobs/:jobId/run Queue a durable manual run.
GET /plugins/cronjob/api/conversations List filing choices visible to the account.
GET /plugins/cronjob/api/week Read the bounded week calendar.
GET /plugins/cronjob/api/runs List retained runs or daily totals.
GET /plugins/cronjob/api/runs/:runId Read one visible run receipt.
GET /plugins/cronjob/api/operations List the account's unacknowledged manual runs.
GET /plugins/cronjob/api/operations/:operationId Read the account's manual-run operation.
POST /plugins/cronjob/api/operations/:operationId/acknowledge Acknowledge a finished owned operation.
POST /plugins/cronjob/api/schedule-preview Validate a draft and preview its next occurrences.

The host matches the entire declared pattern and decodes identifiers exactly once into req.params.jobId, req.params.runId or req.params.operationId. Handlers receive an empty req.path and never parse or decode a suffix. Unsupported methods and undeclared descendants return 404 before a handler runs. Account grants, ownership, history visibility, manual-run initiators, revision conflicts and acknowledgement rules remain enforced at their existing boundaries.

Route fixtures use the candidate's real PluginRegistry matcher. HTTP regressions cover job writes with encoded identifiers and rejected descendants; operation regressions cover queue, detail, acknowledgement and owner isolation through the complete declarations.

Developer reference

History retention and daily totals

GET /plugins/cronjob/api/runs?view=daily&from=2026-09-01&to=2026-09-30 returns { dailySummaries, total }. Each summary contains jobId, jobName, captured ownerUserId, localDate, timezone, ok, error, skipped and total. It uses the same actor visibility and owner/date/job/search/outcome filters as run history, but searches only retained job names and uses limit/offset pagination, capped at 100 rows. An outcome filter selects summaries containing that outcome; all counters remain the day's totals. Without view=daily the existing detailed run list and cursor contract remain. Empty retained history returns an empty summary list. HistoryTab requests both views with independent pagers and never offers run, transcript or result actions for summary rows. Detailed terminal runs older than seven days become summaries retained for 90 days. countsByDay adds both stores without counting pruned details twice.

Captured aggregate ownership

The PostgreSQL daily journal is unique by job, local date and captured owner, treating NULL as the instance bucket. Pruning groups by the captured owner and never overwrites another owner's counters on a transfer. Migration applies through the host plugin DB migrator at load; it never needs a live-DB maintenance script. Historical rows retain the ownership recorded by schema 3; prior overwritten attribution cannot be reconstructed from aggregates. HistoryTab is the reader.

Shared live schedule inputs and preview refusal

createJobProjection awaits the live host timezone and reads generation time plus the active CronAdapter's tickMs and cronLookbackMs. Its liveEngineInputs and next-occurrence projections are asynchronous; calendar routes await them before expanding slots. The scheduler, manual runs, wake-up tool and local-time draft builders likewise await the same timezone resolver, preserving non-UTC and DST wall-clock identities. Daily/weekly due checks and forward projections share schedule.mjs localSlotInstant: the spring gap has no occurrence and a repeated fall hour chooses its earlier instant once. Historical week cards override the reference time to just before their local day; draft previews otherwise use the same live policy as the scheduler. POST schedule-preview reads only schedule and expands forward from the current instant, returning at most five upcoming occurrences. If planOccurrences exhausts its 100000-candidate budget, the route returns HTTP 422, code preview_budget_exhausted and no occurrences. Invalid grammar remains valid:false with its existing response contract. The advanced editor displays the host API error; valid ordinary modes keep their existing controls. No browser cron expansion is introduced.

Scoped command execution

Persisted jobs and create/edit API payloads accept only host and managed Project references, as validated by executionRef; a managed reference requires projectId, while the administrative host reference may omit it. Browser CronJob and CronJobCreateBody select these two variants from the shared ProjectExecutionRef using its kind discriminator. JobDrawer uses that wire type for its stored execution choice, and /projects supplies only host/managed options. Adding a local CLI variant to the shared execution type does not make local targets valid cron schedules. The type narrowing changes no persisted data, request, rendering, or runtime validation.

Cronjob's projectCheck calls the live Sandbox control's runCommand with leaseKind cron, the existing projectRef and explicit account/root actor, the guard timeout/signal and a combined 1 MiB output limit. Core owns capture, cancellation, heartbeat, sanitization and lease release. A missing Sandbox control fails the guard; nonzero or signal exits remain failed checks and producer errors, including cleanup failures, remain errors. Host target and owning-account access are checked at authorization and again before the guard. The scheduler is the consumer.

Cron guard fixtures share tests/helpers/managedSession.ts::childSession with managed LSP tests. Fake EventEmitter children emit stdout/stderr EOF on close; real ChildProcess instances own their pipes. This preserves the host runner's output-drain requirement, partial-output assertions and exactly-once cancellation and lease-release checks.

Shared filing, local times and grammar

CronAdd and HTTP recurring creation use createJobWrites.buildRecurringJob and the existing associationEdit filing authority. A tool supplies its current project binding explicitly even for an instance job; HTTP instance creation remains unbound. Personal creation preserves its existing binding. Direct-delivery origins are supplied only by the tool path and remain independent of filing. createJobWrites.localRunAt is the one create/edit mapper for local one-shots, preserving spring-gap errors and earlier/later instants. ACTIVE_HOURS_PATTERN in scheduleGrammar is shared by daemon active-hour validation and the browser builder; historical malformed-hour handling is unchanged.

Public response cuts

Run-history rows no longer expose requestId or initiatorUserId; operation views and persisted audit/dedupe columns keep them. Week responses omit generatedAt, nowLocalTime, precisionMs and truncated; scheduler exposes only ready. Interval rows retain jobId, schedule, enabled, nextLocalDate and nextLocalTime, removing nextExpectedAt, remainingToday, lastOutcome and lastRunAt. Detailed nextOccurrence keeps expectedAt. planOccurrences returns occurrences/truncated while candidate counting stays private. summarizeJobDay returns exact remaining/head without kind/truncated, bounded to at most 1440 fixed wall clocks and a short head. resolveLocalDateTime returns ms or its existing error; later disambiguation remains. The unused isDue wrapper is removed; dueSlot is the scheduler authority.

Host browser and operation seams

Cronjob uses ElowenUiRuntime.api directly with the published JSON option for create, save, run and preview requests, and its published utils.interpolate type for all existing interpolated consumers. No local serializer or fallback is added. Modal callers no longer pass intent. useRunCronJob returns Promise, retaining accepted-id validation, dock opening, toast and invalidation. Dock presentation uses the existing skipReason and runSkipped translation for terminal skipped operations; no outcome wire field is added. Without skipReason the existing status label remains. Manual HTTP initiators use operationInitiatorFromCredentialScope from the shared package: full/impersonation map to ui, api/advisor to system, agent to agent.