mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
* fix(orcad): identify instances by kernel start ticks, not drifting wall-clock start time On Linux the start time is derived from /proc/stat btime, which drifts (WSL2, NTP), so a live orcad was read as a reused PID and its managed stop reported exited without ever writing the request. The lock and stop request now carry optional linuxStartTicks + bootId; readers prefer them, and a wall-clock mismatch on Linux for records without them is unverifiable, never proof of exit. Co-Authored-By: Claude <noreply@anthropic.com> * fix(orcad): match stop receipts without the incarnation an older client drops A request recovered by an older client loses linuxStartTicks/bootId, which made the newer host reject its own receipts and decision records as mismatched. Also exercise the incarnation reader against a real Linux /proc. --------- Co-authored-by: Claude <noreply@anthropic.com>
130 lines
6.3 KiB
TypeScript
130 lines
6.3 KiB
TypeScript
/**
|
|
* Stop requests for a running orcad, as files beside the process they address.
|
|
*
|
|
* Why files and not signals: a PID can be reused once its process exits, so a signal sent
|
|
* from a stale record can stop an unrelated process. A file in the slot or data root reaches
|
|
* only the orcad that watches it.
|
|
*/
|
|
import { z } from 'zod'
|
|
import { openEnum } from './zod-salvage'
|
|
|
|
/** Plain request in the slot directory: the same graceful stop as SIGTERM. */
|
|
export const ORCAD_STOP_REQUEST_FILENAME = '.orcad-stop-request'
|
|
/** Prefix of an instance-bound request in the data root; older listeners never match it. */
|
|
export const ORCAD_MANAGED_STOP_REQUEST_PREFIX = '.orcad-managed-stop-request'
|
|
export const ORCAD_MANAGED_STOP_REQUEST_MAX_BYTES = 32 * 1024
|
|
export const ORCAD_STOP_RECEIPTS_DIRNAME = 'orcad-stop-receipts'
|
|
export const ORCAD_COMPLETE_MANAGED_STOP_FLAG = '--complete-managed-stop'
|
|
/** Names a staged request file instead of inline JSON; Windows hosts never put the JSON on argv. */
|
|
export const ORCAD_MANAGED_STOP_REQUEST_FILE_FLAG = '--request-file'
|
|
|
|
/** One orcad process: its PID, start time, and the instance lock record it published. */
|
|
export const OrcadManagedStopInstanceSchema = z.object({
|
|
pid: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
startedAtMs: z.number().finite().nonnegative().nullable(),
|
|
/** Linux only, copied from the lock; older clients omit it and older slots drop it. */
|
|
linuxStartTicks: z.string().regex(/^\d+$/).max(32).optional().catch(undefined),
|
|
bootId: z.string().min(1).max(255).optional().catch(undefined),
|
|
nonce: z.string().min(1).max(255),
|
|
lockPath: z.string().min(1).max(4096)
|
|
})
|
|
|
|
// Not strict: the slot's own (possibly older) build parses what a newer client wrote, so an
|
|
// unknown optional field is dropped rather than refusing the stop.
|
|
export const OrcadManagedStopRequestSchema = z.object({
|
|
schemaVersion: z.literal(1),
|
|
transactionId: z.uuid(),
|
|
version: z.string().min(1).max(255),
|
|
runtimeId: z.string().min(1).max(255),
|
|
instance: OrcadManagedStopInstanceSchema,
|
|
/** Best effort: also retire the terminal daemon if it is provably idle. Never blocks the stop. */
|
|
retireIdleDaemon: z.literal(true).optional()
|
|
})
|
|
|
|
export type OrcadManagedStopInstance = z.infer<typeof OrcadManagedStopInstanceSchema>
|
|
export type OrcadManagedStopRequest = z.infer<typeof OrcadManagedStopRequestSchema>
|
|
|
|
/** Equality key for a request; omits the incarnation, which an older client recovering it drops. */
|
|
export function orcadManagedStopRequestKey(request: OrcadManagedStopRequest): string {
|
|
const { instance, ...rest } = OrcadManagedStopRequestSchema.parse(request)
|
|
const { linuxStartTicks: _ticks, bootId: _bootId, ...bound } = instance
|
|
return JSON.stringify({ ...rest, instance: bound })
|
|
}
|
|
/** What a stopping orcad must match before a managed request may stop it. */
|
|
export type OrcadManagedStopContext = Omit<
|
|
OrcadManagedStopRequest,
|
|
'schemaVersion' | 'transactionId'
|
|
>
|
|
|
|
const STOP_VERDICTS = ['live', 'unverifiable', 'exited'] as const
|
|
const RETIREMENT_VERDICTS = ['retired', 'live', 'unverifiable'] as const
|
|
|
|
/** The execution host's verdict; `exited` only on proof, never on silence. */
|
|
export const OrcadManagedStopVerdictSchema = z.enum(STOP_VERDICTS)
|
|
|
|
/** `retired` only when the daemon accepted; a busy daemon stays up and keeps its terminals. */
|
|
export const OrcadDaemonRetirementVerdictSchema = z.enum(RETIREMENT_VERDICTS)
|
|
|
|
// Read by clients older than the slot that printed them: unknown fields and arms degrade.
|
|
const stopReplyRequestFields = OrcadManagedStopRequestSchema.shape
|
|
|
|
export const OrcadManagedStopCompletionSchema = z.object({
|
|
...stopReplyRequestFields,
|
|
kind: z.literal('orcad_managed_stop_completion'),
|
|
verdict: openEnum(STOP_VERDICTS, 'unverifiable'),
|
|
receiptPersisted: z.boolean(),
|
|
/** For a request that asked to retire the daemon, the outcome its receipt recorded. */
|
|
retirement: openEnum(RETIREMENT_VERDICTS, 'unverifiable').optional()
|
|
})
|
|
|
|
/** What the stopping orcad observed about its daemon, written before it exits. */
|
|
export const OrcadDaemonRetirementRecordSchema = z.strictObject({
|
|
schemaVersion: z.literal(1),
|
|
kind: z.literal('orcad_managed_stop_retirement'),
|
|
request: OrcadManagedStopRequestSchema,
|
|
retirement: OrcadDaemonRetirementVerdictSchema,
|
|
liveSessions: z.number().int().nonnegative().nullable(),
|
|
reason: z.string().max(4096).nullable()
|
|
})
|
|
|
|
export const OrcadCompletedStopReceiptSchema = z.strictObject({
|
|
schemaVersion: z.literal(1),
|
|
kind: z.literal('orcad_managed_stop_completed'),
|
|
request: OrcadManagedStopRequestSchema,
|
|
exitedAt: z.iso.datetime({ offset: true }),
|
|
/** Present only when the request asked to retire the daemon. */
|
|
retirement: OrcadDaemonRetirementVerdictSchema.optional()
|
|
})
|
|
|
|
export type OrcadManagedStopVerdict = z.infer<typeof OrcadManagedStopVerdictSchema>
|
|
export type OrcadManagedStopCompletion = z.infer<typeof OrcadManagedStopCompletionSchema>
|
|
export type OrcadCompletedStopReceipt = z.infer<typeof OrcadCompletedStopReceiptSchema>
|
|
export type OrcadDaemonRetirementVerdict = z.infer<typeof OrcadDaemonRetirementVerdictSchema>
|
|
export type OrcadDaemonRetirementRecord = z.infer<typeof OrcadDaemonRetirementRecordSchema>
|
|
|
|
export const ORCAD_CANCEL_MANAGED_STOP_FLAG = '--cancel-managed-stop'
|
|
/** Readiness `health.stopRequests`: this build consumes stop files and both commands above. */
|
|
export const ORCAD_STOP_REQUESTS_CAPABILITY = 1
|
|
|
|
/**
|
|
* Which side won a managed request: the running orcad acting on it, or a client cancelling it.
|
|
* Created exclusively once per transaction, so the two can never both believe they won.
|
|
*/
|
|
export const OrcadManagedStopDecisionSchema = z.strictObject({
|
|
schemaVersion: z.literal(1),
|
|
kind: z.literal('orcad_managed_stop_decision'),
|
|
request: OrcadManagedStopRequestSchema,
|
|
decision: z.enum(['dispatched', 'canceled'])
|
|
})
|
|
|
|
/** `dispatched` means orcad already acted on the request; the caller must await its exit. */
|
|
export const OrcadManagedStopCancellationSchema = z.object({
|
|
...stopReplyRequestFields,
|
|
kind: z.literal('orcad_managed_stop_cancellation'),
|
|
// Unknown degrades to `dispatched`: the caller then awaits exit rather than assume a cancel.
|
|
outcome: openEnum(['canceled', 'dispatched'], 'dispatched')
|
|
})
|
|
|
|
export type OrcadManagedStopDecision = z.infer<typeof OrcadManagedStopDecisionSchema>
|
|
export type OrcadManagedStopCancellation = z.infer<typeof OrcadManagedStopCancellationSchema>
|