Skip to content

Commit bec6771

Browse files
committed
fix(taskyon): stabilize cross-runtime diagnostics
Rename worker cancellation and await complete teardown before releasing resources. Label timed-out FRP requests and support long storage identifiers safely. Align shared API diagnostics across browser and CLI runtimes. Add category-aware diagnostic commands and an aggregate test workflow.
1 parent 8dd3ab8 commit bec6771

25 files changed

Lines changed: 225 additions & 81 deletions

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,21 @@
6868
"tauri:build": "tauri build",
6969
"tycli:build": "yarn workspace @taskyon/tycli build",
7070
"tycli:diagnostics": "yarn workspace @taskyon/tycli cli-diagnostics",
71+
"tycli:diagnostics:authenticated": "yarn tycli:diagnostics --online --category authenticated",
72+
"tycli:diagnostics:experimental": "yarn tycli:diagnostics --experimental --online --category experimental",
73+
"tycli:diagnostics:large-tokens": "yarn tycli:diagnostics --large-tokens --online --category large-tokens",
7174
"tycli:diagnostics:list": "yarn workspace @taskyon/tycli cli-diagnostics:list",
75+
"tycli:diagnostics:long-running": "yarn tycli:diagnostics --experimental --online --allow-long-run --category long-running",
76+
"tycli:diagnostics:model-based": "yarn tycli:diagnostics --large-tokens --online --category model-based",
7277
"tycli:diagnostics:modelica": "yarn workspace @taskyon/tycli cli-diagnostics:modelica",
78+
"tycli:diagnostics:network": "yarn tycli:diagnostics --online --category network",
79+
"tycli:diagnostics:standard": "yarn tycli:diagnostics --category standard",
7380
"tycli:discovery-fixture": "yarn workspace @taskyon/tycli discovery-fixture",
7481
"tycli:lint": "yarn workspace @taskyon/tycli lint",
7582
"tycli:typecheck": "yarn workspace @taskyon/tycli typecheck",
7683
"tycli:prod": "yarn tycli:build && node packages/tycli/dist/cli.cjs",
7784
"tycli": "yarn workspace @taskyon/tycli dev",
85+
"test:all": "yarn lint && yarn tycli:diagnostics --online && yarn test:e2e",
7886
"test:e2e": "playwright test",
7987
"test:e2e:online": "PLAYWRIGHT_REQUIRE_ONLINE=1 playwright test",
8088
"test:e2e:headed": "playwright test --headed",

