@@ -17,7 +17,7 @@ import {
1717import { runOverrides , type OutputFormat , type SharedOptions } from "./options.js"
1818import { initialProjection , reduceSession } from "./projection.js"
1919import { createRenderer } from "./render.js"
20- import { defaultStorageRoot , HostClient } from "./supervisor.js"
20+ import { defaultStorageRoot , HostClient , ZooClientError } from "./supervisor.js"
2121
2222type AutomationOptions = Omit < SharedOptions , "timeout" > & {
2323 format : OutputFormat
@@ -54,7 +54,15 @@ export async function runAutomation(
5454 const makeResult = (
5555 outcome : "needs_input" | "cancelled" | "timed_out" | "failed" ,
5656 rootTaskId : string ,
57- input : { currentTaskId ?: string ; resumable : boolean ; code ?: ZooErrorCode ; message ?: string ; content ?: string } ,
57+ input : {
58+ currentTaskId ?: string
59+ resumable : boolean
60+ code ?: ZooErrorCode
61+ message ?: string
62+ content ?: string
63+ kind ?: "configuration" | "provider" | "runtime"
64+ phase ?: string
65+ } ,
5866 ) : ZooRunResult =>
5967 zooRunResultSchema . parse ( {
6068 schemaVersion : ZOO_PUBLIC_SCHEMA_VERSION ,
@@ -68,7 +76,12 @@ export async function runAutomation(
6876 content : input . content ,
6977 error :
7078 outcome === "failed" || outcome === "timed_out"
71- ? { code : input . code ?? "task_failed" , message : input . message ?? "Task failed" , kind : "runtime" }
79+ ? {
80+ code : input . code ?? "task_failed" ,
81+ message : input . message ?? "Task failed" ,
82+ kind : input . kind ?? "runtime" ,
83+ phase : input . phase ,
84+ }
7285 : undefined ,
7386 elapsedMs : Date . now ( ) - startedAt ,
7487 cancellationReason : outcome === "cancelled" ? "signal" : undefined ,
@@ -106,6 +119,18 @@ export async function runAutomation(
106119 let clientStopped = false
107120 const deadline = options . timeout === undefined ? undefined : startedAt + options . timeout
108121 const remainingDeadline = ( ) => ( deadline === undefined ? 7_000 : Math . max ( 0 , deadline - Date . now ( ) ) )
122+ const commandBudget = ( ) => {
123+ const remaining = remainingDeadline ( )
124+ if ( remaining <= 0 ) throw new ZooClientError ( "task_timed_out" , "Task deadline exceeded" )
125+ return Math . max ( 1 , Math . min ( 15_000 , remaining ) )
126+ }
127+ const runCommand = < T > ( operation : Promise < T > ) =>
128+ Promise . race ( [
129+ operation ,
130+ signalPromise . then ( ( ) => {
131+ throw new ZooClientError ( "cancel_failed" , `Received ${ signal } ` )
132+ } ) ,
133+ ] )
109134 const renderFinal = ( result : ZooRunResult ) => {
110135 if ( finalRendered ) return
111136 finalRendered = true
@@ -138,7 +163,11 @@ export async function runAutomation(
138163 : result . outcome === "cancelled"
139164 ? { outcome : "cancelled" , signal }
140165 : result . outcome === "timed_out"
141- ? { outcome : "timed_out" , errorCode : result . error ?. code === "cleanup_timed_out" ? "cleanup_timed_out" : "task_timed_out" }
166+ ? {
167+ outcome : "timed_out" ,
168+ errorCode :
169+ result . error ?. code === "cleanup_timed_out" ? "cleanup_timed_out" : "task_timed_out" ,
170+ }
142171 : { outcome : result . outcome } ,
143172 )
144173 }
@@ -163,7 +192,7 @@ export async function runAutomation(
163192 if ( options . timeout !== undefined ) timeout = setTimeout ( ( ) => notifyTimeout ?.( ) , options . timeout )
164193 try {
165194 const startup = await Promise . race ( [
166- client . start ( ) . then ( ( ) => "started" as const ) ,
195+ client . start ( remainingDeadline ( ) ) . then ( ( ) => "started" as const ) ,
167196 timeoutPromise ,
168197 signalPromise . then ( ( ) => "signal" as const ) ,
169198 ] )
@@ -182,28 +211,35 @@ export async function runAutomation(
182211 overrides,
183212 }
184213 } else {
185- let taskId = request . taskId
186- if ( ! taskId ) {
187- const history = await client . command ( { type : "history.list" , workspace : options . workspace } )
188- if ( history . data . commandType !== "history.list" || history . data . tasks . length === 0 ) {
189- throw new Error ( "No session exists for this workspace" )
190- }
191- taskId = history . data . tasks [ 0 ] ! . rootTaskId
214+ const history = await runCommand (
215+ client . command ( { type : "history.list" , workspace : options . workspace } , commandBudget ( ) ) ,
216+ )
217+ if ( history . data . commandType !== "history.list" || history . data . tasks . length === 0 ) {
218+ throw new ZooClientError ( "invalid_session" , "No session exists for this workspace" )
192219 }
193- command = { type : "task.resume" , taskId, rootTaskId : taskId , overrides }
220+ const selected = request . taskId
221+ ? history . data . tasks . find (
222+ ( task ) => task . rootTaskId === request . taskId || task . currentTaskId === request . taskId ,
223+ )
224+ : history . data . tasks [ 0 ]
225+ if ( ! selected ) throw new ZooClientError ( "invalid_session" , `Unknown session ${ request . taskId } ` )
226+ const taskId = request . taskId === selected . currentTaskId ? request . taskId : selected . currentTaskId
227+ command = { type : "task.resume" , taskId, rootTaskId : selected . rootTaskId , overrides }
194228 }
195- const accepted = await client . command ( command )
229+ const accepted = await runCommand ( client . command ( command , commandBudget ( ) ) )
196230 if ( accepted . data . commandType !== "task.start" && accepted . data . commandType !== "task.resume" ) {
197231 throw new Error ( "Host returned an invalid task acceptance" )
198232 }
199233 rootTaskId = accepted . data . task . rootTaskId
200234 if ( signal ) void client . command ( { type : "task.cancel" , rootTaskId, reason : "signal" } ) . catch ( ( ) => undefined )
201235 const localSettlement = Promise . race ( [
202236 timeoutPromise . then ( ( ) => {
203- void client . command ( { type : "task.cancel" , rootTaskId : rootTaskId ! , reason : "timeout" } , 1 ) . catch ( ( ) => undefined )
237+ void client
238+ . command ( { type : "task.cancel" , rootTaskId : rootTaskId ! , reason : "timeout" } , 1 )
239+ . catch ( ( ) => undefined )
204240 return makeResult ( "timed_out" , rootTaskId ! , {
205241 currentTaskId : projection . currentTaskId ,
206- resumable : true ,
242+ resumable : false ,
207243 code : "task_timed_out" ,
208244 message : "Task deadline exceeded" ,
209245 } )
@@ -233,8 +269,10 @@ export async function runAutomation(
233269 makeResult ( "failed" , rootTaskId ! , {
234270 currentTaskId : projection . currentTaskId ,
235271 resumable : false ,
236- code : "host_crashed" ,
272+ code : error instanceof ZooClientError ? error . code : "host_crashed" ,
237273 message : error . message ,
274+ kind : error instanceof ZooClientError ? error . detail ?. kind : "runtime" ,
275+ phase : error instanceof ZooClientError ? error . detail ?. phase : undefined ,
238276 } ) ,
239277 ) ,
240278 ] )
@@ -254,16 +292,14 @@ export async function runAutomation(
254292 return resultExitCode ( finalResult )
255293 } catch ( error ) {
256294 const message = error instanceof Error ? error . message : String ( error )
257- const parsedCode = zooErrorCodeSchema . safeParse ( message . split ( ":" , 1 ) [ 0 ] )
295+ const parsedCode = zooErrorCodeSchema . safeParse ( error instanceof ZooClientError ? error . code : undefined )
258296 const code : ZooErrorCode = parsedCode . success
259297 ? parsedCode . data
260- : message . includes ( "protocol" ) || message . includes ( "negotiat" )
261- ? "protocol_incompatible"
262- : rootTaskId
263- ? "task_failed"
264- : "host_start_failed"
298+ : rootTaskId
299+ ? "task_failed"
300+ : "host_start_failed"
265301 const result =
266- message . includes ( "deadline" )
302+ code === "task_timed_out" || message . includes ( "deadline" )
267303 ? makeResult ( "timed_out" , rootTaskId ?? "unavailable" , {
268304 resumable : Boolean ( rootTaskId ) ,
269305 code : "task_timed_out" ,
@@ -275,6 +311,8 @@ export async function runAutomation(
275311 resumable : false ,
276312 code,
277313 message,
314+ kind : error instanceof ZooClientError ? error . detail ?. kind : "runtime" ,
315+ phase : error instanceof ZooClientError ? error . detail ?. phase : undefined ,
278316 } )
279317 renderFinal ( result )
280318 return resultExitCode ( result )
0 commit comments