visionclaw 0.1.219-dev.codex-issue-2805-worker-runtime.3 → 0.1.219-dev.codex-fix-2941-activity-foreground.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.
- package/dist/agent/api-proxy.d.ts +16 -0
- package/dist/agent/api-proxy.d.ts.map +1 -1
- package/dist/agent/api-proxy.js +8 -9
- package/dist/agent/api-proxy.js.map +1 -1
- package/dist/agent/browser-launcher.d.ts +0 -3
- package/dist/agent/browser-launcher.d.ts.map +1 -1
- package/dist/agent/browser-launcher.js +43 -101
- package/dist/agent/browser-launcher.js.map +1 -1
- package/dist/agent/coding-conventions.d.ts +3 -2
- package/dist/agent/coding-conventions.d.ts.map +1 -1
- package/dist/agent/coding-conventions.js +8 -7
- package/dist/agent/coding-conventions.js.map +1 -1
- package/dist/agent/command-handlers.d.ts.map +1 -1
- package/dist/agent/command-handlers.js +16 -4
- package/dist/agent/command-handlers.js.map +1 -1
- package/dist/agent/context-action-restart-fallback.d.ts.map +1 -1
- package/dist/agent/context-action-restart-fallback.js +3 -9
- package/dist/agent/context-action-restart-fallback.js.map +1 -1
- package/dist/agent/context-refresh.d.ts.map +1 -1
- package/dist/agent/context-refresh.js +7 -0
- package/dist/agent/context-refresh.js.map +1 -1
- package/dist/agent/context.d.ts.map +1 -1
- package/dist/agent/context.js +8 -6
- package/dist/agent/context.js.map +1 -1
- package/dist/agent/engine-fallback.d.ts +35 -0
- package/dist/agent/engine-fallback.d.ts.map +1 -1
- package/dist/agent/engine-fallback.js +3 -4
- package/dist/agent/engine-fallback.js.map +1 -1
- package/dist/agent/engines/browser-tool-policy.d.ts +1 -12
- package/dist/agent/engines/browser-tool-policy.d.ts.map +1 -1
- package/dist/agent/engines/browser-tool-policy.js +2 -17
- package/dist/agent/engines/browser-tool-policy.js.map +1 -1
- package/dist/agent/engines/claude/session.d.ts +0 -1
- package/dist/agent/engines/claude/session.d.ts.map +1 -1
- package/dist/agent/engines/claude/session.js +36 -21
- package/dist/agent/engines/claude/session.js.map +1 -1
- package/dist/agent/engines/pi/tools.d.ts.map +1 -1
- package/dist/agent/engines/pi/tools.js +10 -2
- package/dist/agent/engines/pi/tools.js.map +1 -1
- package/dist/agent/engines/pi-coding/session.d.ts +0 -3
- package/dist/agent/engines/pi-coding/session.d.ts.map +1 -1
- package/dist/agent/engines/pi-coding/session.js +22 -19
- package/dist/agent/engines/pi-coding/session.js.map +1 -1
- package/dist/agent/engines/session-types.d.ts +0 -6
- package/dist/agent/engines/session-types.d.ts.map +1 -1
- package/dist/agent/external-context/runtime.d.ts.map +1 -1
- package/dist/agent/external-context/runtime.js +14 -15
- package/dist/agent/external-context/runtime.js.map +1 -1
- package/dist/agent/fast-responder.d.ts +20 -4
- package/dist/agent/fast-responder.d.ts.map +1 -1
- package/dist/agent/fast-responder.js +39 -9
- package/dist/agent/fast-responder.js.map +1 -1
- package/dist/agent/interrupt-handler.d.ts +18 -4
- package/dist/agent/interrupt-handler.d.ts.map +1 -1
- package/dist/agent/interrupt-handler.js +284 -49
- package/dist/agent/interrupt-handler.js.map +1 -1
- package/dist/agent/lab-run-system-prompt.d.ts.map +1 -1
- package/dist/agent/lab-run-system-prompt.js +0 -3
- package/dist/agent/lab-run-system-prompt.js.map +1 -1
- package/dist/agent/lifecycle-trace.d.ts.map +1 -1
- package/dist/agent/lifecycle-trace.js +5 -6
- package/dist/agent/lifecycle-trace.js.map +1 -1
- package/dist/agent/loop.d.ts +1 -4
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +129 -105
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/memory-protocol.d.ts +2 -2
- package/dist/agent/memory-protocol.d.ts.map +1 -1
- package/dist/agent/memory-protocol.js +15 -3
- package/dist/agent/memory-protocol.js.map +1 -1
- package/dist/agent/memory-store.d.ts.map +1 -1
- package/dist/agent/memory-store.js +1 -11
- package/dist/agent/memory-store.js.map +1 -1
- package/dist/agent/model-provider.d.ts +0 -2
- package/dist/agent/model-provider.d.ts.map +1 -1
- package/dist/agent/model-provider.js +25 -31
- package/dist/agent/model-provider.js.map +1 -1
- package/dist/agent/product-identity.d.ts +4 -5
- package/dist/agent/product-identity.d.ts.map +1 -1
- package/dist/agent/product-identity.js +6 -7
- package/dist/agent/product-identity.js.map +1 -1
- package/dist/agent/public-activity.d.ts +1 -1
- package/dist/agent/public-activity.d.ts.map +1 -1
- package/dist/agent/public-activity.js.map +1 -1
- package/dist/agent/reply-watchdog.d.ts +3 -5
- package/dist/agent/reply-watchdog.d.ts.map +1 -1
- package/dist/agent/reply-watchdog.js +4 -6
- package/dist/agent/reply-watchdog.js.map +1 -1
- package/dist/agent/session-manager.d.ts +0 -5
- package/dist/agent/session-manager.d.ts.map +1 -1
- package/dist/agent/session-manager.js +0 -7
- package/dist/agent/session-manager.js.map +1 -1
- package/dist/agent/state.d.ts +0 -7
- package/dist/agent/state.d.ts.map +1 -1
- package/dist/agent/state.js +11 -21
- package/dist/agent/state.js.map +1 -1
- package/dist/agent/stream-handler.d.ts.map +1 -1
- package/dist/agent/stream-handler.js +17 -0
- package/dist/agent/stream-handler.js.map +1 -1
- package/dist/agent/system-prompt.d.ts.map +1 -1
- package/dist/agent/system-prompt.js +6 -10
- package/dist/agent/system-prompt.js.map +1 -1
- package/dist/agent/transcript/transcript-indexer.d.ts +28 -0
- package/dist/agent/transcript/transcript-indexer.d.ts.map +1 -1
- package/dist/agent/transcript/transcript-indexer.js +57 -58
- package/dist/agent/transcript/transcript-indexer.js.map +1 -1
- package/dist/agent/usage/usage-report-queue.d.ts.map +1 -1
- package/dist/agent/usage/usage-report-queue.js +52 -53
- package/dist/agent/usage/usage-report-queue.js.map +1 -1
- package/dist/agent/wake-cycle-runner.d.ts +8 -7
- package/dist/agent/wake-cycle-runner.d.ts.map +1 -1
- package/dist/agent/wake-cycle-runner.js +66 -105
- package/dist/agent/wake-cycle-runner.js.map +1 -1
- package/dist/builtin-skills/owner-manual/SKILL.md +2 -2
- package/dist/calendar/occurrence-ownership.d.ts.map +1 -1
- package/dist/calendar/occurrence-ownership.js +5 -6
- package/dist/calendar/occurrence-ownership.js.map +1 -1
- package/dist/calendar/scheduler.d.ts.map +1 -1
- package/dist/calendar/scheduler.js +3 -4
- package/dist/calendar/scheduler.js.map +1 -1
- package/dist/channels/interface.d.ts +2 -8
- package/dist/channels/interface.d.ts.map +1 -1
- package/dist/channels/interface.js +1 -8
- package/dist/channels/interface.js.map +1 -1
- package/dist/channels/manager.d.ts.map +1 -1
- package/dist/channels/manager.js +0 -2
- package/dist/channels/manager.js.map +1 -1
- package/dist/channels/qq-status.d.ts.map +1 -1
- package/dist/channels/qq-status.js +1 -0
- package/dist/channels/qq-status.js.map +1 -1
- package/dist/channels/queue.d.ts.map +1 -1
- package/dist/channels/queue.js +0 -8
- package/dist/channels/queue.js.map +1 -1
- package/dist/channels/telegram-status.d.ts.map +1 -1
- package/dist/channels/telegram-status.js +2 -0
- package/dist/channels/telegram-status.js.map +1 -1
- package/dist/channels/tui.d.ts +2 -2
- package/dist/channels/tui.d.ts.map +1 -1
- package/dist/channels/tui.js +2 -7
- package/dist/channels/tui.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +15 -16
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +8 -16
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +24 -13
- package/dist/config/types.js.map +1 -1
- package/dist/email/email-route-store.d.ts.map +1 -1
- package/dist/email/email-route-store.js +5 -6
- package/dist/email/email-route-store.js.map +1 -1
- package/dist/i18n/messages.d.ts +0 -12
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/messages.js +9 -21
- package/dist/i18n/messages.js.map +1 -1
- package/dist/logger.d.ts +0 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +1 -19
- package/dist/logger.js.map +1 -1
- package/dist/threads/active-threads-store.d.ts.map +1 -1
- package/dist/threads/active-threads-store.js +27 -8
- package/dist/threads/active-threads-store.js.map +1 -1
- package/dist/threads/execution-host.d.ts.map +1 -1
- package/dist/threads/execution-host.js +0 -1
- package/dist/threads/execution-host.js.map +1 -1
- package/dist/threads/execution-scheduler.d.ts +0 -4
- package/dist/threads/execution-scheduler.d.ts.map +1 -1
- package/dist/threads/execution-scheduler.js +2 -38
- package/dist/threads/execution-scheduler.js.map +1 -1
- package/dist/threads/execution-store.d.ts +5 -20
- package/dist/threads/execution-store.d.ts.map +1 -1
- package/dist/threads/execution-store.js +8 -45
- package/dist/threads/execution-store.js.map +1 -1
- package/dist/threads/foreground-task.d.ts +6 -10
- package/dist/threads/foreground-task.d.ts.map +1 -1
- package/dist/threads/foreground-task.js +26 -34
- package/dist/threads/foreground-task.js.map +1 -1
- package/dist/threads/foreman-client.d.ts.map +1 -1
- package/dist/threads/foreman-client.js +1 -3
- package/dist/threads/foreman-client.js.map +1 -1
- package/dist/threads/foreman-execution.d.ts +12 -12
- package/dist/threads/foreman-execution.d.ts.map +1 -1
- package/dist/threads/foreman-execution.js +19 -30
- package/dist/threads/foreman-execution.js.map +1 -1
- package/dist/threads/foreman-host.d.ts +2 -17
- package/dist/threads/foreman-host.d.ts.map +1 -1
- package/dist/threads/foreman-host.js +63 -452
- package/dist/threads/foreman-host.js.map +1 -1
- package/dist/threads/foreman-protocol.d.ts +12 -113
- package/dist/threads/foreman-protocol.d.ts.map +1 -1
- package/dist/threads/foreman-protocol.js +15 -41
- package/dist/threads/foreman-protocol.js.map +1 -1
- package/dist/threads/foreman-store.d.ts +10 -44
- package/dist/threads/foreman-store.d.ts.map +1 -1
- package/dist/threads/foreman-store.js +19 -78
- package/dist/threads/foreman-store.js.map +1 -1
- package/dist/threads/generic-execution.d.ts +2 -2
- package/dist/threads/generic-execution.d.ts.map +1 -1
- package/dist/threads/generic-execution.js +6 -9
- package/dist/threads/generic-execution.js.map +1 -1
- package/dist/threads/ipc.d.ts +0 -3
- package/dist/threads/ipc.d.ts.map +1 -1
- package/dist/threads/ipc.js.map +1 -1
- package/dist/threads/relay-adapter.d.ts +2 -0
- package/dist/threads/relay-adapter.d.ts.map +1 -1
- package/dist/threads/relay-adapter.js +7 -1
- package/dist/threads/relay-adapter.js.map +1 -1
- package/dist/threads/supervisor.d.ts +51 -21
- package/dist/threads/supervisor.d.ts.map +1 -1
- package/dist/threads/supervisor.js +449 -160
- package/dist/threads/supervisor.js.map +1 -1
- package/dist/threads/task-contracts.d.ts +194 -0
- package/dist/threads/task-contracts.d.ts.map +1 -0
- package/dist/threads/task-contracts.js +82 -0
- package/dist/threads/task-contracts.js.map +1 -0
- package/dist/threads/task-sidecar.d.ts +145 -0
- package/dist/threads/task-sidecar.d.ts.map +1 -0
- package/dist/threads/task-sidecar.js +311 -0
- package/dist/threads/task-sidecar.js.map +1 -0
- package/dist/threads/task-store.d.ts +172 -0
- package/dist/threads/task-store.d.ts.map +1 -0
- package/dist/threads/task-store.js +599 -0
- package/dist/threads/task-store.js.map +1 -0
- package/dist/threads/thread-activity-controller.d.ts +7 -0
- package/dist/threads/thread-activity-controller.d.ts.map +1 -1
- package/dist/threads/thread-activity-controller.js +52 -2
- package/dist/threads/thread-activity-controller.js.map +1 -1
- package/dist/threads/thread-task-context.d.ts +40 -0
- package/dist/threads/thread-task-context.d.ts.map +1 -0
- package/dist/threads/thread-task-context.js +160 -0
- package/dist/threads/thread-task-context.js.map +1 -0
- package/dist/threads/thread-task-controller.d.ts +122 -0
- package/dist/threads/thread-task-controller.d.ts.map +1 -0
- package/dist/threads/thread-task-controller.js +276 -0
- package/dist/threads/thread-task-controller.js.map +1 -0
- package/dist/threads/thread-task-runtime.d.ts +350 -0
- package/dist/threads/thread-task-runtime.d.ts.map +1 -0
- package/dist/threads/thread-task-runtime.js +896 -0
- package/dist/threads/thread-task-runtime.js.map +1 -0
- package/dist/threads/worker-entry.d.ts.map +1 -1
- package/dist/threads/worker-entry.js +28 -29
- package/dist/threads/worker-entry.js.map +1 -1
- package/dist/threads/worker-execution-runtime.d.ts +2 -3
- package/dist/threads/worker-execution-runtime.d.ts.map +1 -1
- package/dist/threads/worker-execution-runtime.js +2 -5
- package/dist/threads/worker-execution-runtime.js.map +1 -1
- package/dist/tools/delegation.d.ts +2 -75
- package/dist/tools/delegation.d.ts.map +1 -1
- package/dist/tools/delegation.js +29 -63
- package/dist/tools/delegation.js.map +1 -1
- package/dist/tools/email.d.ts +1 -1
- package/dist/tools/execution-control.d.ts +0 -5
- package/dist/tools/execution-control.d.ts.map +1 -1
- package/dist/tools/execution-control.js +2 -9
- package/dist/tools/execution-control.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp-manager.d.ts +1 -1
- package/dist/tools/memory.d.ts +1 -1
- package/dist/tools/notify.d.ts +0 -2
- package/dist/tools/notify.d.ts.map +1 -1
- package/dist/tools/notify.js +5 -21
- package/dist/tools/notify.js.map +1 -1
- package/dist/tools/skill.d.ts +1 -1
- package/dist/tools/thread-tasks.d.ts +23 -0
- package/dist/tools/thread-tasks.d.ts.map +1 -0
- package/dist/tools/thread-tasks.js +122 -0
- package/dist/tools/thread-tasks.js.map +1 -0
- package/dist/tools/totp.d.ts +1 -1
- package/dist/tui/request-store.d.ts +1 -1
- package/dist/tui/request-store.d.ts.map +1 -1
- package/dist/tui/request-store.js +2 -2
- package/dist/tui/request-store.js.map +1 -1
- package/dist/tui/server.d.ts.map +1 -1
- package/dist/tui/server.js +2 -17
- package/dist/tui/server.js.map +1 -1
- package/dist/utils/restart.d.ts.map +1 -1
- package/dist/utils/restart.js +0 -6
- package/dist/utils/restart.js.map +1 -1
- package/dist-agent/bundle.cjs +2 -2
- package/package.json +1 -1
- package/dist/agent/runtime-context.d.ts +0 -27
- package/dist/agent/runtime-context.d.ts.map +0 -1
- package/dist/agent/runtime-context.js +0 -60
- package/dist/agent/runtime-context.js.map +0 -1
- package/dist/agent/runtime-disclosure-policy.d.ts +0 -6
- package/dist/agent/runtime-disclosure-policy.d.ts.map +0 -1
- package/dist/agent/runtime-disclosure-policy.js +0 -16
- package/dist/agent/runtime-disclosure-policy.js.map +0 -1
- package/dist/conversations/lazy-runtime.d.ts +0 -5
- package/dist/conversations/lazy-runtime.d.ts.map +0 -1
- package/dist/conversations/lazy-runtime.js +0 -12
- package/dist/conversations/lazy-runtime.js.map +0 -1
- package/dist/conversations/runtime.d.ts +0 -17
- package/dist/conversations/runtime.d.ts.map +0 -1
- package/dist/conversations/runtime.js +0 -92
- package/dist/conversations/runtime.js.map +0 -1
- package/dist/conversations/scheduler.d.ts +0 -57
- package/dist/conversations/scheduler.d.ts.map +0 -1
- package/dist/conversations/scheduler.js +0 -175
- package/dist/conversations/scheduler.js.map +0 -1
- package/dist/threads/foreman-business.d.ts +0 -9
- package/dist/threads/foreman-business.d.ts.map +0 -1
- package/dist/threads/foreman-business.js +0 -117
- package/dist/threads/foreman-business.js.map +0 -1
- package/dist/threads/foreman-delivery.d.ts +0 -5
- package/dist/threads/foreman-delivery.d.ts.map +0 -1
- package/dist/threads/foreman-delivery.js +0 -21
- package/dist/threads/foreman-delivery.js.map +0 -1
- package/dist/threads/task-workspace.d.ts +0 -8
- package/dist/threads/task-workspace.d.ts.map +0 -1
- package/dist/threads/task-workspace.js +0 -52
- package/dist/threads/task-workspace.js.map +0 -1
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThreadTaskRuntime (issue #1996, PR 3): the one object the live loop talks
|
|
3
|
+
* to. It owns this process's {@link ThreadTaskStore} and
|
|
4
|
+
* {@link ThreadTaskController} for the thread it serves — the default thread
|
|
5
|
+
* in the main process, a business thread in its worker.
|
|
6
|
+
*
|
|
7
|
+
* Hot-path rule: every method is fail-safe. A store, controller or model
|
|
8
|
+
* error is logged and swallowed; it must never break message handling, a
|
|
9
|
+
* wake or a reply. The cost of a failure is a missed task update that the
|
|
10
|
+
* next run can still recover from the persisted events, never a lost message.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { getConfigDir } from "../config/index.js";
|
|
15
|
+
import { DEFAULT_THREAD_ID } from "../channels/interface.js";
|
|
16
|
+
import { getMessageDedupKey } from "../channels/message-dedup.js";
|
|
17
|
+
import { EXTERNAL_WAKE_KIND } from "../channels/queue.js";
|
|
18
|
+
import { logger } from "../logger.js";
|
|
19
|
+
import { OPEN_TASK_STATUSES } from "./task-contracts.js";
|
|
20
|
+
import { createSidecarModel } from "./task-sidecar.js";
|
|
21
|
+
import { ThreadTaskStore, messageEventKey } from "./task-store.js";
|
|
22
|
+
import { ThreadTaskController } from "./thread-task-controller.js";
|
|
23
|
+
import { renderThreadTaskContext } from "./thread-task-context.js";
|
|
24
|
+
import { taskContinuationRouting } from "../agent/reply-target.js";
|
|
25
|
+
/** Execution events are batched harder than owner messages; each run is a model call. */
|
|
26
|
+
const LIVE_CONTROLLER_DEFAULTS = {
|
|
27
|
+
batchDelayMs: 30_000,
|
|
28
|
+
maxBatchEvents: 40,
|
|
29
|
+
callTimeoutMs: 60_000,
|
|
30
|
+
budget: { maxCalls: 12, windowMs: 10 * 60_000 },
|
|
31
|
+
// Execution events alone never use the last 4 calls of the window, so an
|
|
32
|
+
// owner message still gets a run during a long busy wake.
|
|
33
|
+
ownerReserve: 4,
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* How long a fast reply waits for the sidecar before it settles for
|
|
37
|
+
* "received". Added in front of the fast responder's own 5 s budget.
|
|
38
|
+
*/
|
|
39
|
+
export const REGISTRATION_WAIT_MS = 6_000;
|
|
40
|
+
/**
|
|
41
|
+
* How long a lifecycle boundary waits for the sidecar to catch up (#1996 PR 4).
|
|
42
|
+
* Wake end is short because the next message waits behind it; compaction
|
|
43
|
+
* and refresh are already slow.
|
|
44
|
+
*/
|
|
45
|
+
export const RECONCILE_WAIT_MS = { wakeEnd: 5_000, boundary: 10_000 };
|
|
46
|
+
/** Newest unfolded events read for the context block. */
|
|
47
|
+
const UNFOLDED_CONTEXT_EVENTS = 200;
|
|
48
|
+
/** `meta.kind` of a queued resume (PR 5). A system row: no fast reply, watchdog or gate reply. */
|
|
49
|
+
export const TASK_RESUME_KIND = "thread-task-resume";
|
|
50
|
+
const RESUME_SENDER = "thread-task-resume";
|
|
51
|
+
/**
|
|
52
|
+
* At most this many resumes are queued per pass. One wake works one task at
|
|
53
|
+
* a time; the next pass after it queues the next one.
|
|
54
|
+
*/
|
|
55
|
+
const RESUMES_PER_PASS = 1;
|
|
56
|
+
/** Unfolded events read at `/stop` to tell pre-stop tasks from later ones. */
|
|
57
|
+
const STOP_EVENTS = 1_000;
|
|
58
|
+
/** A resume pass waits for the sidecar to catch up; if it cannot, it retries after this. */
|
|
59
|
+
export const RESUME_RETRY_MS = 60_000;
|
|
60
|
+
function errorMessage(error) {
|
|
61
|
+
return error instanceof Error ? error.message : String(error);
|
|
62
|
+
}
|
|
63
|
+
export class ThreadTaskRuntime {
|
|
64
|
+
threadId;
|
|
65
|
+
store;
|
|
66
|
+
controller;
|
|
67
|
+
/**
|
|
68
|
+
* Latest event seq when this process started. A task whose last committed
|
|
69
|
+
* change came from events up to here was moved by a previous process.
|
|
70
|
+
*/
|
|
71
|
+
startSeq;
|
|
72
|
+
/** Resumes this process accepted, by task id: a resume runs once per task per process. Released when its message goes back on the queue. */
|
|
73
|
+
resumesAccepted = new Set();
|
|
74
|
+
closed = false;
|
|
75
|
+
resumeHooks = null;
|
|
76
|
+
resumeRetry = null;
|
|
77
|
+
resumeRunning = false;
|
|
78
|
+
constructor(threadId, store, controller, startSeq, inProgressAtStart) {
|
|
79
|
+
this.threadId = threadId;
|
|
80
|
+
this.store = store;
|
|
81
|
+
this.controller = controller;
|
|
82
|
+
this.startSeq = startSeq;
|
|
83
|
+
if (inProgressAtStart > 0) {
|
|
84
|
+
logger.system(`[thread-tasks] thread=${threadId}: ${inProgressAtStart} task(s) were in progress when the previous process stopped`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Open the thread's store next to its queue.db, take the writer slot and
|
|
89
|
+
* start the controller (which picks up events a previous process left).
|
|
90
|
+
* Returns null instead of throwing: without a runtime the agent runs
|
|
91
|
+
* exactly as before.
|
|
92
|
+
*/
|
|
93
|
+
static start(options) {
|
|
94
|
+
let store;
|
|
95
|
+
try {
|
|
96
|
+
store = new ThreadTaskStore({
|
|
97
|
+
threadId: options.threadId,
|
|
98
|
+
writerId: options.writerId,
|
|
99
|
+
// Same file the MessageQueue owner opens, whichever process this is.
|
|
100
|
+
dbPath: options.dbPath ?? path.join(getConfigDir(), "queue.db"),
|
|
101
|
+
claimWriter: true,
|
|
102
|
+
});
|
|
103
|
+
// Read before the controller starts: it may move these tasks on, and
|
|
104
|
+
// nothing after start() may throw and leave its timer running.
|
|
105
|
+
const startSeq = store.latestSeq();
|
|
106
|
+
const inProgressAtStart = store.listTasks(["in_progress"]).length;
|
|
107
|
+
const controller = new ThreadTaskController({
|
|
108
|
+
store,
|
|
109
|
+
model: options.model ?? createSidecarModel(options.config),
|
|
110
|
+
...LIVE_CONTROLLER_DEFAULTS,
|
|
111
|
+
...options.controller,
|
|
112
|
+
});
|
|
113
|
+
const runtime = new ThreadTaskRuntime(options.threadId, store, controller, startSeq, inProgressAtStart);
|
|
114
|
+
controller.start();
|
|
115
|
+
return runtime;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.err(`[thread-tasks] disabled for thread=${options.threadId}: ${errorMessage(error)}`);
|
|
119
|
+
try {
|
|
120
|
+
store?.close();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Already closed or never opened.
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Persist owner messages as events. Idempotent per message (queue dedup
|
|
130
|
+
* key), so a redelivered message, the watchdog and the wake path can all
|
|
131
|
+
* call it. Returns false if any message could not be recorded.
|
|
132
|
+
*/
|
|
133
|
+
recordMessages(messages, options = {}) {
|
|
134
|
+
if (this.closed || messages.length === 0)
|
|
135
|
+
return !this.closed;
|
|
136
|
+
let ok = true;
|
|
137
|
+
let appended = false;
|
|
138
|
+
for (const msg of messages) {
|
|
139
|
+
try {
|
|
140
|
+
appended = this.store.appendMessageEvent(msg).appended || appended;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
ok = false;
|
|
144
|
+
logger.err(`[thread-tasks] could not record message ${msg.id} thread=${this.threadId}: ${errorMessage(error)}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (appended && options.notify !== false)
|
|
148
|
+
this.safeNotify(true);
|
|
149
|
+
return ok;
|
|
150
|
+
}
|
|
151
|
+
/** Persist an execution event (response, tool result, todo update, status report). */
|
|
152
|
+
recordExecution(input) {
|
|
153
|
+
if (this.closed)
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
if (this.store.appendEvent(input).appended)
|
|
157
|
+
this.safeNotify(false);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
logger.warn(`[thread-tasks] could not record ${input.kind} thread=${this.threadId}: ${errorMessage(error)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Record the messages, then wait up to `waitMs` for the sidecar to consume
|
|
165
|
+
* them. `registered` lists the open tasks the commit tied to these messages;
|
|
166
|
+
* empty means a reply may only acknowledge receipt.
|
|
167
|
+
*
|
|
168
|
+
* A run that started before these events cannot commit them (it snapshotted
|
|
169
|
+
* its batch), and a batch is a contiguous prefix, so the message may need
|
|
170
|
+
* more than one run: an execution-event backlog in front of it is folded
|
|
171
|
+
* first. The loop therefore waits for this message's events to be consumed,
|
|
172
|
+
* not for a single run, and stops as soon as the deadline passes or a run
|
|
173
|
+
* fails (the controller keeps retrying those events on its own schedule).
|
|
174
|
+
*/
|
|
175
|
+
async registerMessages(messages, waitMs = REGISTRATION_WAIT_MS) {
|
|
176
|
+
const recorded = this.recordMessages(messages, { notify: false });
|
|
177
|
+
if (!recorded || this.closed)
|
|
178
|
+
return { recorded, registered: [] };
|
|
179
|
+
try {
|
|
180
|
+
const target = this.store.latestSeq();
|
|
181
|
+
const deadline = Date.now() + waitMs;
|
|
182
|
+
while (!this.isClosed() && this.store.getPosition() < target) {
|
|
183
|
+
const remaining = deadline - Date.now();
|
|
184
|
+
if (remaining <= 0)
|
|
185
|
+
break;
|
|
186
|
+
let timer;
|
|
187
|
+
const timeout = new Promise((resolve) => { timer = setTimeout(() => { resolve("timeout"); }, remaining); });
|
|
188
|
+
const outcome = await Promise.race([this.controller.runOnce(), timeout]).finally(() => { clearTimeout(timer); });
|
|
189
|
+
// Only a commit or a conflict re-read is worth another pass; anything
|
|
190
|
+
// else (deadline, budget, fence, failure) is retried by the controller.
|
|
191
|
+
if (outcome === "timeout" || (outcome.status !== "committed" && outcome.status !== "conflict"))
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
return { recorded, registered: this.tasksForMessages(messages) };
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
logger.warn(`[thread-tasks] registration run failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
198
|
+
return { recorded, registered: [] };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** `close()` may run while a wait awaits; a method read is not narrowed away. */
|
|
202
|
+
isClosed() {
|
|
203
|
+
return this.closed;
|
|
204
|
+
}
|
|
205
|
+
/** Open tasks already tied to any of these messages (no model call). */
|
|
206
|
+
tasksForMessages(messages) {
|
|
207
|
+
try {
|
|
208
|
+
const eventIds = new Set();
|
|
209
|
+
for (const msg of messages) {
|
|
210
|
+
const event = this.store.getEventByKey(messageEventKey(getMessageDedupKey(msg)));
|
|
211
|
+
if (event)
|
|
212
|
+
eventIds.add(event.id);
|
|
213
|
+
}
|
|
214
|
+
if (eventIds.size === 0)
|
|
215
|
+
return [];
|
|
216
|
+
return this.store.listOpenTasks().filter((task) => task.sourceEventIds.some((id) => eventIds.has(id)));
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
logger.warn(`[thread-tasks] lookup failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** Tasks of this thread for the query tool; `[]` on error. */
|
|
224
|
+
listTasks(statuses = OPEN_TASK_STATUSES) {
|
|
225
|
+
try {
|
|
226
|
+
return this.store.listTasks(statuses);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
logger.warn(`[thread-tasks] list failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Let the sidecar fold every event up to the latest one (the explicit
|
|
235
|
+
* position of this boundary), waiting at most `waitMs`. No events past the
|
|
236
|
+
* position means no model call. On timeout, budget exhaustion or failure
|
|
237
|
+
* the events simply stay unconsumed: the controller keeps retrying them,
|
|
238
|
+
* and {@link contextBlock} shows them raw to the next recovery.
|
|
239
|
+
*/
|
|
240
|
+
async reconcile(phase, waitMs) {
|
|
241
|
+
let target = 0;
|
|
242
|
+
let position = 0;
|
|
243
|
+
try {
|
|
244
|
+
target = this.store.latestSeq();
|
|
245
|
+
position = this.store.getPosition();
|
|
246
|
+
const deadline = Date.now() + waitMs;
|
|
247
|
+
while (!this.closed && position < target) {
|
|
248
|
+
const remaining = deadline - Date.now();
|
|
249
|
+
if (remaining <= 0)
|
|
250
|
+
break;
|
|
251
|
+
let timer;
|
|
252
|
+
const timeout = new Promise((resolve) => { timer = setTimeout(() => { resolve("timeout"); }, remaining); });
|
|
253
|
+
const outcome = await Promise.race([this.controller.runOnce(), timeout]).finally(() => { clearTimeout(timer); });
|
|
254
|
+
position = this.store.getPosition();
|
|
255
|
+
// Only a commit or a conflict re-read is worth another pass; anything
|
|
256
|
+
// else (timeout, budget, failure, fence) is retried by the controller.
|
|
257
|
+
if (outcome === "timeout" || (outcome.status !== "committed" && outcome.status !== "conflict"))
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
logger.warn(`[thread-tasks] ${phase} reconcile failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
263
|
+
}
|
|
264
|
+
if (position < target) {
|
|
265
|
+
logger.info(`[thread-tasks] ${phase} reconcile left ${target - position} event seq(s) unfolded thread=${this.threadId}`);
|
|
266
|
+
}
|
|
267
|
+
return { reached: position >= target, target, position };
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Tasks a previous process left `in_progress`: their last change came from
|
|
271
|
+
* events recorded before this process started. That includes a task those
|
|
272
|
+
* events only move to `in_progress` when this process folds them at
|
|
273
|
+
* startup (the sidecar batches, so a crash usually leaves some). Once this
|
|
274
|
+
* process's own events move the task, it is no longer interrupted.
|
|
275
|
+
* TODO: a startup fold whose batch also holds events of this process
|
|
276
|
+
* stamps the task with the later seq and drops the flag early.
|
|
277
|
+
*/
|
|
278
|
+
interruptedTaskIds() {
|
|
279
|
+
return new Set(this.store.listTasks(["in_progress"])
|
|
280
|
+
.filter((task) => task.lastEventSeq <= this.startSeq)
|
|
281
|
+
.map((task) => task.id));
|
|
282
|
+
}
|
|
283
|
+
/** The recovery context for this thread, or null when it owes nothing. Never throws. */
|
|
284
|
+
contextBlock() {
|
|
285
|
+
try {
|
|
286
|
+
const position = this.store.getPosition();
|
|
287
|
+
return renderThreadTaskContext({
|
|
288
|
+
openTasks: this.store.listOpenTasks(),
|
|
289
|
+
unfoldedEvents: this.store.readLatestEventsAfter(position, UNFOLDED_CONTEXT_EVENTS),
|
|
290
|
+
unfoldedTotal: this.store.latestSeq() - position,
|
|
291
|
+
interruptedTaskIds: this.interruptedTaskIds(),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
logger.warn(`[thread-tasks] context unavailable thread=${this.threadId}: ${errorMessage(error)}`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Queue a resume for the next runnable task (PR 5), so the thread keeps
|
|
301
|
+
* working without a new owner message. Crash-safe in three steps:
|
|
302
|
+
* reserve the delivery record (`recovery:<task>:v<version>`), enqueue a
|
|
303
|
+
* system message whose id is that key, then mark it delivered. A crash
|
|
304
|
+
* before the enqueue leaves a reservation that the next pass re-sends; a
|
|
305
|
+
* crash after it is harmless because the queue dedups the id. A resume
|
|
306
|
+
* that was already delivered is never queued again for the same version.
|
|
307
|
+
*
|
|
308
|
+
* Never throws. Returns what it queued.
|
|
309
|
+
*/
|
|
310
|
+
deliverResumes(queue, now = Date.now()) {
|
|
311
|
+
if (this.closed)
|
|
312
|
+
return { queued: [] };
|
|
313
|
+
const queued = [];
|
|
314
|
+
try {
|
|
315
|
+
// Re-send reservations a previous pass made but did not deliver.
|
|
316
|
+
for (const record of this.store.listUndeliveredRecoveries()) {
|
|
317
|
+
const task = this.store.getTask(record.taskId);
|
|
318
|
+
if (this.enqueueResume(queue, record.idempotencyKey, task, record.taskVersion))
|
|
319
|
+
queued.push(record.idempotencyKey);
|
|
320
|
+
}
|
|
321
|
+
if (queued.length < RESUMES_PER_PASS) {
|
|
322
|
+
// Work the previous process left in progress comes first. Its resume
|
|
323
|
+
// may have been replayed already (then it is accepted and skipped).
|
|
324
|
+
const interrupted = [...this.interruptedTaskIds()]
|
|
325
|
+
.filter((id) => !this.resumesAccepted.has(id))
|
|
326
|
+
.map((id) => this.store.getTask(id))
|
|
327
|
+
.filter((task) => task !== undefined);
|
|
328
|
+
for (const task of [...interrupted, ...this.store.listRunnableTasks(now)]) {
|
|
329
|
+
if (queued.length >= RESUMES_PER_PASS)
|
|
330
|
+
break;
|
|
331
|
+
if (hasNoRemainingWork(task))
|
|
332
|
+
continue;
|
|
333
|
+
const reservation = this.store.reserveRecoveryDelivery({ taskId: task.id, taskVersion: task.version });
|
|
334
|
+
if (!reservation.ok) {
|
|
335
|
+
logger.warn(`[thread-tasks] resume not reserved thread=${this.threadId}: ${reservation.reason}`);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
// Already delivered for this version: a wake took it and made no
|
|
339
|
+
// recorded progress. Do not loop on it; progress bumps the version.
|
|
340
|
+
if (!reservation.value.reserved)
|
|
341
|
+
continue;
|
|
342
|
+
if (this.enqueueResume(queue, reservation.value.record.idempotencyKey, task, task.version)) {
|
|
343
|
+
queued.push(reservation.value.record.idempotencyKey);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return { queued };
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
logger.warn(`[thread-tasks] resume pass failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
351
|
+
return { queued };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Whether a dequeued resume may still run (the re-check before execution).
|
|
356
|
+
* Yes when its task is still pending at the version it was queued for and
|
|
357
|
+
* is due: a cancel, modification, wait or block since then makes it stale.
|
|
358
|
+
* Also yes, once per task per process, when a previous process left the
|
|
359
|
+
* task `in_progress` ({@link interruptedTaskIds}): that is a resume replayed
|
|
360
|
+
* after a crash, or one queued for the interrupted task. Its version is not
|
|
361
|
+
* compared, because folding the previous process's last events at startup
|
|
362
|
+
* bumps it.
|
|
363
|
+
*
|
|
364
|
+
* The "once" test is against {@link resumesAccepted}, which a requeue
|
|
365
|
+
* releases ({@link releaseResume}): an accepted resume whose message went
|
|
366
|
+
* back on the queue must still run when it is dequeued again, or the
|
|
367
|
+
* interrupted task keeps its only recovery row forever.
|
|
368
|
+
* Fail-closed: any error means no.
|
|
369
|
+
*/
|
|
370
|
+
isResumeRunnable(msg, now = Date.now()) {
|
|
371
|
+
try {
|
|
372
|
+
const taskId = msg.meta?.taskId;
|
|
373
|
+
const taskVersion = msg.meta?.taskVersion;
|
|
374
|
+
if (typeof taskId !== "string" || typeof taskVersion !== "number")
|
|
375
|
+
return false;
|
|
376
|
+
const task = this.store.getTask(taskId);
|
|
377
|
+
if (!task || hasNoRemainingWork(task))
|
|
378
|
+
return false;
|
|
379
|
+
// Left in progress by a previous process: once, whatever the version.
|
|
380
|
+
if (task.status === "in_progress" && this.interruptedTaskIds().has(task.id)) {
|
|
381
|
+
return !this.resumesAccepted.has(task.id);
|
|
382
|
+
}
|
|
383
|
+
// Pending, or unfinished by an earlier wake of this process: unchanged
|
|
384
|
+
// since it was queued. Any progress, wait, block or cancel bumps the version.
|
|
385
|
+
return (task.status === "pending" || task.status === "in_progress")
|
|
386
|
+
&& task.version === taskVersion
|
|
387
|
+
&& (task.runAfter === undefined || task.runAfter <= now);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
logger.warn(`[thread-tasks] resume check failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* The re-check at dequeue: {@link isResumeRunnable}, and if yes, record
|
|
396
|
+
* that this process is now working on the task so a second copy of its
|
|
397
|
+
* resume is stale.
|
|
398
|
+
*/
|
|
399
|
+
acceptResume(msg, now = Date.now()) {
|
|
400
|
+
if (!this.isResumeRunnable(msg, now))
|
|
401
|
+
return false;
|
|
402
|
+
this.resumesAccepted.add(msg.meta?.taskId);
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* The resume was accepted but its message went back on the queue (a failed
|
|
407
|
+
* inject, an orphaned injection, a failed wake). Takes the acceptance back
|
|
408
|
+
* so the next dequeue may run it. Safe for any message: not a resume, or
|
|
409
|
+
* never accepted, does nothing. #2664 review P1-B.
|
|
410
|
+
*/
|
|
411
|
+
releaseResume(messages) {
|
|
412
|
+
for (const msg of messages) {
|
|
413
|
+
const taskId = msg.meta?.taskId;
|
|
414
|
+
if (!isTaskResumeMessage(msg) || typeof taskId !== "string")
|
|
415
|
+
continue;
|
|
416
|
+
if (this.resumesAccepted.delete(taskId)) {
|
|
417
|
+
logger.info(`[thread-tasks] resume released back to the queue thread=${this.threadId} task=${taskId}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** Install the loop's hooks. Resumes are only queued once this is set. */
|
|
422
|
+
enableResumes(hooks) {
|
|
423
|
+
this.resumeHooks = hooks;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* The owner sent `/stop` (option B, owner 10-04): cancel every unfinished
|
|
427
|
+
* task of this thread. Cancelled is a stored status, so nothing queued
|
|
428
|
+
* starts on its own, a restart cannot bring it back, and resumes already
|
|
429
|
+
* queued are retired at dequeue.
|
|
430
|
+
*
|
|
431
|
+
* The open tasks are cancelled at once. A message sent just before
|
|
432
|
+
* `/stop` may not be folded yet: the sidecar then folds up to the stop
|
|
433
|
+
* (bounded by `waitMs`) and the tasks those events created are cancelled
|
|
434
|
+
* too. Tasks from messages after the `/stop` are kept. Returns every
|
|
435
|
+
* cancelled task, for the reply. Never throws.
|
|
436
|
+
* TODO: a fold that misses `waitMs` (or more than {@link STOP_EVENTS}
|
|
437
|
+
* unfolded events) can still create a task from a pre-stop message later.
|
|
438
|
+
*/
|
|
439
|
+
async cancelOpenTasks(waitMs = RECONCILE_WAIT_MS.wakeEnd, queue) {
|
|
440
|
+
if (this.closed)
|
|
441
|
+
return { cancelled: [], droppedQueued: 0 };
|
|
442
|
+
let beforeStop;
|
|
443
|
+
let cancelled;
|
|
444
|
+
let droppedQueued = 0;
|
|
445
|
+
try {
|
|
446
|
+
// Queue rows first, at this boundary: a message the interrupt debounce
|
|
447
|
+
// has not recorded yet is not an event and owns no task, so nothing
|
|
448
|
+
// below would ever see it. Left in place, the next wake reads it as a
|
|
449
|
+
// new request and runs it (#2664 review P1-A).
|
|
450
|
+
droppedQueued = this.dropQueuedMessages(queue);
|
|
451
|
+
beforeStop = new Set(this.store.readUnconsumedEvents(STOP_EVENTS).map((event) => event.id));
|
|
452
|
+
cancelled = this.cancelTasks(this.store.listOpenTasks());
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
logger.warn(`[thread-tasks] /stop cancel failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
456
|
+
return { cancelled: [], droppedQueued };
|
|
457
|
+
}
|
|
458
|
+
if (beforeStop.size === 0)
|
|
459
|
+
return { cancelled, droppedQueued };
|
|
460
|
+
await this.reconcile("stop", waitMs);
|
|
461
|
+
if (this.isClosed())
|
|
462
|
+
return { cancelled, droppedQueued };
|
|
463
|
+
try {
|
|
464
|
+
// A task's first source event is the one that created it.
|
|
465
|
+
const late = this.store.listOpenTasks().filter((task) => beforeStop.has(task.sourceEventIds[0] ?? ""));
|
|
466
|
+
return { cancelled: [...cancelled, ...this.cancelTasks(late)], droppedQueued };
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
logger.warn(`[thread-tasks] /stop cancel failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
470
|
+
return { cancelled, droppedQueued };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Drop queued owner messages: rows not claimed by a wake, that are not
|
|
475
|
+
* system rows (a resume, or the stop command itself) and that the task
|
|
476
|
+
* sidecar would read as owner input. Returns how many were dropped.
|
|
477
|
+
* Fail-safe: a row that cannot be read is left alone.
|
|
478
|
+
*/
|
|
479
|
+
dropQueuedMessages(queue) {
|
|
480
|
+
if (!queue)
|
|
481
|
+
return 0;
|
|
482
|
+
let dropped = 0;
|
|
483
|
+
for (const msg of queue.snapshot()) {
|
|
484
|
+
if (msg.channel === "system" || !isTaskRelevantMessage(msg))
|
|
485
|
+
continue;
|
|
486
|
+
try {
|
|
487
|
+
// Handled, not just removed: a provider retry of the same message
|
|
488
|
+
// would otherwise be accepted again as a request after the stop.
|
|
489
|
+
if (queue.remove(msg.id)) {
|
|
490
|
+
queue.markHandled(msg);
|
|
491
|
+
dropped += 1;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
logger.warn(`[thread-tasks] could not drop queued message ${msg.id} thread=${this.threadId}: ${errorMessage(error)}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (dropped > 0)
|
|
499
|
+
logger.system(`[thread-tasks] /stop dropped ${dropped} queued message(s) thread=${this.threadId}`);
|
|
500
|
+
return dropped;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Commit `cancelled` for these tasks without consuming any event, so the
|
|
504
|
+
* sidecar still folds everything. Read and commit run in one synchronous
|
|
505
|
+
* stretch, so no sidecar commit lands between them; a sidecar call in
|
|
506
|
+
* flight hits a version conflict and re-reads.
|
|
507
|
+
*/
|
|
508
|
+
cancelTasks(tasks) {
|
|
509
|
+
if (tasks.length === 0)
|
|
510
|
+
return [];
|
|
511
|
+
const position = this.store.getPosition();
|
|
512
|
+
const result = this.store.commit({
|
|
513
|
+
expectedPosition: position,
|
|
514
|
+
throughSeq: position,
|
|
515
|
+
changes: tasks.map((task) => ({ op: "update", taskId: task.id, expectedVersion: task.version, patch: { status: "cancelled" } })),
|
|
516
|
+
});
|
|
517
|
+
if (!result.ok) {
|
|
518
|
+
logger.warn(`[thread-tasks] /stop cancel rejected thread=${this.threadId}: ${result.reason} ${result.detail}`);
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
logger.system(`[thread-tasks] /stop cancelled ${tasks.length} task(s) thread=${this.threadId}`);
|
|
522
|
+
return [...tasks];
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Close one open task of this thread for the agent's `close_thread_task`
|
|
526
|
+
* tool (#2822). The sidecar completes a task only with evidence from the
|
|
527
|
+
* batch it folds, so finished work it missed stayed open and kept being
|
|
528
|
+
* resumed; this is the explicit way out. Commits like {@link cancelTasks}:
|
|
529
|
+
* no event is consumed, and a sidecar call in flight hits a version
|
|
530
|
+
* conflict and re-reads. Never throws.
|
|
531
|
+
*/
|
|
532
|
+
closeTask(input) {
|
|
533
|
+
if (this.closed)
|
|
534
|
+
return { ok: false, reason: "The task list is closed." };
|
|
535
|
+
try {
|
|
536
|
+
// Scoped to this thread: another thread's id is not found.
|
|
537
|
+
const task = this.store.getTask(input.taskId);
|
|
538
|
+
if (!task)
|
|
539
|
+
return { ok: false, reason: `No task with id ${input.taskId} in this thread.` };
|
|
540
|
+
if (task.status === "completed" || task.status === "cancelled") {
|
|
541
|
+
return { ok: false, reason: `Task ${task.id} is already ${task.status}.` };
|
|
542
|
+
}
|
|
543
|
+
const position = this.store.getPosition();
|
|
544
|
+
const result = this.store.commit({
|
|
545
|
+
expectedPosition: position,
|
|
546
|
+
throughSeq: position,
|
|
547
|
+
changes: [{ op: "update", taskId: task.id, expectedVersion: task.version, patch: {
|
|
548
|
+
status: input.status,
|
|
549
|
+
result: { summary: input.summary, evidence: [input.evidenceRef] },
|
|
550
|
+
resumeCondition: null,
|
|
551
|
+
} }],
|
|
552
|
+
});
|
|
553
|
+
if (!result.ok)
|
|
554
|
+
return { ok: false, reason: `Not closed: ${result.reason} ${result.detail}` };
|
|
555
|
+
logger.system(`[thread-tasks] agent closed task=${task.id} as ${input.status} thread=${this.threadId}`);
|
|
556
|
+
const closed = this.store.getTask(task.id);
|
|
557
|
+
return closed ? { ok: true, task: closed } : { ok: false, reason: `Task ${task.id} vanished after closing.` };
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
logger.warn(`[thread-tasks] close failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
561
|
+
return { ok: false, reason: `Not closed: ${errorMessage(error)}` };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Fold the events up to now, then queue the next runnable task. Called at
|
|
566
|
+
* wake end and at startup; never throws and never blocks the caller's
|
|
567
|
+
* loop. Without the fold a task the wake just finished could still look
|
|
568
|
+
* pending, so a pass that cannot catch up delivers nothing and retries
|
|
569
|
+
* after {@link RESUME_RETRY_MS} instead. A fold that needs a slow sidecar
|
|
570
|
+
* call therefore starts the next task about a minute later, not at once.
|
|
571
|
+
*/
|
|
572
|
+
async requestResume(waitMs = RECONCILE_WAIT_MS.wakeEnd) {
|
|
573
|
+
const hooks = this.resumeHooks;
|
|
574
|
+
if (!hooks || this.closed || this.resumeRunning)
|
|
575
|
+
return { queued: [] };
|
|
576
|
+
this.resumeRunning = true;
|
|
577
|
+
this.clearResumeRetry();
|
|
578
|
+
try {
|
|
579
|
+
const reconciled = await this.reconcile("wake_end", waitMs);
|
|
580
|
+
if (this.isClosed())
|
|
581
|
+
return { queued: [] };
|
|
582
|
+
// Events that arrived during the fold (another wake ran and ended; its
|
|
583
|
+
// own pass was dropped because this one was running) may cancel or
|
|
584
|
+
// finish the task this pass would queue: fold them first.
|
|
585
|
+
if (!reconciled.reached || this.store.latestSeq() > reconciled.target) {
|
|
586
|
+
this.scheduleResumeRetry();
|
|
587
|
+
return { queued: [] };
|
|
588
|
+
}
|
|
589
|
+
// Busy: usually a wake, whose end starts the next pass; but nightly
|
|
590
|
+
// maintenance is busy too and has no wake end, so retry either way.
|
|
591
|
+
if (hooks.isBusy() || hooks.isGated()) {
|
|
592
|
+
this.scheduleResumeRetry();
|
|
593
|
+
return { queued: [] };
|
|
594
|
+
}
|
|
595
|
+
const outcome = this.deliverResumes(hooks.queue);
|
|
596
|
+
if (outcome.queued.length > 0)
|
|
597
|
+
hooks.wake();
|
|
598
|
+
// By design (owner 10-03): a not-yet-due task waits; its due or
|
|
599
|
+
// unblock wake comes from the existing calendar / event-trigger
|
|
600
|
+
// messages to this thread. Thread tasks never schedule wakes.
|
|
601
|
+
return outcome;
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
logger.warn(`[thread-tasks] resume request failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
605
|
+
return { queued: [] };
|
|
606
|
+
}
|
|
607
|
+
finally {
|
|
608
|
+
this.resumeRunning = false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
scheduleResumeRetry() {
|
|
612
|
+
if (this.closed || this.resumeRetry)
|
|
613
|
+
return;
|
|
614
|
+
this.resumeRetry = setTimeout(() => {
|
|
615
|
+
this.resumeRetry = null;
|
|
616
|
+
void this.requestResume();
|
|
617
|
+
}, RESUME_RETRY_MS);
|
|
618
|
+
this.resumeRetry.unref();
|
|
619
|
+
}
|
|
620
|
+
clearResumeRetry() {
|
|
621
|
+
if (this.resumeRetry)
|
|
622
|
+
clearTimeout(this.resumeRetry);
|
|
623
|
+
this.resumeRetry = null;
|
|
624
|
+
}
|
|
625
|
+
/** Enqueue one reserved resume and mark it delivered. False when nothing was handed over. */
|
|
626
|
+
enqueueResume(queue, key, task, version) {
|
|
627
|
+
if (!task) {
|
|
628
|
+
// TODO: a reservation whose task row is gone stays reserved; harmless,
|
|
629
|
+
// the next pass skips it again.
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
// `enqueue` returns false for an id it already holds or handled: that
|
|
633
|
+
// resume reached the queue before a crash, so marking it is still right.
|
|
634
|
+
queue.enqueue(resumeMessage(this.threadId, key, task, version));
|
|
635
|
+
const marked = this.store.markRecoveryDelivered(key);
|
|
636
|
+
if (!marked.ok) {
|
|
637
|
+
logger.warn(`[thread-tasks] resume ${key} queued but not marked thread=${this.threadId}: ${marked.reason}`);
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
this.recordExecution({
|
|
641
|
+
idempotencyKey: key,
|
|
642
|
+
kind: "recovery",
|
|
643
|
+
payload: { text: `Resumed: ${task.title}`, data: { taskId: task.id, taskVersion: version } },
|
|
644
|
+
});
|
|
645
|
+
logger.system(`[thread-tasks] queued resume thread=${this.threadId} task=${task.id} v${version}`);
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
/** Events not yet folded into tasks (the sidecar is behind or failing). */
|
|
649
|
+
pendingEventCount() {
|
|
650
|
+
try {
|
|
651
|
+
return this.store.readUnconsumedEvents(1_000).length;
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
return 0;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async close(graceMs) {
|
|
658
|
+
if (this.closed)
|
|
659
|
+
return;
|
|
660
|
+
this.closed = true;
|
|
661
|
+
this.clearResumeRetry();
|
|
662
|
+
try {
|
|
663
|
+
await this.controller.close(graceMs);
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
logger.warn(`[thread-tasks] controller close failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
this.store.close();
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
// Closing twice or after a failed open is harmless.
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
safeNotify(immediate) {
|
|
676
|
+
try {
|
|
677
|
+
this.controller.notify({ immediate });
|
|
678
|
+
}
|
|
679
|
+
catch (error) {
|
|
680
|
+
logger.warn(`[thread-tasks] notify failed thread=${this.threadId}: ${errorMessage(error)}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* A task whose checkpoint lists nothing remaining and no blockers: the work
|
|
686
|
+
* is done but the sidecar did not complete it (no evidence in its batch).
|
|
687
|
+
* Resuming it only wakes the agent to find nothing to do (#2822). A task
|
|
688
|
+
* with no checkpoint yet has not been worked on, so it still resumes.
|
|
689
|
+
* TODO: a checkpoint that empties `remaining` too early stops auto-resume;
|
|
690
|
+
* the task stays open and visible in the context block.
|
|
691
|
+
*/
|
|
692
|
+
export function hasNoRemainingWork(task) {
|
|
693
|
+
return task.checkpoint?.remaining.length === 0 && task.checkpoint.blockers.length === 0;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* The queue row for a resume. Its id is the delivery key, so the queue's
|
|
697
|
+
* own dedup refuses a second copy. `system` keeps it out of fast replies,
|
|
698
|
+
* the watchdog, activity cards and gate replies; the reply target travels
|
|
699
|
+
* with the task, not with this row.
|
|
700
|
+
*/
|
|
701
|
+
function resumeMessage(threadId, key, task, version) {
|
|
702
|
+
// Replies go where the task came from, like any task continuation.
|
|
703
|
+
const routing = task.replyTarget
|
|
704
|
+
? taskContinuationRouting({ sender: RESUME_SENDER, delivery: task.replyTarget })
|
|
705
|
+
: { meta: {} };
|
|
706
|
+
return {
|
|
707
|
+
id: key,
|
|
708
|
+
channel: "system",
|
|
709
|
+
sender: RESUME_SENDER,
|
|
710
|
+
text: `[Thread task resume] Continue the queued task "${task.title}" (id ${task.id}). ` +
|
|
711
|
+
"Its requirements and checkpoint are in the Thread Tasks block. Check the checkpoint and results first; never redo a step listed as done. " +
|
|
712
|
+
"If it is no longer wanted or cannot start yet, report that instead of doing it.",
|
|
713
|
+
attachments: [],
|
|
714
|
+
timestamp: new Date().toISOString(),
|
|
715
|
+
...routing,
|
|
716
|
+
meta: { ...routing.meta, kind: TASK_RESUME_KIND, taskId: task.id, taskVersion: version },
|
|
717
|
+
...(threadId !== DEFAULT_THREAD_ID ? { threadId } : {}),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
/** True for a queued thread-task resume row. */
|
|
721
|
+
export function isTaskResumeMessage(msg) {
|
|
722
|
+
return msg.channel === "system" && msg.meta?.kind === TASK_RESUME_KIND;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* The re-check before execution, at every place a wake takes rows off the
|
|
726
|
+
* queue: a resume whose task was cancelled, changed, put on hold or already
|
|
727
|
+
* started since it was queued is retired unexecuted. Without a runtime (the
|
|
728
|
+
* switch was turned off) every resume is stale. Returns true when retired.
|
|
729
|
+
*/
|
|
730
|
+
export function retireStaleResume(queue, runtime, msg) {
|
|
731
|
+
if (!isTaskResumeMessage(msg) || runtime?.acceptResume(msg) === true)
|
|
732
|
+
return false;
|
|
733
|
+
logger.system(`[thread-tasks] dropping stale resume ${msg.id}`);
|
|
734
|
+
queue.markHandled(msg);
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Messages the task store tracks: anything from a real channel. System
|
|
739
|
+
* messages (heartbeats, internal continuations) and external-wake markers
|
|
740
|
+
* carry no owner request. Email and group chats are included; the sidecar
|
|
741
|
+
* decides whether they hold a request.
|
|
742
|
+
*/
|
|
743
|
+
export function isTaskRelevantMessage(msg) {
|
|
744
|
+
return msg.channel !== "system" && msg.meta?.kind !== EXTERNAL_WAKE_KIND;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* The end-of-wake handoff (issue #1996 PR 1 rule): a claim is released as
|
|
748
|
+
* handled only after its message is a durable task event, so the follow-up
|
|
749
|
+
* belongs to the task store instead of vanishing with the queue row.
|
|
750
|
+
*
|
|
751
|
+
* Fail-safe: if recording fails the claims are still released. The agent
|
|
752
|
+
* already processed them in this wake; replaying them would redo the work.
|
|
753
|
+
* The error is logged. `handled` never means a task is completed.
|
|
754
|
+
*/
|
|
755
|
+
export function releaseClaimsAfterRecording(queue, runtime) {
|
|
756
|
+
if (runtime) {
|
|
757
|
+
try {
|
|
758
|
+
runtime.recordMessages(queue.currentCycleClaims().filter(isTaskRelevantMessage));
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
logger.err(`[thread-tasks] could not record claims before release: ${errorMessage(error)}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
queue.markAllInFlightHandled();
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Wake end. A wake cut short by an engine failover restart leaves its claims
|
|
768
|
+
* in flight for the replacement process (issue #2519) and folds nothing.
|
|
769
|
+
* Otherwise release the claims, fold this wake's events into the
|
|
770
|
+
* checkpoints and queue the next runnable task (PR 5). Not awaited: the
|
|
771
|
+
* session keeps its transcript, so the loop need not hold the next message
|
|
772
|
+
* for it. Bounded; leftovers stay unconsumed and the next wake's context
|
|
773
|
+
* shows them raw.
|
|
774
|
+
*/
|
|
775
|
+
export function endWakeThreadTasks(queue, runtime, options) {
|
|
776
|
+
if (options.failoverRestartPending)
|
|
777
|
+
return;
|
|
778
|
+
releaseClaimsAfterRecording(queue, runtime);
|
|
779
|
+
void runtime?.requestResume();
|
|
780
|
+
}
|
|
781
|
+
/** Per-event caps; the sidecar clips again, this keeps the event log small. */
|
|
782
|
+
const EXECUTION_TEXT_CHARS = 1_500;
|
|
783
|
+
const TOOL_OUTPUT_CHARS = 400;
|
|
784
|
+
function clipText(text, max) {
|
|
785
|
+
return text.length <= max ? text : `${text.slice(0, max)}… [${text.length - max} more chars]`;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Provider tool-call ids are not unique for the life of a thread: some
|
|
789
|
+
* providers number them per response (`functions.notify_user:0`). A stream
|
|
790
|
+
* emits each result once, so the key only has to be unique; a process nonce
|
|
791
|
+
* plus a counter makes it so.
|
|
792
|
+
*/
|
|
793
|
+
const EXECUTION_KEY_NONCE = randomUUID().slice(0, 8);
|
|
794
|
+
let executionKeySeq = 0;
|
|
795
|
+
function executionKey(prefix, toolUseId) {
|
|
796
|
+
executionKeySeq += 1;
|
|
797
|
+
return `${prefix}:${toolUseId}:${EXECUTION_KEY_NONCE}-${executionKeySeq}`;
|
|
798
|
+
}
|
|
799
|
+
/** Tools whose results say nothing about task progress. */
|
|
800
|
+
const NOISE_TOOLS = new Set(["wait", "finish", "TodoWrite", "TaskCreate", "TaskUpdate", "TaskList", "TaskGet"]);
|
|
801
|
+
function baseToolName(name) {
|
|
802
|
+
return name.replace(/^mcp__[^_]+(?:_[^_]+)*__/, "");
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Engine-neutral execution events. Both engines feed the same stream
|
|
806
|
+
* handler, so these are called from there. Each id is unique per call, so
|
|
807
|
+
* keys never collide across wakes or retries.
|
|
808
|
+
*/
|
|
809
|
+
export const executionEvents = {
|
|
810
|
+
toolResult(toolUseId, toolName, output, isError) {
|
|
811
|
+
const name = baseToolName(toolName);
|
|
812
|
+
// Some providers stream tool calls without an id; a shared empty key
|
|
813
|
+
// would dedup every later one away, so those results are not recorded.
|
|
814
|
+
if (NOISE_TOOLS.has(name) || !toolUseId)
|
|
815
|
+
return null;
|
|
816
|
+
return {
|
|
817
|
+
idempotencyKey: executionKey("tool", toolUseId),
|
|
818
|
+
kind: "tool_result",
|
|
819
|
+
payload: { data: { tool: name, ok: !isError, output: clipText(output, TOOL_OUTPUT_CHARS) } },
|
|
820
|
+
};
|
|
821
|
+
},
|
|
822
|
+
todos(toolUseId, todos) {
|
|
823
|
+
if (!toolUseId)
|
|
824
|
+
return null;
|
|
825
|
+
return {
|
|
826
|
+
idempotencyKey: executionKey("todo", toolUseId),
|
|
827
|
+
kind: "todo_update",
|
|
828
|
+
payload: { data: { todos: todos.map((todo) => ({ content: clipText(todo.content, 200), status: todo.status })) } },
|
|
829
|
+
};
|
|
830
|
+
},
|
|
831
|
+
response(turnId, text) {
|
|
832
|
+
const trimmed = text.trim();
|
|
833
|
+
if (!trimmed)
|
|
834
|
+
return null;
|
|
835
|
+
return { idempotencyKey: `response:${turnId}`, kind: "assistant_response", payload: { text: clipText(trimmed, EXECUTION_TEXT_CHARS) } };
|
|
836
|
+
},
|
|
837
|
+
/** What the agent told the owner through notify_user: an explicit status report. */
|
|
838
|
+
statusReport(toolUseId, message) {
|
|
839
|
+
const trimmed = message.trim();
|
|
840
|
+
if (!trimmed || !toolUseId)
|
|
841
|
+
return null;
|
|
842
|
+
return { idempotencyKey: executionKey("report", toolUseId), kind: "status_report", payload: { text: clipText(trimmed, EXECUTION_TEXT_CHARS) } };
|
|
843
|
+
},
|
|
844
|
+
};
|
|
845
|
+
/**
|
|
846
|
+
* The fast responder's reading of a batch, as one candidate event. It shares
|
|
847
|
+
* the message's dedup key, so the sidecar's origin key matches the message's
|
|
848
|
+
* and the same task is never created twice. Only for a single-message batch:
|
|
849
|
+
* a brief over several messages cannot be keyed to the right one, and the
|
|
850
|
+
* messages themselves are already events.
|
|
851
|
+
*/
|
|
852
|
+
export function fastResponderCandidate(messages, brief) {
|
|
853
|
+
const relevant = messages.filter(isTaskRelevantMessage);
|
|
854
|
+
const first = relevant.length === 1 ? relevant[0] : undefined;
|
|
855
|
+
const text = typeof brief === "string" ? brief.trim() : "";
|
|
856
|
+
if (!first || !text)
|
|
857
|
+
return null;
|
|
858
|
+
const dedupKey = getMessageDedupKey(first);
|
|
859
|
+
return {
|
|
860
|
+
idempotencyKey: `candidate:${dedupKey}`,
|
|
861
|
+
kind: "fast_responder_candidate",
|
|
862
|
+
source: { channel: first.channel, sender: first.sender, messageId: first.id, dedupKey },
|
|
863
|
+
payload: { text: clipText(text, EXECUTION_TEXT_CHARS) },
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/** Record the fast responder's brief as a candidate event. Never throws. */
|
|
867
|
+
export function recordFastResponderCandidate(messages, brief) {
|
|
868
|
+
try {
|
|
869
|
+
recordExecutionEvent(fastResponderCandidate(messages, brief));
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
logger.warn(`[thread-tasks] could not record fast-responder candidate: ${errorMessage(error)}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/** Record an execution event on the process runtime, if there is one. Never throws. */
|
|
876
|
+
export function recordExecutionEvent(input) {
|
|
877
|
+
if (input)
|
|
878
|
+
current?.recordExecution(input);
|
|
879
|
+
}
|
|
880
|
+
/** The process runtime's recovery context, or null without a runtime (then nothing is injected). */
|
|
881
|
+
export function buildThreadTaskContext() {
|
|
882
|
+
return current?.contextBlock() ?? null;
|
|
883
|
+
}
|
|
884
|
+
/** Bounded reconcile on the process runtime; resolves immediately without one. */
|
|
885
|
+
export async function reconcileThreadTasks(phase, waitMs = RECONCILE_WAIT_MS.boundary) {
|
|
886
|
+
await current?.reconcile(phase, waitMs);
|
|
887
|
+
}
|
|
888
|
+
/** The runtime for this process, set once by the loop. Tools and hooks read it. */
|
|
889
|
+
let current = null;
|
|
890
|
+
export function setThreadTaskRuntime(runtime) {
|
|
891
|
+
current = runtime;
|
|
892
|
+
}
|
|
893
|
+
export function getThreadTaskRuntime() {
|
|
894
|
+
return current;
|
|
895
|
+
}
|
|
896
|
+
//# sourceMappingURL=thread-task-runtime.js.map
|