packages/common/modules/diagnosticsRunner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface TaskyonTestFn {
1818
gui?: boolean
1919
experimental?: boolean
2020
requiresLargeTokens?: boolean
21+
requiresLongRun?: boolean
2122
requiresAuth?: boolean
2223
modelBased?: boolean
2324
helper?: boolean

packages/common/modules/frpBus.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,7 @@ export function createStreamRpcRequest<
13161316
return awaitRequestResponse({
13171317
subscribe: options.port.receive,
13181318
sendRequest: () => options.port.send(options.request),
1319+
requestLabel: options.requestId,
13191320
timeoutMs: options.timeoutMs,
13201321
signal: options.signal,
13211322
sendCancel: createCancelRequest

packages/common/modules/requestLifecycle.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function awaitRequestResponse<TReceive, TResult>(options: {
2222
subscribe(receive: (message: TReceive) => void): RequestUnsubscribe
2323
sendRequest(): void
2424
sendCancel?: ((reason: string) => void) | undefined
25+
requestLabel?: string | undefined
2526
timeoutMs?: number | undefined
2627
signal?: AbortSignal | undefined
2728
readResponse(message: TReceive): RequestResult<TResult> | undefined
@@ -83,13 +84,12 @@ export function awaitRequestResponse<TReceive, TResult>(options: {
8384
})
8485
options.signal?.addEventListener('abort', abort, { once: true })
8586
if (options.timeoutMs !== undefined) {
86-
timeout = setTimeout(
87-
() =>
88-
cancel(
89-
interruptionError('TimeoutError', `Request timed out after ${options.timeoutMs}ms`),
90-
),
91-
options.timeoutMs,
87+
const requestContext = options.requestLabel ? ` (${options.requestLabel})` : ''
88+
const timeoutError = interruptionError(
89+
'TimeoutError',
90+
`Request timed out after ${options.timeoutMs}ms${requestContext}`,
9291
)
92+
timeout = setTimeout(() => cancel(timeoutError), options.timeoutMs)
9393
}
9494
options.sendRequest()
9595
} catch (error) {

packages/common/modules/test_requestLifecycle.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,13 @@ export const testRequestLifecycleTimesOutAndCleansUp = async () => {
8585
awaitRequestResponse({
8686
subscribe: harness.subscribe,
8787
sendRequest: () => undefined,
88+
requestLabel: 'example.request-1',
8889
timeoutMs: 1,
8990
readResponse: () => undefined,
9091
}),
9192
)
9293
assert(error.name === 'TimeoutError', 'Expected an explicit timeout error.')
94+
assert(error.message.includes('example.request-1'), 'Expected the request label in the error.')
9395
assert(harness.unsubscribeCount() === 1, 'Expected cleanup after a timeout.')
9496
return { success: true }
9597
}

packages/runtime-browser/src/core.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,16 @@ export const createTaskyonBrowserCoreRuntime = (options: TaskyonBrowserCoreRunti
7979
stop?.()
8080
}
8181

82-
const disposeCore = (core: Awaited<ReturnType<typeof tyCore>>, reason: string) => {
82+
const disposeCore = async (core: Awaited<ReturnType<typeof tyCore>>, reason: string) => {
8383
if (disposed) return
8484
disposed = true
8585
disconnectCore?.()
8686
disconnectCore = undefined
87-
core.dispose(reason)
88-
stopStorageService()
87+
try {
88+
await core.dispose(reason)
89+
} finally {
90+
stopStorageService()
91+
}
8992
}
9093

9194
const taskyon = (async () => {
@@ -122,7 +125,7 @@ export const createTaskyonBrowserCoreRuntime = (options: TaskyonBrowserCoreRunti
122125
disconnectCore = corePort.connect(core.port)
123126
corePort.send({ type: 'taskyonReady' })
124127
options.onStage?.('ready')
125-
if (stopReason) disposeCore(core, stopReason)
128+
if (stopReason) await disposeCore(core, stopReason)
126129
return core
127130
} catch (error) {
128131
stopStorageService()
@@ -139,7 +142,7 @@ export const createTaskyonBrowserCoreRuntime = (options: TaskyonBrowserCoreRunti
139142
if (stopReason) return
140143
stopReason = reason
141144
const core = await taskyon
142-
disposeCore(core, reason)
145+
await disposeCore(core, reason)
143146
},
144147
}
145148
}

packages/taskyon/src/api/storageRecordFileBackend.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod'
2+
import { canonicalHash } from '@taskyon/common/modules/canonicalHash'
23
import { deepMerge } from '../utils/objHelpers'
34
import type { StorageRecordBackend } from './storageProtocol'
45

@@ -71,11 +72,19 @@ const mergeStorageRecord = (
7172
export const storageRecordNamespacePath = (namespace: string) =>
7273
['records', ...namespace.split('/').map(encodeURIComponent)].join('/')
7374

75+
const MAX_READABLE_ID_LENGTH = 160
76+
77+
const storageRecordFileName = (id: string | number) => {
78+
const type = typeof id
79+
const encodedId = encodeURIComponent(String(id))
80+
if (encodedId.length <= MAX_READABLE_ID_LENGTH) return `${type}-${encodedId}.json`
81+
82+
const digest = canonicalHash({ type, value: id }).slice('sha256:'.length)
83+
return `${type}-sha256-${digest}.json`
84+
}
85+
7486
export const storageRecordFilePath = (namespace: string, id: string | number) =>
75-
[
76-
storageRecordNamespacePath(namespace),
77-
`${typeof id}-${encodeURIComponent(String(id))}.json`,
78-
].join('/')
87+
[storageRecordNamespacePath(namespace), storageRecordFileName(id)].join('/')
7988

8089
export const createStorageRecordFileBackend = (
8190
adapter: StorageRecordFileAdapter,

packages/taskyon/src/core/init.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ const dynamicContext =
431431
}
432432
const continuationTask = partialTaskDraft.parse(entryNode())
433433
const taskWorkerConfig = llmSettings().taskWorker
434-
const { workerStream, toolRpcPort, workerStop, queueTask } = runTaskWorker(
434+
const { workerStream, toolRpcPort, cancelCurrentRun, workerSettled, queueTask } = runTaskWorker(
435435
taskManagerInstance,
436436
continuationTask,
437437
continuationTask,
@@ -487,23 +487,24 @@ const dynamicContext =
487487
return {
488488
callTool: (name: string, args: FunctionArguments) => toolExecutionClient.callTool(name, args),
489489
runtimeConfiguration,
490-
workerStop: (message: string) => {
490+
cancelCurrentRun: (message: string) => {
491491
console.log('tycore stopping all tasks:', message)
492-
workerStop(message)
492+
cancelCurrentRun(message)
493493
workerToolBroker.stop(message)
494494
coreToolExecutor.stop(message)
495495
},
496-
dispose: (message: string) => {
496+
dispose: async (message: string) => {
497497
if (disposed) return
498498
disposed = true
499499
console.log('tycore disposing session context:', message)
500500
unsubscribeChatCompletion()
501501
unsubscribeSessionStreams.forEach((unsubscribe) => unsubscribe())
502-
workerStop(message)
503-
workerToolBroker.stop(message)
504-
coreToolExecutor.stop(message)
505502
unsubscribeApiServer()
506503
unsubscribeHostApiServer()
504+
cancelCurrentRun(message)
505+
workerToolBroker.stop(message)
506+
coreToolExecutor.stop(message)
507+
await workerSettled()
507508
unsubscribeTaskStreamBridge()
508509
workerToolBroker.destroy()
509510
coreToolExecutor.destroy()
@@ -586,7 +587,7 @@ export async function tyCore(
586587

587588
const replaceSessionContext = async (newCs: CryptoSession) => {
588589
const currentToolchainConfig = ctx.runtimeConfiguration.toolchainConfig
589-
ctx.dispose('switching crypto session')
590+
await ctx.dispose('switching crypto session')
590591
cs = newCs
591592
// we need to re-initialize our entire context in order to have access to key store, decrypted data
592593
// etc with the new session...
@@ -608,7 +609,7 @@ export async function tyCore(
608609
chatCompletionStream: chatCompletionStream.stream,
609610
workerStream: workerStream.stream,
610611
taskStream: taskStream.stream,
611-
workerStop: (message: string) => ctx.workerStop(message),
612+
cancelCurrentRun: (message: string) => ctx.cancelCurrentRun(message),
612613
dispose: (message: string) => ctx.dispose(message),
613614
updateChatCompletionApiKey: async (key: string, value?: string) => {
614615
const { tool, def } = await ctx.taskManagerInstance.getToolDefinition(

packages/taskyon/src/core/taskWorker.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,8 @@ const setupRun = (
529529
const readyQueue = createAsyncQueue<string>()
530530
const pendingByPrior = new Map<string, Set<string>>()
531531
const activeTaskIds = new Set<string>()
532+
const routingTasks = new Set<Promise<void>>()
533+
const reconciliationTasks = new Set<Promise<void>>()
532534
const taskTracker = createTaskTracker(taskManager)
533535
let routingTaskCount = 0
534536
let didEmitAllProcessed = false
@@ -570,7 +572,7 @@ const setupRun = (
570572

571573
didEmitAllProcessed = false
572574
routingTaskCount += 1
573-
void (async () => {
575+
const routingTask = (async () => {
574576
let wasRouted = false
575577
try {
576578
const task = await taskManager.getTask(id)
@@ -588,6 +590,11 @@ const setupRun = (
588590
if (!wasRouted) emitAllProcessedIfIdle()
589591
}
590592
})()
593+
routingTasks.add(routingTask)
594+
void routingTask.then(
595+
() => routingTasks.delete(routingTask),
596+
() => routingTasks.delete(routingTask),
597+
)
591598
}
592599
const { taskisInLoop, taskOutOfLoop, getTasksInProgress, clearTasksInProgress } =
593600
workerLoggingHelper(streamEmit)
@@ -667,7 +674,12 @@ const setupRun = (
667674
if (getTasksInProgress() <= 0 && readyQueue.count() === 0 && pendingByPrior.size > 0) {
668675
streamEmit({ stage: 'waiting' })
669676
}
670-
void reconcilePendingTasks().finally(() => emitAllProcessedIfIdle())
677+
const reconciliationTask = reconcilePendingTasks().finally(() => emitAllProcessedIfIdle())
678+
reconciliationTasks.add(reconciliationTask)
679+
void reconciliationTask.then(
680+
() => reconciliationTasks.delete(reconciliationTask),
681+
() => reconciliationTasks.delete(reconciliationTask),
682+
)
671683
}, 1000)
672684

673685
try {
@@ -679,6 +691,7 @@ const setupRun = (
679691
} finally {
680692
clearInterval(reconciliationInterval)
681693
}
694+
await Promise.allSettled([...routingTasks, ...reconciliationTasks])
682695
readyQueue.clear()
683696
pendingByPrior.clear()
684697
activeTaskIds.clear()
@@ -728,8 +741,9 @@ export function runTaskWorker(
728741
>()
729742
let currentTaskCtrl: AbortController | undefined = new AbortController()
730743
let queueTask: ((id: string) => void) | undefined = undefined
744+
const activeRuns = new Set<Promise<void>>()
731745

732-
const workerStop = (message: string) => {
746+
const cancelCurrentRun = (message: string) => {
733747
currentTaskCtrl?.abort(message)
734748
// in case of any errors, especially if its an interrupt event we simply want to cancel everything :P
735749
// empty our task queue :)
@@ -739,7 +753,7 @@ export function runTaskWorker(
739753
}
740754

741755
// we have put all our dependencies in restartable workers.
742-
// if anyone calls the "workerStop" the function wil simply re-start the worker
756+
// If the current run is cancelled, the worker starts a new run when another task is queued.
743757
// as soon as a new task was added....
744758
const externalQueueTask = (id: string) => {
745759
if (currentTaskCtrl?.signal.aborted || !queueTask) {
@@ -751,14 +765,25 @@ export function runTaskWorker(
751765
currentTaskCtrl = newTaskCtrl
752766
queueTask = newQueueTask
753767

754-
void run(defaultTask, errorTask)
768+
const workerRun = run(defaultTask, errorTask)
769+
activeRuns.add(workerRun)
770+
void workerRun.then(
771+
() => activeRuns.delete(workerRun),
772+
(error) => {
773+
activeRuns.delete(workerRun)
774+
taskProcessingStream.emit({ stage: 'error', info: humanizeError(error) })
775+
},
776+
)
755777
}
756778
queueTask(id)
757779
}
758780
return {
759781
workerStream: taskProcessingStream.stream,
760782
toolRpcPort,
761-
workerStop,
783+
cancelCurrentRun,
784+
workerSettled: async () => {
785+
await Promise.allSettled(activeRuns)
786+
},
762787
queueTask: externalQueueTask,
763788
}
764789
}

packages/taskyon/src/taskyon.space/taskyon.space_api.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ export async function returnToken(
5252
) {
5353
const url = `${baseUrl}/return`
5454

55-
console.log('[returnToken] Returning token with data:', {
56-
token,
55+
console.log('[returnToken] Returning token usage data:', {
5756
credits_spent_increase,
5857
reference_data,
5958
})

0 commit comments

Comments
 (0)