@alma-harness/batch
Governed single-dispatch batch execution. Node >=22 and matching Alma
packages are required. createGovernedBatchRunner composes BatchSubmissionStore,
BatchUsageStore, BatchAccountingStore, ExecutionResultStore, governed settlement,
same-counter spend.peek, audit and SingleDispatchBatchClient capabilities.
Supply immutable configRevision (including provider account selection), bounded request/output/aggregate sizes, runTimeoutMs <=5 minutes and onBackgroundError. The host creates authorized complete manifests: scope/session, item identities, original prices/policy/controls, content/config revisions and result retention. The host derives allowed tools/media/schema; this primitive does not route policy or execute handlers. Never accept manifests/scopes/handles from model output or unverified browser input. Changing config requires a new revision; old batches must still resolve their original configuration, prices and provider account.
submit({input,rationale,loadItems,signal?}) reserves every result and awaits audit
before loading content. Requests must match ordered IDs, model, batch tier and
controls. Only the first durable dispatch:true response permits a provider attempt.
Equal replay never loads input or submits again. The optional awaited onInputFile
hook can retain upload references before create; without it no durable orphan-file
inventory is claimed. Any ambiguous submission stays unresolved, even after expiry.
No fallback, automatic resubmission or lease takeover exists. Explicit provider
cancellation is separate from submission ownership and settlement.
collect({scope,key},{signal?}) resolves the stored handle and collects every
terminal status, including failed/expired/cancelled. Every observation is journaled
before governed settlement and inclusive accounting. Those are the only financial
writers: neither SpendStore.add nor AuditLog.cost runs. Hashes deduplicate metadata
observations only; original manifest settlement identities own charges. Unknown,
unpriced and contradictory evidence stays pending for explicit reconciliation.
New output is retained only after complete, independently validated EOF. Corrupt
or missing tails cannot authorize partial new output. Content bounds/failures do
not stop known-usage drainage. eof describes this invocation, not a durable
completion certificate. reconciliationRequired reports this collection's pending
financial/content work. Existing output remains readable after a later failed GET.
A crash between puts can require another GET, but never another model submission.
Copies at the provider (spec: provider-batch-retention). collect never
deletes them: a job routine collects again until it has delivered.
discard({scope,key}) deletes them through the client's discard, for a host
that collects directly. Call it only after your own delivery. It refuses a batch
with an item not yet accounted and decided.
read({scope,key}) returns persisted metadata, inclusive financial evidence and
current retained item results; it makes no claim of EOF or delivery. Successful
output is a backstage governed-batch-v1 envelope with ordered neutral blocks and
stop. Reasoning is not a public reply; the host validates its business schema and
authorizes any effects. Refusal/truncation/invalid/blocked/unavailable stay distinct.
Original block decisions suppress successful outputs; missing aggregate receipts
keep block-mode success pending until those decisions can be evaluated; warn preserves them. Monetary
warnings are durable evidence, not calls to an external notification service.
reconcile({scope,key},query?) repairs journaled finance without provider/content
access. It returns scanned/repaired/pending counts and afterId. Pages default50,
maximum100; restart future passes at the beginning. Already adopted observations
must still repair missing aggregate receipts, so default queries include them.
Result retention begins at submission reservation. Collection never prepares a new reservation, renews retention or resurrects erasure. Read/put respect the original binding; register result erasure/purge with the host. Successful public submit/collect promises are bounded by timeout/abort (submission also by its original deadline); cancellation does not prove dependency termination. Late acceptance/financial work is observed, and background failures report only scoped stage metadata. Permanent process loss cannot guarantee missing provider evidence.
Migrate batch accounting (which composes submission, usage and governed costs), results and audit before enabling this runner. PostgreSQL store deadlines still apply to read/reconcile. Routines and channel/business delivery require their own persistent claims; this runner never sends real messages or certifies delivery. See spec.
A failure before any dispatch grant, including an abort right after an
acknowledged claim, records not_submitted when that cleanup succeeds (spec:
batch-not-submitted). A lost claim acknowledgement leaves no fence, and a cleanup
write can fail; the store's expiry sweep then records the state after the deadline. collect on it returns
{eof:false, reconciliationRequired:false} without provider access; replaying
submit returns it and never dispatches. The runner has no sweep: call the batch
store's reconcileExpired from the host's scheduler.
Status and cancellation (spec: governed-batch-cancellation-adoption)
status({scope,key},{signal?}) and cancel({scope,key},{signal?}) resolve the
original accepted handle under the runner's configuration. They reject missing
handles, foreign scope/configuration and unsupported cancellation before HTTP.
They remain available after the submission deadline, bounded by this invocation's
runTimeoutMs/signal. Neither loads input nor changes durable execution or finance.
Cancellation returns {status:"requested",progress}: one validated provider
acknowledgement, not terminal cancellation or a refund. A failed/ambiguous request
is never retried automatically; an explicit later call is another cancellation
request. Continue explicit status/collection to account for completed items and
retain unknown evidence. Closing a connection does not prove dependency termination.
Tool selection
Each item's execution.controls.toolChoice must equal its loaded
request.toolChoice. Supported neutral modes are auto, none, required and
{type:"tool",name}; provider/model restrictions remain those documented by the
providers package. Named choices are validated against the actual authorized
ToolSpecs before dispatch, including with custom transport validators. Choice is
part of immutable batch identity; changed choice conflicts without another input
load or submission. Absence retains the old wire and replay contract.
Successful extraction items preserve their tool_use stop and ordered tool_call
blocks, alongside original financial receipts. Inspect outcome, stop, expected name
and validate arguments before using them. Selection invokes no business handler
or personal-memory write. Read/status/collect/reconcile/cancel never infer authority
to submit again from missing content, unknown usage or an absent handle.
Batch call (spec: batch-call)
createBatchCallRunner(config) sends one structured extraction by batch and
retries it by reason, with no fallback or cancellation. Its configuration is
the governed runner's plus the call's:
policy, versions, batch-tierprices,capsandconsumers;system,tools,toolChoice,maxTokensandresultRetentionMs;- an optional pure
validate(output); retries: { provider, invalid, not_sent, uncertain }, from 0 to 4, with no defaults;retryDelayMs, andsubmitWindowMsof at most one hour.
Change configRevision whenever the retries, the wait or validate change.
The host's scheduler runs two loops:
const r = await calls.attempt({ scope, sessionId, key, inputRevision, intent, loadInput });
// pending: call again later. completed: publish r.output. failed: show r.reason.
if (r.status !== 'pending' && !r.settled) await calls.settle(scope, key); // repeat until settled
Copies at the provider (spec: provider-batch-retention). When attempt
or settle reports settled: true and the latest attempt is decided, the
runner discards every attempt's batch at the provider. No later call reads
them again. A discard that fails loses nothing and is reported to
onBackgroundError with stage discard. A client without discard keeps its
batches until the provider's own expiry.
What attempt does:
- It acts only on the latest attempt. It submits a new batch, or collects, or classifies the item.
- Attempt k+1 starts only while the reason's allowance remains, and after
k × retryDelayMs. Until then it returnspending. - Earlier attempts count against every reason their journals support, so a count never goes down.
- Run calls for one key one at a time, as a job lease or a transactional
poll claim already does. The retry table, latest-only and
settledare exact under that contract. - Even if calls overlap, there is never more than one batch per attempt, or more than five attempts.
- Before an end or a submission,
attemptreads the chain again, and decides again if something moved: a late acceptance, an erasure, or an earlier call's background work.
What ends the call:
too_long,refusal,capandinvalid_validator;unavailable: a result that was erased or expired. It is never generated again;- an allowance used up, or five attempts.
Stopping a key: stop calling attempt, and call settle until it
returns { settled: true }.
settlecollects every batch already sent, and closes a submission that died, by the store's own clock.- It submits nothing and loads no input.
- Batches in
lostnever returned a handle, and need the operator.
The host's loader should refuse a key it no longer wants: a refused load sends nothing.
A batch whose usage stays unknown or unpriced keeps the call pending until
it is reconciled.