/** * sync-client.ts — durable local queue + upload for syncing LoopRecords to a hosted Control Plane. * * Opt-in: silent no-op unless MARTIN_TELEMETRY_ENDPOINT and MARTIN_API_TOKEN are set. * * Contracts: * syncLoopToHosted — never throws; all errors are caught and logged to stderr. * flushSyncQueue — may throw on unrecoverable filesystem errors (permission denied, etc.). * syncQueueStatus — may throw on unrecoverable filesystem errors. * * Verified server contract (POST /api/runs/sync): * Auth: Authorization: Bearer martin_cp_ (CP-issued credential, "ingest" scope) * Dedup: Server deduplicates by (tenantId, loopId). Duplicate events within a run are * deduplicated by eventId. Duplicate sync → 202 { ok: true, replayedEvents: N, * acceptedEvents: 0 } — treated as success. * 401: Bad/missing/revoked token — permanent, do not retry. * 403: Missing "ingest" scope — permanent, do not retry. * 400: Invalid payload (missing loopId, empty events, bad schema) — permanent. * 409: Backdated syncedAt (earlier than existing lastSyncedAt) — permanent, do not retry. * 429: Rate limit — transient; respect Retry-After if present. * 5xx: Server error — transient, retry. * * Failure modes: * Transient (timeout, offline, 429, 5xx) → item stays in queue for flushSyncQueue(). * Permanent (4xx exc. 429) → item moved to quarantine dir with reason. * Queue full (200 items) → oldest by enqueuedAt quarantined; if quarantine * fails, new item is NOT enqueued (error logged). * Corrupt queue file → quarantined; skipped if quarantine fails. * Oversized payload (> 256 KB) → rejected before enqueue; logged to stderr. * * Concurrent process safety: * Items are claimed via atomic rename to .inflight/ before upload. * Only the process that wins the rename proceeds to upload. * Stale .inflight items (from crashed processes) are recovered at flush start. * * Attempt persistence: * attempts and nextRetryNotBefore are persisted to the queue file after each attempt. * FLUSH_MAX_ATTEMPTS is a lifetime cap enforced across separate invocations. */ import type { LoopRecord } from "../contracts/index.js"; /** Portable basename — handles both forward and backslash separators on all platforms. @internal */ export declare function queueFileName(filePath: string): string; interface HostedRunEventDraft { eventId: string; eventType: string; occurredAt: string; sequence: number; attemptId?: string; payload?: Record; } interface CoreReceiptIntegrityMaterial { schemaVersion: "martin.receipt-integrity.v1"; runId: string; keyId: string; signedAt: string; scope?: Record; loopRecordSha256: string; ledgerSha256: string; ledgerHeadHash: string; entryCount: number; chain: Array>; signatureHmacSha256: string; } interface CoreReceiptBundle { loopRecord: Record; ledgerEntries: Array>; integrity: CoreReceiptIntegrityMaterial; } interface HostedRunSyncDraft { loopId: string; workspaceId?: string; projectId?: string; task: { title: string; objective: string; }; status?: string; budget?: { spentUsd?: number; avoidedUsd?: number; }; receiptScope?: Record; receiptIntegrity?: CoreReceiptIntegrityMaterial; events: HostedRunEventDraft[]; syncedAt?: string; coreReceipt?: CoreReceiptBundle; } interface SyncQueueItem { queueId: string; loopId: string; payload: HostedRunSyncDraft; enqueuedAt: string; attempts: number; lastAttemptAt?: string; nextRetryNotBefore?: string; payloadBytes: number; } type UploadResult = { ok: true; } | { ok: false; permanent: boolean; retryAfterMs?: number; }; /** * @internal Exported for targeted HTTP behavior tests only. */ export declare function attemptUpload(item: SyncQueueItem, endpoint: string, token: string): Promise; /** * Atomically writes a LoopRecord to the local sync queue. This is the durability * guarantee — the record is persisted before this function returns. * * Must be awaited by the caller. Never throws — errors are caught and logged to * stderr so the governed run output is never blocked. * * Opt-in: silent no-op when MARTIN_TELEMETRY_ENDPOINT or MARTIN_API_TOKEN are unset. * Use `martin sync flush` or the background flush in index.ts to upload. */ export declare function enqueueLoopForHostedSync(loop: LoopRecord, opts: { runtimeVersion: string; }): Promise; /** * Enqueues a LoopRecord and immediately attempts an upload to the hosted Control Plane. * * Never throws — all errors are caught and logged to stderr. * On transient failure the item stays queued for `martin sync flush`. * On permanent failure (4xx exc. 429) the item is quarantined with a diagnostic. * * Used in tests that exercise the full enqueue + upload path in one call. * In production, index.ts uses enqueueLoopForHostedSync + flushSyncQueue separately. */ export declare function syncLoopToHosted(loop: LoopRecord, opts: { runtimeVersion: string; }): Promise; /** * Processes the sync queue: recovers stale inflight items, then for each eligible item * (not within backoff window, under attempt cap) attempts one upload. * * Attempt count and backoff are persisted — multiple flush invocations count toward * the FLUSH_MAX_ATTEMPTS lifetime cap per item, not per invocation. * * May throw on unrecoverable filesystem errors (permission denied, disk full, etc.). * Called by `martin sync flush`. */ export declare function flushSyncQueue(): Promise; /** * Prints the current sync queue and quarantine state. * May throw on unrecoverable filesystem errors. * Called by `martin sync status`. */ export declare function syncQueueStatus(): Promise; export {};