@crouton-kit/humanloop 0.3.39 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { spawn } from 'node:child_process';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { inboxRootsDirectory, listInboxRoots } from './registry.js';
6
+ import { reconcileCompletions } from './completion.js';
7
+ import { dispatchVisualCleanup, listVisualCleanupObligationsForRoot, reconcileStaleVisualRequestsForRoot } from './visual.js';
8
+ const LEASE_STALE_MS = 300_000;
9
+ function leasePath() { return join(dirname(inboxRootsDirectory()), 'maintenance.lock'); }
10
+ function roots() { return listInboxRoots().filter((root) => root.available).map((root) => root.root); }
11
+ function leaseOwner(path) {
12
+ try {
13
+ const parsed = JSON.parse(readFileSync(join(path, 'owner.json'), 'utf8'));
14
+ return typeof parsed.pid === 'number' && Number.isInteger(parsed.pid) ? parsed.pid : undefined;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ function processIsAlive(pid) {
21
+ try {
22
+ process.kill(pid, 0);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ function staleLease(path) {
30
+ const owner = leaseOwner(path);
31
+ if (owner !== undefined)
32
+ return !processIsAlive(owner);
33
+ try {
34
+ return Date.now() - statSync(path).mtimeMs > LEASE_STALE_MS;
35
+ }
36
+ catch {
37
+ return true;
38
+ }
39
+ }
40
+ /** Start one detached repair pass. The lease makes repeated UI gestures free. */
41
+ export function kickInboxMaintenance() {
42
+ const lease = leasePath();
43
+ try {
44
+ mkdirSync(dirname(lease), { recursive: true, mode: 0o700 });
45
+ }
46
+ catch {
47
+ return;
48
+ }
49
+ try {
50
+ mkdirSync(lease, { mode: 0o700 });
51
+ }
52
+ catch {
53
+ if (!staleLease(lease))
54
+ return;
55
+ try {
56
+ rmSync(lease, { recursive: true, force: true });
57
+ mkdirSync(lease, { mode: 0o700 });
58
+ }
59
+ catch {
60
+ return;
61
+ }
62
+ }
63
+ const builtEntry = fileURLToPath(new URL('../cli.js', import.meta.url));
64
+ const sourceEntry = fileURLToPath(new URL('../cli.ts', import.meta.url));
65
+ const entry = existsSync(builtEntry) ? builtEntry : sourceEntry;
66
+ if (!existsSync(entry)) {
67
+ rmSync(lease, { recursive: true, force: true });
68
+ return;
69
+ }
70
+ // A built CLI needs no parent flags. Source-mode launches retain their tsx
71
+ // loader, but always execute Humanloop's CLI rather than the caller's
72
+ // entrypoint (which may be a test or a consuming application).
73
+ const runtimeArgs = entry.endsWith('.ts') ? process.execArgv : [];
74
+ const child = spawn(process.execPath, [...runtimeArgs, entry, 'inbox', '_maintain', '--lease', lease], {
75
+ detached: true,
76
+ stdio: 'ignore',
77
+ });
78
+ if (child.pid === undefined) {
79
+ rmSync(lease, { recursive: true, force: true });
80
+ return;
81
+ }
82
+ try {
83
+ writeFileSync(join(lease, 'owner.json'), JSON.stringify({ pid: child.pid }));
84
+ }
85
+ catch {
86
+ child.kill();
87
+ rmSync(lease, { recursive: true, force: true });
88
+ return;
89
+ }
90
+ child.unref();
91
+ }
92
+ function dueTasks(allRoots) {
93
+ const tasks = [];
94
+ for (const root of allRoots) {
95
+ try {
96
+ tasks.push(...listVisualCleanupObligationsForRoot(root));
97
+ }
98
+ catch { /* malformed historical state is isolated to its root */ }
99
+ }
100
+ return tasks;
101
+ }
102
+ async function repairOnce() {
103
+ const allRoots = roots();
104
+ for (const root of allRoots) {
105
+ try {
106
+ await reconcileCompletions(root);
107
+ }
108
+ catch { /* receipt remains durable for the next pass */ }
109
+ }
110
+ const retirements = allRoots.flatMap((root) => {
111
+ try {
112
+ return reconcileStaleVisualRequestsForRoot(root).map((entry) => entry.delivery);
113
+ }
114
+ catch {
115
+ return [];
116
+ }
117
+ });
118
+ await Promise.all(retirements);
119
+ const tasks = dueTasks(allRoots);
120
+ await Promise.all(tasks.filter((task) => Date.parse(task.nextAttemptAt) <= Date.now()).map((task) => dispatchVisualCleanup(task.root, task.dir, task.requestId)));
121
+ return dueTasks(roots());
122
+ }
123
+ function releaseLease(lease) {
124
+ if (leaseOwner(lease) !== process.pid)
125
+ return;
126
+ rmSync(lease, { recursive: true, force: true });
127
+ }
128
+ /** Run repair and stay alive only until the durable cleanup retry queue is empty. */
129
+ export async function runInboxMaintenance(lease) {
130
+ try {
131
+ while (true) {
132
+ const tasks = await repairOnce();
133
+ const next = tasks.reduce((earliest, task) => {
134
+ const due = Date.parse(task.nextAttemptAt);
135
+ return Number.isFinite(due) && (earliest === undefined || due < earliest) ? due : earliest;
136
+ }, undefined);
137
+ if (next === undefined)
138
+ return;
139
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, next - Date.now())));
140
+ }
141
+ }
142
+ finally {
143
+ releaseLease(lease);
144
+ }
145
+ }
@@ -9,6 +9,8 @@ export interface InboxRootRegistration {
9
9
  owner: string;
10
10
  handler?: CompletionHandler;
11
11
  followUpHandler?: CompletionHandler;
12
+ visualHandler?: CompletionHandler;
13
+ focusHandler?: CompletionHandler;
12
14
  }
13
15
  export interface InboxRootStatus extends InboxRootRegistration {
14
16
  available: boolean;
@@ -18,8 +20,14 @@ export interface RegisterInboxRootOptions {
18
20
  owner: string;
19
21
  handler?: CompletionHandler;
20
22
  followUpHandler?: CompletionHandler;
23
+ visualHandler?: CompletionHandler;
24
+ focusHandler?: CompletionHandler;
21
25
  }
26
+ export declare function inboxStateDirectory(): string;
22
27
  export declare function inboxRootsDirectory(): string;
28
+ export declare function inboxActivityPath(): string;
29
+ /** Signal one durable ticket mutation to every open inbox without watching every root. */
30
+ export declare function signalInboxActivity(): void;
23
31
  /** Create/canonicalize a root and claim its user-scoped registration. */
24
32
  export declare function registerInboxRoot(opts: RegisterInboxRootOptions): InboxRootRegistration;
25
33
  /** Removes a matching available root through its real path, or an unavailable record by its stored canonical path. */
@@ -1,10 +1,20 @@
1
- import { chmodSync, existsSync, mkdirSync, readdirSync, realpathSync, unlinkSync } from 'node:fs';
1
+ import { chmodSync, existsSync, mkdirSync, readdirSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { homedir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { atomicWriteJson, readJson, withExclusiveDirectoryLock } from './convention.js';
6
6
  function stateHome() { return process.env['XDG_STATE_HOME'] || join(homedir(), '.local', 'state'); }
7
- export function inboxRootsDirectory() { return join(stateHome(), 'humanloop', 'inbox-roots'); }
7
+ export function inboxStateDirectory() { return join(stateHome(), 'humanloop'); }
8
+ export function inboxRootsDirectory() { return join(inboxStateDirectory(), 'inbox-roots'); }
9
+ export function inboxActivityPath() { return join(inboxStateDirectory(), 'inbox-activity'); }
10
+ /** Signal one durable ticket mutation to every open inbox without watching every root. */
11
+ export function signalInboxActivity() {
12
+ try {
13
+ mkdirSync(inboxStateDirectory(), { recursive: true, mode: 0o700 });
14
+ writeFileSync(inboxActivityPath(), `${Date.now()}\n`, { mode: 0o600 });
15
+ }
16
+ catch { /* a later popup scan is still authoritative */ }
17
+ }
8
18
  function recordPath(root) { return join(inboxRootsDirectory(), createHash('sha256').update(root).digest('hex')); }
9
19
  function canonicalRoot(root) { try {
10
20
  return realpathSync(root);
@@ -15,8 +25,8 @@ catch {
15
25
  function validateHandler(handler) {
16
26
  if (handler === undefined)
17
27
  return undefined;
18
- if (!handler.command || !Array.isArray(handler.args) || !handler.args.every((arg) => typeof arg === 'string'))
19
- throw new Error('completion handler requires a command and string args');
28
+ if (typeof handler.command !== 'string' || handler.command.trim() === '' || !Array.isArray(handler.args) || !handler.args.every((arg) => typeof arg === 'string'))
29
+ throw new Error('handler requires a non-empty command and string args');
20
30
  return { command: handler.command, args: [...handler.args] };
21
31
  }
22
32
  function validateRegistration(raw) {
@@ -30,6 +40,8 @@ function validateRegistration(raw) {
30
40
  schema: 'humanloop.inbox-root/v1', root: value.root, owner: value.owner,
31
41
  handler: value.handler === undefined ? undefined : validateHandler(value.handler),
32
42
  followUpHandler: value.followUpHandler === undefined ? undefined : validateHandler(value.followUpHandler),
43
+ visualHandler: value.visualHandler === undefined ? undefined : validateHandler(value.visualHandler),
44
+ focusHandler: value.focusHandler === undefined ? undefined : validateHandler(value.focusHandler),
33
45
  };
34
46
  }
35
47
  catch {
@@ -50,7 +62,7 @@ export function registerInboxRoot(opts) {
50
62
  throw new Error(`inbox root is already owned by ${existing.owner}`);
51
63
  if (existing !== null && existing.root !== root)
52
64
  throw new Error('inbox root registry hash collision');
53
- const registration = { schema: 'humanloop.inbox-root/v1', root, owner: opts.owner, handler: validateHandler(opts.handler), followUpHandler: validateHandler(opts.followUpHandler) };
65
+ const registration = { schema: 'humanloop.inbox-root/v1', root, owner: opts.owner, handler: validateHandler(opts.handler), followUpHandler: validateHandler(opts.followUpHandler), visualHandler: validateHandler(opts.visualHandler), focusHandler: validateHandler(opts.focusHandler) };
54
66
  atomicWriteJson(path, registration);
55
67
  chmodSync(path, 0o600);
56
68
  return registration;
@@ -1,6 +1,11 @@
1
1
  import type { Deck, DeckTicketResult, FeedbackResult, ReviewDescriptor, TicketResult } from '../types.js';
2
2
  /** Strict decoder for the only canonical final marker. */
3
3
  export declare function readTicketResult(dirOrResponsePath: string): TicketResult | null;
4
+ /** Canonical registered-root/direct-child containment shared by host protocols. */
5
+ export declare function requireCanonicalTicket(root: string, dir: string): {
6
+ root: string;
7
+ dir: string;
8
+ };
4
9
  export interface SubmitDeckOptions {
5
10
  root: string;
6
11
  id: string;
@@ -1,12 +1,14 @@
1
- import { existsSync, linkSync, mkdirSync, readFileSync, realpathSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'node:fs';
2
2
  import { basename, dirname, isAbsolute, resolve } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  import { buildSummary } from '../summary.js';
5
- import { clearProgress, claimPath, deckPath, deliveryErrorPath, deliveryPath, followupRequestPath, followupResultPath, progressPath, responsePath, reviewPath } from './convention.js';
5
+ import { clearProgress, claimPath, deckPath, deliveryErrorPath, deliveryPath, followupRequestPath, followupResultPath, progressPath, publishJsonExclusive, responsePath, reviewPath, visualsDir } from './convention.js';
6
6
  import { validateDeck, validateReviewDescriptor, validateReviewProjection, resolveDeckBodyPaths } from './deck-schema.js';
7
- import { registeredInboxRoot } from './registry.js';
7
+ import { registeredInboxRoot, signalInboxActivity } from './registry.js';
8
8
  import { readTicketClaim, releaseClaimLocked, withTicketLock } from './claim.js';
9
9
  import { dispatchCompletion } from './completion.js';
10
+ import { cancelVisualRequestsForTicket } from './visual.js';
11
+ import { kickInboxMaintenance } from './maintenance.js';
10
12
  const ticketId = z.string().regex(/^[A-Za-z0-9_-]+$/).min(1).max(128);
11
13
  const responseSchema = z.object({ id: z.string().min(1), selectedOptionId: z.string().optional(), selectedOptionIds: z.array(z.string()).optional(), freetext: z.string().optional(), optionComments: z.record(z.string(), z.string()).optional() }).strict();
12
14
  const feedbackCommentSchema = z.object({ id: z.string().min(1), line: z.number().int().positive(), endLine: z.number().int().positive(), quote: z.string().optional(), colStart: z.number().int().nonnegative().optional(), colEnd: z.number().int().nonnegative().optional(), lineText: z.string(), comment: z.string().min(1), createdAt: z.string().min(1) }).strict();
@@ -70,7 +72,7 @@ function ticketDir(root, id) {
70
72
  function discardCreatedTicket(dir, created) { if (created)
71
73
  rmSync(dir, { recursive: true, force: true }); }
72
74
  function hasTicketProtocolState(dir) {
73
- return [deckPath(dir), reviewPath(dir), responsePath(dir), progressPath(dir), claimPath(dir), deliveryPath(dir), deliveryErrorPath(dir), followupRequestPath(dir), followupResultPath(dir)].some(existsSync);
75
+ return [deckPath(dir), reviewPath(dir), responsePath(dir), progressPath(dir), claimPath(dir), deliveryPath(dir), deliveryErrorPath(dir), followupRequestPath(dir), followupResultPath(dir), visualsDir(dir)].some(existsSync);
74
76
  }
75
77
  function requireRegisteredTicket(dir) {
76
78
  const canonical = realpathSync(dir);
@@ -82,21 +84,17 @@ function requireRegisteredTicket(dir) {
82
84
  throw new Error('ticket has no request descriptor');
83
85
  return { root, dir: canonical };
84
86
  }
87
+ /** Canonical registered-root/direct-child containment shared by host protocols. */
88
+ export function requireCanonicalTicket(root, dir) {
89
+ const registration = registeredInboxRoot(root);
90
+ const ticket = requireRegisteredTicket(dir);
91
+ if (registration === null || ticket.root !== registration.root)
92
+ throw new Error('ticket is not a canonical direct child of the registered root');
93
+ return ticket;
94
+ }
85
95
  function publishRequest(path, value) {
86
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
87
- const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
88
- writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
89
- try {
90
- linkSync(temp, path);
91
- }
92
- catch (error) {
93
- if (error.code === 'EEXIST')
94
- throw new Error(`ticket request already exists: ${path}`);
95
- throw error;
96
- }
97
- finally {
98
- unlinkSync(temp);
99
- }
96
+ if (!publishJsonExclusive(path, value))
97
+ throw new Error(`ticket request already exists: ${path}`);
100
98
  }
101
99
  export function submitDeck(opts) {
102
100
  // Validate shape before mutating caller-owned interaction directories.
@@ -108,6 +106,7 @@ export function submitDeck(opts) {
108
106
  const deck = validateDeck(resolveDeckBodyPaths(opts.deck, dir));
109
107
  const stamped = { ...deck, source: { ...(deck.source ?? {}), blockedSince: deck.source?.blockedSince ?? new Date().toISOString() } };
110
108
  publishRequest(deckPath(dir), stamped);
109
+ signalInboxActivity();
111
110
  return { id: opts.id, dir, kind: 'deck' };
112
111
  }
113
112
  catch (error) {
@@ -127,6 +126,7 @@ export function submitReview(opts) {
127
126
  throw new Error(`ticket protocol state already exists: ${dir}`);
128
127
  const descriptor = validateReviewProjection(dir, { schema: 'humanloop.review/v1', file: source, output: resolve(opts.review.output ?? `${dir}/feedback.json`), title: opts.review.title, source: opts.review.source, blockedSince: opts.review.blockedSince ?? new Date().toISOString() });
129
128
  publishRequest(reviewPath(dir), descriptor);
129
+ signalInboxActivity();
130
130
  return { id: opts.id, dir, kind: 'review' };
131
131
  }
132
132
  catch (error) {
@@ -135,21 +135,7 @@ export function submitReview(opts) {
135
135
  }
136
136
  }
137
137
  function exclusiveResult(dir, result) {
138
- const path = responsePath(dir);
139
- const temp = `${path}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
140
- writeFileSync(temp, `${JSON.stringify(result, null, 2)}\n`, { mode: 0o600 });
141
- try {
142
- linkSync(temp, path);
143
- return true;
144
- }
145
- catch (error) {
146
- if (error.code === 'EEXIST')
147
- return false;
148
- throw error;
149
- }
150
- finally {
151
- unlinkSync(temp);
152
- }
138
+ return publishJsonExclusive(responsePath(dir), result);
153
139
  }
154
140
  function requireDeck(dir) { return validateDeck(JSON.parse(readFileSync(deckPath(dir), 'utf8'))); }
155
141
  function requireReview(dir) { return validateReviewDescriptor(JSON.parse(readFileSync(reviewPath(dir), 'utf8'))); }
@@ -187,19 +173,26 @@ function requireClaimOwnership(dir, token) {
187
173
  function clearOwnedWork(dir, claimToken) { clearProgress(dir); releaseClaimLocked(dir, claimToken); }
188
174
  export function finalizeDeck(dir, responses, claimToken, completedAt = new Date().toISOString()) {
189
175
  const ticket = requireRegisteredTicket(dir);
190
- return withTicketLock(ticket.dir, () => {
176
+ const finalized = withTicketLock(ticket.dir, () => {
191
177
  requireClaimOwnership(ticket.dir, claimToken);
192
178
  const deck = requireDeck(ticket.dir);
193
179
  const parsedResponses = validateDeckResponses(deck, responses);
180
+ // A valid primary result ends its panel generation before the canonical
181
+ // response can cross the owner boundary. The protocol persists cancellation
182
+ // first; its independent cleanup executor owns eventual handler delivery.
183
+ void cancelVisualRequestsForTicket(ticket.root, ticket.dir).finally(kickInboxMaintenance);
194
184
  const result = { schema: 'humanloop.response/v2', kind: 'deck', responses: parsedResponses, summary: buildSummary(deck, parsedResponses), completedAt };
195
185
  const won = exclusiveResult(ticket.dir, result);
196
186
  clearOwnedWork(ticket.dir, claimToken);
197
187
  return { won, result: won ? result : readTicketResult(ticket.dir) ?? result };
198
188
  });
189
+ if (finalized.won)
190
+ signalInboxActivity();
191
+ return finalized;
199
192
  }
200
193
  export function finalizeReview(dir, feedback, claimToken, completedAt = new Date().toISOString()) {
201
194
  const ticket = requireRegisteredTicket(dir);
202
- return withTicketLock(ticket.dir, () => {
195
+ const finalized = withTicketLock(ticket.dir, () => {
203
196
  requireClaimOwnership(ticket.dir, claimToken);
204
197
  const descriptor = requireReview(ticket.dir);
205
198
  const parsed = feedbackSchema.parse(feedback);
@@ -210,14 +203,20 @@ export function finalizeReview(dir, feedback, claimToken, completedAt = new Date
210
203
  clearOwnedWork(ticket.dir, claimToken);
211
204
  return { won, result: won ? result : readTicketResult(ticket.dir) ?? result, descriptor };
212
205
  });
206
+ if (finalized.won)
207
+ signalInboxActivity();
208
+ return finalized;
213
209
  }
214
210
  export function cancelTicketResult(dir, opts = {}) {
215
211
  const ticket = requireRegisteredTicket(dir);
216
- return withTicketLock(ticket.dir, () => {
212
+ const canceled = withTicketLock(ticket.dir, () => {
217
213
  const result = { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: new Date().toISOString(), ...(opts.reason === undefined ? {} : { reason: opts.reason }), ...(opts.actor === undefined ? {} : { actor: opts.actor }) };
218
214
  const won = exclusiveResult(ticket.dir, result);
219
215
  return { status: won ? 'canceled' : 'already_resolved', result: won ? result : readTicketResult(ticket.dir) ?? result };
220
216
  });
217
+ if (canceled.status === 'canceled')
218
+ signalInboxActivity();
219
+ return canceled;
221
220
  }
222
221
  export function ticketRoot(dir) {
223
222
  try {
@@ -0,0 +1,130 @@
1
+ import type { CanonicalInteraction, Interaction, VisualRequest as PanelVisualRequest } from '../types.js';
2
+ import { type CompletionHandler } from './registry.js';
3
+ export declare const VISUAL_CAPABILITY: "humanloop.visual/v1";
4
+ export interface VisualClaimIdentity {
5
+ token: string;
6
+ host: string;
7
+ pid: number;
8
+ claimedAt: string;
9
+ }
10
+ export interface VisualCleanupObligation {
11
+ reason: 'canceled' | 'unreceipted_start_error';
12
+ attempts: number;
13
+ nextAttemptAt: string;
14
+ lastAttemptAt?: string;
15
+ lastError?: string;
16
+ }
17
+ interface VisualBinding {
18
+ capability: typeof VISUAL_CAPABILITY;
19
+ owner: string;
20
+ root: string;
21
+ dir: string;
22
+ ticketId: string;
23
+ requestId: string;
24
+ generationId: string;
25
+ interactionId: string;
26
+ interaction: CanonicalInteraction;
27
+ claim: VisualClaimIdentity;
28
+ handler: CompletionHandler;
29
+ }
30
+ export interface VisualProtocolRequest extends VisualBinding {
31
+ schema: 'humanloop.visual-request/v1';
32
+ state: 'running' | 'canceled' | 'terminal';
33
+ requestedAt: string;
34
+ settledAt?: string;
35
+ cleanup?: VisualCleanupObligation;
36
+ }
37
+ export interface VisualRequestEvent extends VisualBinding {
38
+ schema: 'humanloop.visual-request-event/v1';
39
+ action: 'start' | 'cancel';
40
+ }
41
+ interface VisualResultBinding extends VisualBinding {
42
+ schema: 'humanloop.visual-result/v1';
43
+ completedAt: string;
44
+ }
45
+ export type VisualProtocolResult = (VisualResultBinding & {
46
+ status: 'ready';
47
+ markdown: string;
48
+ }) | (VisualResultBinding & {
49
+ status: 'error';
50
+ error: string;
51
+ });
52
+ export type VisualResultSubmission = {
53
+ requestId: string;
54
+ generationId: string;
55
+ interactionId: string;
56
+ interaction: CanonicalInteraction;
57
+ claimToken: string;
58
+ status: 'ready';
59
+ markdown: string;
60
+ } | {
61
+ requestId: string;
62
+ generationId: string;
63
+ interactionId: string;
64
+ interaction: CanonicalInteraction;
65
+ claimToken: string;
66
+ status: 'error';
67
+ error: string;
68
+ };
69
+ export interface StartVisualRequestOptions {
70
+ root: string;
71
+ dir: string;
72
+ claimToken: string;
73
+ request: PanelVisualRequest;
74
+ }
75
+ export type VisualStartDeliveryResult = 'delivered' | 'already_attempted' | 'ineligible' | 'failed';
76
+ export type VisualCleanupDeliveryResult = 'delivered' | 'pending' | 'none';
77
+ export interface StartedVisualRequest {
78
+ request: VisualProtocolRequest;
79
+ /** Resolves after the one permitted start attempt reaches a determinate outcome. */
80
+ delivery: Promise<VisualStartDeliveryResult>;
81
+ }
82
+ export interface VisualCleanupTask {
83
+ root: string;
84
+ dir: string;
85
+ requestId: string;
86
+ reason: VisualCleanupObligation['reason'];
87
+ nextAttemptAt: string;
88
+ }
89
+ /** Normalize one Interaction into the sole persisted/event correlation shape. */
90
+ export declare function canonicalizeInteraction(raw: Interaction | CanonicalInteraction): CanonicalInteraction;
91
+ /** Stable bytes used for every interaction equality check across the protocol. */
92
+ export declare function canonicalInteractionJson(raw: Interaction | CanonicalInteraction): string;
93
+ export declare function parseVisualRequestEvent(raw: unknown): VisualRequestEvent;
94
+ /** Strict, repairing reader; malformed or unbound request bytes return null. */
95
+ export declare function readVisualRequest(root: string, dir: string, requestId: string): VisualProtocolRequest | null;
96
+ /** Strict, repairing reader; canceled and mismatched results are never returned. */
97
+ export declare function readVisualResult(root: string, dir: string, requestId: string): VisualProtocolResult | null;
98
+ /** Decode an event and atomically bind it to Humanloop's immutable request state. */
99
+ export declare function readVisualRequestForEvent(raw: unknown): VisualProtocolRequest | null;
100
+ /** Persist one claim-bound request and dispatch its frozen handler at most once. */
101
+ export declare function startVisualRequest(opts: StartVisualRequestOptions): StartedVisualRequest;
102
+ /** Compare-publish the first fully correlated ready/error result. Stale writers are no-ops. */
103
+ export declare function submitVisualResult(root: string, dir: string, raw: VisualResultSubmission): {
104
+ published: boolean;
105
+ };
106
+ /** Attempt one due cleanup delivery through the request's frozen handler. */
107
+ export declare function dispatchVisualCleanup(root: string, dir: string, requestId: string): Promise<VisualCleanupDeliveryResult>;
108
+ /** State-first cancellation for one handle; the returned promise is cleanup delivery only. */
109
+ export declare function cancelVisualRequest(root: string, dir: string, requestId: string): Promise<VisualCleanupDeliveryResult>;
110
+ /** State-first cancel every running request, then dispatch every owed cleanup (including start failures). */
111
+ export declare function cancelVisualRequestsForTicket(root: string, dir: string): Promise<{
112
+ canceled: number;
113
+ cleanupOwed: number;
114
+ }>;
115
+ /** Durable tasks the next controller-owned retry executor should schedule. */
116
+ export declare function listVisualCleanupObligations(root: string, dir: string): VisualCleanupTask[];
117
+ export interface VisualRequestReconciliation {
118
+ retired: number;
119
+ cleanupOwed: number;
120
+ /** Cleanup delivery only; reconciliation never redelivers a start. */
121
+ delivery: Promise<VisualCleanupDeliveryResult[]>;
122
+ }
123
+ /** Retire abandoned generations under the ticket claim boundary. A live claim is
124
+ * authoritative; a newly acquired claim retires every older generation first. */
125
+ export declare function reconcileVisualRequestsForTicket(root: string, dir: string, currentClaimToken?: string): VisualRequestReconciliation;
126
+ /** Enumerate direct ticket children only, including resolved tickets whose cleanup is still owed. */
127
+ export declare function listVisualCleanupObligationsForRoot(root: string): VisualCleanupTask[];
128
+ /** Startup reconciliation only retires stale/missing-claim work; live owners stay untouched. */
129
+ export declare function reconcileStaleVisualRequestsForRoot(root: string): VisualRequestReconciliation[];
130
+ export {};