@gajae-code/coding-agent 0.2.4 → 0.3.0
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/CHANGELOG.md +27 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +145 -2
- package/dist/types/commands/harness.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +13 -3
- package/dist/types/config/settings.d.ts +3 -1
- package/dist/types/deep-interview/render-middleware.d.ts +5 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +8 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +24 -0
- package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
- package/dist/types/harness-control-plane/classifier.d.ts +13 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +30 -0
- package/dist/types/harness-control-plane/finalize.d.ts +47 -0
- package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
- package/dist/types/harness-control-plane/operate.d.ts +35 -0
- package/dist/types/harness-control-plane/owner.d.ts +46 -0
- package/dist/types/harness-control-plane/preserve.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +88 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
- package/dist/types/harness-control-plane/seams.d.ts +21 -0
- package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
- package/dist/types/harness-control-plane/storage.d.ts +53 -0
- package/dist/types/harness-control-plane/types.d.ts +162 -0
- package/dist/types/hooks/skill-keywords.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +2 -29
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/theme/defaults/index.d.ts +45 -9477
- package/dist/types/modes/theme/theme.d.ts +1 -5
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/skill-state/active-state.d.ts +3 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/types.d.ts +56 -3
- package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +24 -0
- package/dist/types/tools/cron.d.ts +110 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/monitor.d.ts +54 -0
- package/dist/types/tools/subagent.d.ts +11 -1
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/provider.d.ts +11 -4
- package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +522 -6
- package/src/cli/agents-cli.ts +3 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/config-cli.ts +10 -2
- package/src/cli.ts +2 -0
- package/src/commands/harness.ts +592 -0
- package/src/commands/team.ts +36 -39
- package/src/config/settings-schema.ts +15 -2
- package/src/config/settings.ts +49 -7
- package/src/deep-interview/render-middleware.ts +366 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +9 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/defaults/gjc/skills/team/SKILL.md +47 -21
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
- package/src/discovery/helpers.ts +5 -0
- package/src/eval/js/shared/rewrite-imports.ts +1 -2
- package/src/exec/bash-executor.ts +20 -9
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +40 -21
- package/src/gjc-runtime/goal-mode-request.ts +11 -3
- package/src/gjc-runtime/ralplan-runtime.ts +27 -10
- package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
- package/src/gjc-runtime/state-graph.ts +86 -0
- package/src/gjc-runtime/state-migrations.ts +132 -0
- package/src/gjc-runtime/state-renderer.ts +345 -0
- package/src/gjc-runtime/state-runtime.ts +733 -21
- package/src/gjc-runtime/state-validation.ts +49 -0
- package/src/gjc-runtime/state-writer.ts +718 -0
- package/src/gjc-runtime/team-runtime.ts +1083 -89
- package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
- package/src/gjc-runtime/workflow-manifest.ts +425 -0
- package/src/harness-control-plane/classifier.ts +128 -0
- package/src/harness-control-plane/control-endpoint.ts +137 -0
- package/src/harness-control-plane/finalize.ts +222 -0
- package/src/harness-control-plane/frame-mapper.ts +286 -0
- package/src/harness-control-plane/operate.ts +225 -0
- package/src/harness-control-plane/owner.ts +553 -0
- package/src/harness-control-plane/preserve.ts +102 -0
- package/src/harness-control-plane/receipts.ts +216 -0
- package/src/harness-control-plane/rpc-adapter.ts +276 -0
- package/src/harness-control-plane/seams.ts +39 -0
- package/src/harness-control-plane/session-lease.ts +388 -0
- package/src/harness-control-plane/state-machine.ts +97 -0
- package/src/harness-control-plane/storage.ts +257 -0
- package/src/harness-control-plane/types.ts +214 -0
- package/src/hooks/skill-keywords.ts +4 -2
- package/src/hooks/skill-state.ts +25 -42
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/lsp/render.ts +1 -1
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/acp/acp-client-bridge.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/assistant-message.ts +5 -1
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/hook-selector.ts +72 -2
- package/src/modes/components/skill-hud/render.ts +7 -2
- package/src/modes/controllers/event-controller.ts +71 -6
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +19 -3
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/interactive-mode.ts +21 -2
- package/src/modes/theme/defaults/index.ts +0 -196
- package/src/modes/theme/theme.ts +35 -35
- package/src/modes/types.ts +2 -0
- package/src/prompts/agents/architect.md +5 -1
- package/src/prompts/agents/critic.md +5 -1
- package/src/prompts/agents/executor.md +13 -0
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/agents/planner.md +5 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/cron.md +25 -0
- package/src/prompts/tools/monitor.md +30 -0
- package/src/prompts/tools/subagent.md +33 -3
- package/src/runtime-mcp/oauth-flow.ts +4 -2
- package/src/sdk.ts +7 -0
- package/src/session/agent-session.ts +247 -38
- package/src/session/session-manager.ts +13 -1
- package/src/session/streaming-output.ts +21 -0
- package/src/skill-state/active-state.ts +222 -78
- package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
- package/src/skill-state/initial-phase.ts +2 -0
- package/src/skill-state/workflow-state-contract.ts +26 -0
- package/src/task/agents.ts +1 -0
- package/src/task/executor.ts +51 -8
- package/src/task/index.ts +120 -8
- package/src/task/render.ts +6 -3
- package/src/task/types.ts +57 -3
- package/src/tools/ask.ts +28 -7
- package/src/tools/bash-allowed-prefixes.ts +169 -0
- package/src/tools/bash.ts +190 -29
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/cron.ts +665 -0
- package/src/tools/index.ts +20 -2
- package/src/tools/monitor.ts +136 -0
- package/src/tools/subagent.ts +255 -64
- package/src/vim/engine.ts +3 -3
- package/src/web/search/index.ts +31 -18
- package/src/web/search/provider.ts +57 -12
- package/src/web/search/providers/duckduckgo.ts +279 -0
- package/src/web/search/types.ts +2 -0
- package/src/modes/theme/dark.json +0 -95
- package/src/modes/theme/defaults/alabaster.json +0 -93
- package/src/modes/theme/defaults/amethyst.json +0 -96
- package/src/modes/theme/defaults/anthracite.json +0 -93
- package/src/modes/theme/defaults/basalt.json +0 -91
- package/src/modes/theme/defaults/birch.json +0 -95
- package/src/modes/theme/defaults/dark-abyss.json +0 -91
- package/src/modes/theme/defaults/dark-arctic.json +0 -104
- package/src/modes/theme/defaults/dark-aurora.json +0 -95
- package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
- package/src/modes/theme/defaults/dark-cavern.json +0 -91
- package/src/modes/theme/defaults/dark-copper.json +0 -95
- package/src/modes/theme/defaults/dark-cosmos.json +0 -90
- package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
- package/src/modes/theme/defaults/dark-dracula.json +0 -98
- package/src/modes/theme/defaults/dark-eclipse.json +0 -91
- package/src/modes/theme/defaults/dark-ember.json +0 -95
- package/src/modes/theme/defaults/dark-equinox.json +0 -90
- package/src/modes/theme/defaults/dark-forest.json +0 -96
- package/src/modes/theme/defaults/dark-github.json +0 -105
- package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
- package/src/modes/theme/defaults/dark-lavender.json +0 -95
- package/src/modes/theme/defaults/dark-lunar.json +0 -89
- package/src/modes/theme/defaults/dark-midnight.json +0 -95
- package/src/modes/theme/defaults/dark-monochrome.json +0 -94
- package/src/modes/theme/defaults/dark-monokai.json +0 -98
- package/src/modes/theme/defaults/dark-nebula.json +0 -90
- package/src/modes/theme/defaults/dark-nord.json +0 -97
- package/src/modes/theme/defaults/dark-ocean.json +0 -101
- package/src/modes/theme/defaults/dark-one.json +0 -100
- package/src/modes/theme/defaults/dark-poimandres.json +0 -141
- package/src/modes/theme/defaults/dark-rainforest.json +0 -91
- package/src/modes/theme/defaults/dark-reef.json +0 -91
- package/src/modes/theme/defaults/dark-retro.json +0 -92
- package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
- package/src/modes/theme/defaults/dark-sakura.json +0 -95
- package/src/modes/theme/defaults/dark-slate.json +0 -95
- package/src/modes/theme/defaults/dark-solarized.json +0 -97
- package/src/modes/theme/defaults/dark-solstice.json +0 -90
- package/src/modes/theme/defaults/dark-starfall.json +0 -91
- package/src/modes/theme/defaults/dark-sunset.json +0 -99
- package/src/modes/theme/defaults/dark-swamp.json +0 -90
- package/src/modes/theme/defaults/dark-synthwave.json +0 -103
- package/src/modes/theme/defaults/dark-taiga.json +0 -91
- package/src/modes/theme/defaults/dark-terminal.json +0 -95
- package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
- package/src/modes/theme/defaults/dark-tundra.json +0 -91
- package/src/modes/theme/defaults/dark-twilight.json +0 -91
- package/src/modes/theme/defaults/dark-volcanic.json +0 -91
- package/src/modes/theme/defaults/graphite.json +0 -92
- package/src/modes/theme/defaults/light-arctic.json +0 -107
- package/src/modes/theme/defaults/light-aurora-day.json +0 -91
- package/src/modes/theme/defaults/light-canyon.json +0 -91
- package/src/modes/theme/defaults/light-catppuccin.json +0 -106
- package/src/modes/theme/defaults/light-cirrus.json +0 -90
- package/src/modes/theme/defaults/light-coral.json +0 -95
- package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
- package/src/modes/theme/defaults/light-dawn.json +0 -90
- package/src/modes/theme/defaults/light-dunes.json +0 -91
- package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
- package/src/modes/theme/defaults/light-forest.json +0 -100
- package/src/modes/theme/defaults/light-frost.json +0 -95
- package/src/modes/theme/defaults/light-github.json +0 -115
- package/src/modes/theme/defaults/light-glacier.json +0 -91
- package/src/modes/theme/defaults/light-gruvbox.json +0 -108
- package/src/modes/theme/defaults/light-haze.json +0 -90
- package/src/modes/theme/defaults/light-honeycomb.json +0 -95
- package/src/modes/theme/defaults/light-lagoon.json +0 -91
- package/src/modes/theme/defaults/light-lavender.json +0 -95
- package/src/modes/theme/defaults/light-meadow.json +0 -91
- package/src/modes/theme/defaults/light-mint.json +0 -95
- package/src/modes/theme/defaults/light-monochrome.json +0 -101
- package/src/modes/theme/defaults/light-ocean.json +0 -99
- package/src/modes/theme/defaults/light-one.json +0 -99
- package/src/modes/theme/defaults/light-opal.json +0 -91
- package/src/modes/theme/defaults/light-orchard.json +0 -91
- package/src/modes/theme/defaults/light-paper.json +0 -95
- package/src/modes/theme/defaults/light-poimandres.json +0 -141
- package/src/modes/theme/defaults/light-prism.json +0 -90
- package/src/modes/theme/defaults/light-retro.json +0 -98
- package/src/modes/theme/defaults/light-sand.json +0 -95
- package/src/modes/theme/defaults/light-savanna.json +0 -91
- package/src/modes/theme/defaults/light-solarized.json +0 -102
- package/src/modes/theme/defaults/light-soleil.json +0 -90
- package/src/modes/theme/defaults/light-sunset.json +0 -99
- package/src/modes/theme/defaults/light-synthwave.json +0 -98
- package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
- package/src/modes/theme/defaults/light-wetland.json +0 -91
- package/src/modes/theme/defaults/light-zenith.json +0 -89
- package/src/modes/theme/defaults/limestone.json +0 -94
- package/src/modes/theme/defaults/mahogany.json +0 -97
- package/src/modes/theme/defaults/marble.json +0 -93
- package/src/modes/theme/defaults/obsidian.json +0 -91
- package/src/modes/theme/defaults/onyx.json +0 -91
- package/src/modes/theme/defaults/pearl.json +0 -93
- package/src/modes/theme/defaults/porcelain.json +0 -91
- package/src/modes/theme/defaults/quartz.json +0 -96
- package/src/modes/theme/defaults/sandstone.json +0 -95
- package/src/modes/theme/defaults/titanium.json +0 -90
- package/src/modes/theme/light.json +0 -93
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/coding-agent",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Gajae Code CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
49
49
|
"@babel/parser": "^7.29.3",
|
|
50
50
|
"@mozilla/readability": "^0.6.0",
|
|
51
|
-
"@gajae-code/stats": "0.
|
|
52
|
-
"@gajae-code/agent-core": "0.
|
|
53
|
-
"@gajae-code/ai": "0.
|
|
54
|
-
"@gajae-code/natives": "0.
|
|
55
|
-
"@gajae-code/tui": "0.
|
|
56
|
-
"@gajae-code/utils": "0.
|
|
51
|
+
"@gajae-code/stats": "0.3.0",
|
|
52
|
+
"@gajae-code/agent-core": "0.3.0",
|
|
53
|
+
"@gajae-code/ai": "0.3.0",
|
|
54
|
+
"@gajae-code/natives": "0.3.0",
|
|
55
|
+
"@gajae-code/tui": "0.3.0",
|
|
56
|
+
"@gajae-code/utils": "0.3.0",
|
|
57
57
|
"@puppeteer/browsers": "^2.13.0",
|
|
58
58
|
"@types/turndown": "5.0.6",
|
|
59
59
|
"@xterm/headless": "^6.0.0",
|
package/src/async/job-manager.ts
CHANGED
|
@@ -10,7 +10,7 @@ const DEFAULT_MAX_RUNNING_JOBS = 15;
|
|
|
10
10
|
export interface AsyncJob {
|
|
11
11
|
id: string;
|
|
12
12
|
type: "bash" | "task";
|
|
13
|
-
status: "running" | "completed" | "failed" | "cancelled";
|
|
13
|
+
status: "running" | "completed" | "failed" | "cancelled" | "paused";
|
|
14
14
|
startTime: number;
|
|
15
15
|
label: string;
|
|
16
16
|
abortController: AbortController;
|
|
@@ -37,6 +37,64 @@ export interface AsyncJobMetadata {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Typed outcome a subagent task run may produce. A `paused` outcome is
|
|
42
|
+
* non-terminal and non-delivering: the run suspended at a safe boundary and the
|
|
43
|
+
* subagent can be resumed from its persisted sessionFile. `completed` always
|
|
44
|
+
* wins a race with a late pause because the run returns it once it has actually
|
|
45
|
+
* finished.
|
|
46
|
+
*/
|
|
47
|
+
export type SubagentRunOutcome = { kind: "completed"; text: string } | { kind: "paused"; note?: string };
|
|
48
|
+
|
|
49
|
+
/** Canonical lifecycle of a subagent across pause/resume cycles. */
|
|
50
|
+
export type SubagentLifecycle = "running" | "paused" | "queued" | "completed" | "failed" | "cancelled";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Live, executor-owned control handle for a RUNNING subagent. Registered when a
|
|
54
|
+
* subagent run starts and removed on pause/terminal so a paused subagent retains
|
|
55
|
+
* no live `AgentSession` reference (leak-free).
|
|
56
|
+
*/
|
|
57
|
+
export interface SubagentLiveHandle {
|
|
58
|
+
/** Request a cooperative safe-boundary pause (never aborts the in-flight tool). */
|
|
59
|
+
requestPause(): void;
|
|
60
|
+
/** Inject a steering message into the live session. */
|
|
61
|
+
injectMessage(content: string, deliverAs: "steer" | "followUp" | "nextTurn"): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Canonical, stable-id-keyed record for a subagent. Survives `AsyncJob`
|
|
66
|
+
* eviction so resume stays addressable by subagent id, and is the single source
|
|
67
|
+
* of truth for control-plane status and identity.
|
|
68
|
+
*/
|
|
69
|
+
export interface SubagentRecord {
|
|
70
|
+
subagentId: string;
|
|
71
|
+
ownerId?: string;
|
|
72
|
+
/** Current live/last AsyncJob id; null while queued with no active job. */
|
|
73
|
+
currentJobId: string | null;
|
|
74
|
+
historicalJobIds: string[];
|
|
75
|
+
status: SubagentLifecycle;
|
|
76
|
+
sessionFile: string | null;
|
|
77
|
+
/** False for ephemeral sessions (no persistent artifacts dir). */
|
|
78
|
+
resumable: boolean;
|
|
79
|
+
queued?: { ownerId?: string; seq: number; message?: string; createdAt: number };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Lightweight, manager-owned resume payload. The async layer treats `data` as opaque. */
|
|
83
|
+
export interface ResumeDescriptor {
|
|
84
|
+
subagentId: string;
|
|
85
|
+
ownerId?: string;
|
|
86
|
+
data: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A pending resume awaiting a free concurrency slot. */
|
|
90
|
+
interface ResumeQueueEntry {
|
|
91
|
+
subagentId: string;
|
|
92
|
+
ownerId?: string;
|
|
93
|
+
seq: number;
|
|
94
|
+
message?: string;
|
|
95
|
+
createdAt: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
40
98
|
export interface AsyncJobManagerOptions {
|
|
41
99
|
onJobComplete: (jobId: string, text: string, job?: AsyncJob) => void | Promise<void>;
|
|
42
100
|
maxRunningJobs?: number;
|
|
@@ -78,6 +136,56 @@ export interface AsyncJobFilter {
|
|
|
78
136
|
ownerId?: string;
|
|
79
137
|
}
|
|
80
138
|
|
|
139
|
+
function sliceTextFromUtf8ByteOffset(text: string, offsetBytes: number): string {
|
|
140
|
+
if (offsetBytes <= 0) return text;
|
|
141
|
+
let consumedBytes = 0;
|
|
142
|
+
let codeUnitIndex = 0;
|
|
143
|
+
for (const char of text) {
|
|
144
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
145
|
+
if (consumedBytes + charBytes > offsetBytes) break;
|
|
146
|
+
consumedBytes += charBytes;
|
|
147
|
+
codeUnitIndex += char.length;
|
|
148
|
+
}
|
|
149
|
+
return text.slice(codeUnitIndex);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A slice of process-stream output for a background job, as recorded by
|
|
154
|
+
* `appendOutput` / read by `readOutputSince`.
|
|
155
|
+
*
|
|
156
|
+
* The cursor model is monotonic UTF-8 byte offsets. `nextOffset` is the offset
|
|
157
|
+
* to pass to the next read to receive only fresh bytes; `startOffset` is the
|
|
158
|
+
* first byte the manager still retains for this job. When the requested offset
|
|
159
|
+
* is older than `startOffset`, the manager returns the retained tail and sets
|
|
160
|
+
* `truncated: true`.
|
|
161
|
+
*/
|
|
162
|
+
export interface AsyncJobOutputSlice {
|
|
163
|
+
jobId: string;
|
|
164
|
+
status: AsyncJob["status"];
|
|
165
|
+
text: string;
|
|
166
|
+
startOffset: number;
|
|
167
|
+
nextOffset: number;
|
|
168
|
+
truncated: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Internal: a single chunk of captured stdout/stderr keyed by its byte range. */
|
|
172
|
+
interface AsyncJobOutputChunk {
|
|
173
|
+
startByte: number;
|
|
174
|
+
endByte: number;
|
|
175
|
+
text: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface AsyncJobOutputState {
|
|
179
|
+
chunks: AsyncJobOutputChunk[];
|
|
180
|
+
startOffset: number;
|
|
181
|
+
nextOffset: number;
|
|
182
|
+
retainedBytes: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Default retention cap for per-job captured output. ~512 KiB matches the
|
|
186
|
+
* bash tail-buffer order of magnitude without dominating session memory. */
|
|
187
|
+
export const DEFAULT_JOB_OUTPUT_RETENTION_BYTES = 512 * 1024;
|
|
188
|
+
|
|
81
189
|
export class AsyncJobManager {
|
|
82
190
|
static #instance: AsyncJobManager | undefined;
|
|
83
191
|
|
|
@@ -102,11 +210,20 @@ export class AsyncJobManager {
|
|
|
102
210
|
readonly #suppressedDeliveries = new Set<string>();
|
|
103
211
|
readonly #watchedJobs = new Set<string>();
|
|
104
212
|
readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
|
|
213
|
+
readonly #outputState = new Map<string, AsyncJobOutputState>();
|
|
214
|
+
readonly #ownerCleanups = new Map<string, Set<() => void>>();
|
|
215
|
+
readonly #outputRetentionBytes = DEFAULT_JOB_OUTPUT_RETENTION_BYTES;
|
|
105
216
|
readonly #onJobComplete: AsyncJobManagerOptions["onJobComplete"];
|
|
106
217
|
readonly #maxRunningJobs: number;
|
|
107
218
|
readonly #retentionMs: number;
|
|
108
219
|
#deliveryLoop: Promise<void> | undefined;
|
|
109
220
|
#disposed = false;
|
|
221
|
+
readonly #subagentRecords = new Map<string, SubagentRecord>();
|
|
222
|
+
readonly #liveHandles = new Map<string, SubagentLiveHandle>();
|
|
223
|
+
readonly #resumeQueue: ResumeQueueEntry[] = [];
|
|
224
|
+
#resumeSeq = 0;
|
|
225
|
+
#resumeRunner?: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined;
|
|
226
|
+
readonly #resumeDescriptors = new Map<string, ResumeDescriptor>();
|
|
110
227
|
|
|
111
228
|
#filterJobs(jobs: Iterable<AsyncJob>, filter?: AsyncJobFilter): AsyncJob[] {
|
|
112
229
|
const ownerId = filter?.ownerId;
|
|
@@ -131,7 +248,7 @@ export class AsyncJobManager {
|
|
|
131
248
|
jobId: string;
|
|
132
249
|
signal: AbortSignal;
|
|
133
250
|
reportProgress: (text: string, details?: Record<string, unknown>) => Promise<void>;
|
|
134
|
-
}) => Promise<string>,
|
|
251
|
+
}) => Promise<string | SubagentRunOutcome>,
|
|
135
252
|
options?: AsyncJobRegisterOptions,
|
|
136
253
|
): string {
|
|
137
254
|
if (this.#disposed) {
|
|
@@ -174,20 +291,38 @@ export class AsyncJobManager {
|
|
|
174
291
|
};
|
|
175
292
|
job.promise = (async () => {
|
|
176
293
|
try {
|
|
177
|
-
const
|
|
294
|
+
const result = await run({ jobId: id, signal: abortController.signal, reportProgress });
|
|
295
|
+
const outcome: SubagentRunOutcome =
|
|
296
|
+
typeof result === "string" ? { kind: "completed", text: result } : result;
|
|
178
297
|
if (job.status === "cancelled") {
|
|
179
|
-
job.resultText = text;
|
|
298
|
+
job.resultText = outcome.kind === "completed" ? outcome.text : outcome.note;
|
|
180
299
|
this.#scheduleEviction(id);
|
|
300
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
301
|
+
this.#drainResumeQueue();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (outcome.kind === "paused") {
|
|
305
|
+
// Sole canonical writer of the running -> paused transition. No
|
|
306
|
+
// delivery and no eviction scheduling: a paused subagent stays
|
|
307
|
+
// listed and resumable from its sessionFile.
|
|
308
|
+
job.status = "paused";
|
|
309
|
+
if (outcome.note) job.resultText = outcome.note;
|
|
310
|
+
this.#markRecordPaused(id);
|
|
311
|
+
this.#drainResumeQueue();
|
|
181
312
|
return;
|
|
182
313
|
}
|
|
183
314
|
job.status = "completed";
|
|
184
|
-
job.resultText = text;
|
|
185
|
-
this.#enqueueDelivery(id, text);
|
|
315
|
+
job.resultText = outcome.text;
|
|
316
|
+
this.#enqueueDelivery(id, outcome.text);
|
|
186
317
|
this.#scheduleEviction(id);
|
|
318
|
+
this.#markRecordTerminal(id, "completed");
|
|
319
|
+
this.#drainResumeQueue();
|
|
187
320
|
} catch (error) {
|
|
188
321
|
if (job.status === "cancelled") {
|
|
189
322
|
job.errorText = error instanceof Error ? error.message : String(error);
|
|
190
323
|
this.#scheduleEviction(id);
|
|
324
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
325
|
+
this.#drainResumeQueue();
|
|
191
326
|
return;
|
|
192
327
|
}
|
|
193
328
|
const errorText = error instanceof Error ? error.message : String(error);
|
|
@@ -195,6 +330,8 @@ export class AsyncJobManager {
|
|
|
195
330
|
job.errorText = errorText;
|
|
196
331
|
this.#enqueueDelivery(id, errorText);
|
|
197
332
|
this.#scheduleEviction(id);
|
|
333
|
+
this.#markRecordTerminal(id, "failed");
|
|
334
|
+
this.#drainResumeQueue();
|
|
198
335
|
}
|
|
199
336
|
})();
|
|
200
337
|
|
|
@@ -211,6 +348,15 @@ export class AsyncJobManager {
|
|
|
211
348
|
const job = this.#jobs.get(id);
|
|
212
349
|
if (!job) return false;
|
|
213
350
|
if (filter?.ownerId && job.ownerId !== filter.ownerId) return false;
|
|
351
|
+
if (job.status === "paused") {
|
|
352
|
+
// Paused jobs have no running promise to abort; transition directly.
|
|
353
|
+
// The session file is kept, so the record stays resumable by id.
|
|
354
|
+
job.status = "cancelled";
|
|
355
|
+
this.#markRecordTerminal(id, "cancelled");
|
|
356
|
+
this.#scheduleEviction(id);
|
|
357
|
+
this.#drainResumeQueue();
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
214
360
|
if (job.status !== "running") return false;
|
|
215
361
|
job.status = "cancelled";
|
|
216
362
|
job.abortController.abort();
|
|
@@ -218,6 +364,200 @@ export class AsyncJobManager {
|
|
|
218
364
|
return true;
|
|
219
365
|
}
|
|
220
366
|
|
|
367
|
+
// ── Subagent control plane (pause / resume / steer support) ──────────
|
|
368
|
+
|
|
369
|
+
/** Register or replace the canonical record for a subagent. */
|
|
370
|
+
registerSubagentRecord(record: SubagentRecord): void {
|
|
371
|
+
this.#subagentRecords.set(record.subagentId, record);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
getSubagentRecord(subagentId: string, filter?: AsyncJobFilter): SubagentRecord | undefined {
|
|
375
|
+
const rec = this.#subagentRecords.get(subagentId.trim());
|
|
376
|
+
if (!rec) return undefined;
|
|
377
|
+
if (filter?.ownerId && rec.ownerId !== filter.ownerId) return undefined;
|
|
378
|
+
return rec;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
getSubagentRecords(filter?: AsyncJobFilter): SubagentRecord[] {
|
|
382
|
+
const ownerId = filter?.ownerId;
|
|
383
|
+
const out: SubagentRecord[] = [];
|
|
384
|
+
for (const rec of this.#subagentRecords.values()) {
|
|
385
|
+
if (ownerId && rec.ownerId !== ownerId) continue;
|
|
386
|
+
out.push(rec);
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
registerLiveHandle(subagentId: string, handle: SubagentLiveHandle): void {
|
|
392
|
+
this.#liveHandles.set(subagentId, handle);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
getLiveHandle(subagentId: string): SubagentLiveHandle | undefined {
|
|
396
|
+
return this.#liveHandles.get(subagentId);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
removeLiveHandle(subagentId: string): void {
|
|
400
|
+
this.#liveHandles.delete(subagentId);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Install the TaskTool-owned resume runner. Returns the new job id, or undefined on failure. */
|
|
404
|
+
setResumeRunner(
|
|
405
|
+
runner: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined,
|
|
406
|
+
): void {
|
|
407
|
+
this.#resumeRunner = runner;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
registerResumeDescriptor(descriptor: ResumeDescriptor): void {
|
|
411
|
+
this.#resumeDescriptors.set(descriptor.subagentId, descriptor);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
getResumeDescriptor(subagentId: string, filter?: AsyncJobFilter): ResumeDescriptor | undefined {
|
|
415
|
+
const descriptor = this.#resumeDescriptors.get(subagentId.trim());
|
|
416
|
+
if (!descriptor) return undefined;
|
|
417
|
+
if (filter?.ownerId && descriptor.ownerId !== filter.ownerId) return undefined;
|
|
418
|
+
return descriptor;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
#recordByJobId(jobId: string): SubagentRecord | undefined {
|
|
422
|
+
for (const rec of this.#subagentRecords.values()) {
|
|
423
|
+
if (rec.currentJobId === jobId) return rec;
|
|
424
|
+
}
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#markRecordPaused(jobId: string): void {
|
|
429
|
+
const rec = this.#recordByJobId(jobId);
|
|
430
|
+
if (rec) {
|
|
431
|
+
rec.status = "paused";
|
|
432
|
+
this.#liveHandles.delete(rec.subagentId);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
#markRecordTerminal(jobId: string, status: "completed" | "failed" | "cancelled"): void {
|
|
437
|
+
const rec = this.#recordByJobId(jobId);
|
|
438
|
+
if (!rec) return;
|
|
439
|
+
rec.status = status;
|
|
440
|
+
this.#liveHandles.delete(rec.subagentId);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Request a graceful safe-boundary pause of a running subagent. */
|
|
444
|
+
pauseSubagent(
|
|
445
|
+
subagentId: string,
|
|
446
|
+
filter?: AsyncJobFilter,
|
|
447
|
+
): { ok: boolean; status?: SubagentLifecycle; reason?: string } {
|
|
448
|
+
const rec = this.getSubagentRecord(subagentId, filter);
|
|
449
|
+
if (!rec) return { ok: false, reason: "not_found" };
|
|
450
|
+
if (rec.status !== "running") return { ok: false, status: rec.status, reason: "not_running" };
|
|
451
|
+
const handle = this.#liveHandles.get(rec.subagentId);
|
|
452
|
+
if (!handle) return { ok: false, status: rec.status, reason: "no_live_handle" };
|
|
453
|
+
handle.requestPause();
|
|
454
|
+
return { ok: true, status: rec.status };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Resume a non-running subagent from its sessionFile, optionally injecting a message first. */
|
|
458
|
+
resumeSubagent(
|
|
459
|
+
subagentId: string,
|
|
460
|
+
filter?: AsyncJobFilter,
|
|
461
|
+
message?: string,
|
|
462
|
+
): { ok: boolean; status?: SubagentLifecycle; jobId?: string; queued?: boolean; reason?: string } {
|
|
463
|
+
const rec = this.getSubagentRecord(subagentId, filter);
|
|
464
|
+
if (!rec) return { ok: false, reason: "not_found" };
|
|
465
|
+
if (rec.status === "running") return { ok: false, status: "running", reason: "already_running" };
|
|
466
|
+
if (rec.status === "queued") {
|
|
467
|
+
if (message !== undefined && rec.queued) {
|
|
468
|
+
rec.queued.message = message;
|
|
469
|
+
const queued = this.#resumeQueue.find(entry => entry.subagentId === rec.subagentId);
|
|
470
|
+
if (queued) queued.message = message;
|
|
471
|
+
return { ok: true, queued: true, status: "queued" };
|
|
472
|
+
}
|
|
473
|
+
return { ok: false, status: "queued", reason: "already_queued" };
|
|
474
|
+
}
|
|
475
|
+
if (!rec.resumable || !rec.sessionFile) return { ok: false, reason: "context_unavailable" };
|
|
476
|
+
if (!this.#resumeRunner) return { ok: false, reason: "no_runner" };
|
|
477
|
+
if (this.getRunningJobs().length >= this.#maxRunningJobs) {
|
|
478
|
+
const seq = ++this.#resumeSeq;
|
|
479
|
+
rec.status = "queued";
|
|
480
|
+
rec.queued = { ownerId: rec.ownerId, seq, message, createdAt: Date.now() };
|
|
481
|
+
this.#resumeQueue.push({
|
|
482
|
+
subagentId: rec.subagentId,
|
|
483
|
+
ownerId: rec.ownerId,
|
|
484
|
+
seq,
|
|
485
|
+
message,
|
|
486
|
+
createdAt: rec.queued.createdAt,
|
|
487
|
+
});
|
|
488
|
+
return { ok: true, queued: true, status: "queued" };
|
|
489
|
+
}
|
|
490
|
+
return this.#startResume(rec, message);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
#startResume(
|
|
494
|
+
rec: SubagentRecord,
|
|
495
|
+
message?: string,
|
|
496
|
+
): { ok: boolean; status?: SubagentLifecycle; jobId?: string; reason?: string } {
|
|
497
|
+
const prevJobId = rec.currentJobId;
|
|
498
|
+
const newJobId = this.#resumeRunner?.(rec.subagentId, message, this.#resumeDescriptors.get(rec.subagentId));
|
|
499
|
+
if (!newJobId) return { ok: false, reason: "resume_failed" };
|
|
500
|
+
if (prevJobId && prevJobId !== newJobId) rec.historicalJobIds.push(prevJobId);
|
|
501
|
+
rec.currentJobId = newJobId;
|
|
502
|
+
rec.status = this.#jobs.get(newJobId)?.status ?? "running";
|
|
503
|
+
rec.queued = undefined;
|
|
504
|
+
return { ok: true, status: rec.status, jobId: newJobId };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Drain queued resumes (FIFO by seq) while concurrency slots are available. */
|
|
508
|
+
#drainResumeQueue(): void {
|
|
509
|
+
if (this.#resumeQueue.length === 0) return;
|
|
510
|
+
this.#resumeQueue.sort((a, b) => a.seq - b.seq);
|
|
511
|
+
while (this.#resumeQueue.length > 0 && this.getRunningJobs().length < this.#maxRunningJobs) {
|
|
512
|
+
const entry = this.#resumeQueue.shift();
|
|
513
|
+
if (!entry) return;
|
|
514
|
+
const rec = this.#subagentRecords.get(entry.subagentId);
|
|
515
|
+
if (rec?.status !== "queued") continue;
|
|
516
|
+
this.#startResume(rec, entry.message);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Cancel a subagent by stable id across running/paused/queued states (keeps the session file). */
|
|
521
|
+
cancelSubagent(subagentId: string, filter?: AsyncJobFilter): boolean {
|
|
522
|
+
const rec = this.getSubagentRecord(subagentId, filter);
|
|
523
|
+
if (!rec) return false;
|
|
524
|
+
if (rec.status === "running" && rec.currentJobId) return this.cancel(rec.currentJobId, filter);
|
|
525
|
+
if (rec.status === "paused") {
|
|
526
|
+
if (rec.currentJobId) {
|
|
527
|
+
const job = this.#jobs.get(rec.currentJobId);
|
|
528
|
+
if (job && job.status === "paused") {
|
|
529
|
+
job.status = "cancelled";
|
|
530
|
+
this.#scheduleEviction(rec.currentJobId);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
rec.status = "cancelled";
|
|
534
|
+
this.#liveHandles.delete(rec.subagentId);
|
|
535
|
+
this.#drainResumeQueue();
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
if (rec.status === "queued") {
|
|
539
|
+
const idx = this.#resumeQueue.findIndex(e => e.subagentId === rec.subagentId);
|
|
540
|
+
if (idx !== -1) this.#resumeQueue.splice(idx, 1);
|
|
541
|
+
rec.status = "cancelled";
|
|
542
|
+
rec.queued = undefined;
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#purgeOwnerSubagentState(ownerId?: string): void {
|
|
549
|
+
for (let i = this.#resumeQueue.length - 1; i >= 0; i--) {
|
|
550
|
+
if (!ownerId || this.#resumeQueue[i].ownerId === ownerId) this.#resumeQueue.splice(i, 1);
|
|
551
|
+
}
|
|
552
|
+
for (const [sid, rec] of this.#subagentRecords) {
|
|
553
|
+
if (!ownerId || rec.ownerId === ownerId) {
|
|
554
|
+
this.#liveHandles.delete(sid);
|
|
555
|
+
this.#resumeDescriptors.delete(sid);
|
|
556
|
+
this.#subagentRecords.delete(sid);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
221
561
|
getJob(id: string): AsyncJob | undefined {
|
|
222
562
|
return this.#jobs.get(id);
|
|
223
563
|
}
|
|
@@ -237,6 +577,170 @@ export class AsyncJobManager {
|
|
|
237
577
|
return this.#filterJobs(this.#jobs.values(), filter);
|
|
238
578
|
}
|
|
239
579
|
|
|
580
|
+
/**
|
|
581
|
+
* Append a sanitized process-stream chunk for a background job. Called from
|
|
582
|
+
* the unthrottled bash-executor capture hook (`onRawChunk`) so monitor sees
|
|
583
|
+
* every chunk even when preview/progress callbacks are throttled.
|
|
584
|
+
*
|
|
585
|
+
* Offsets are in UTF-8 bytes. Storing chunk metadata avoids unsafe byte
|
|
586
|
+
* slicing across multibyte characters at read time. The retention window is
|
|
587
|
+
* a per-job rolling cap (`DEFAULT_JOB_OUTPUT_RETENTION_BYTES`); when it
|
|
588
|
+
* overflows, oldest whole chunks are evicted and `startOffset` advances —
|
|
589
|
+
* subsequent reads from a stale offset get `truncated: true`.
|
|
590
|
+
*/
|
|
591
|
+
appendOutput(jobId: string, chunk: string): void {
|
|
592
|
+
if (this.#disposed) return;
|
|
593
|
+
if (!chunk) return;
|
|
594
|
+
if (!this.#jobs.has(jobId)) return;
|
|
595
|
+
|
|
596
|
+
const state = this.#outputState.get(jobId) ?? {
|
|
597
|
+
chunks: [],
|
|
598
|
+
startOffset: 0,
|
|
599
|
+
nextOffset: 0,
|
|
600
|
+
retainedBytes: 0,
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const byteLength = Buffer.byteLength(chunk, "utf8");
|
|
604
|
+
if (byteLength === 0) return;
|
|
605
|
+
|
|
606
|
+
const startByte = state.nextOffset;
|
|
607
|
+
const endByte = startByte + byteLength;
|
|
608
|
+
state.chunks.push({ startByte, endByte, text: chunk });
|
|
609
|
+
state.retainedBytes += byteLength;
|
|
610
|
+
state.nextOffset = endByte;
|
|
611
|
+
|
|
612
|
+
while (state.retainedBytes > this.#outputRetentionBytes && state.chunks.length > 0) {
|
|
613
|
+
const dropped = state.chunks.shift();
|
|
614
|
+
if (!dropped) break;
|
|
615
|
+
const droppedBytes = dropped.endByte - dropped.startByte;
|
|
616
|
+
state.retainedBytes -= droppedBytes;
|
|
617
|
+
state.startOffset = dropped.endByte;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
this.#outputState.set(jobId, state);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Read fresh process-stream output for a job since `offset` (in UTF-8
|
|
625
|
+
* bytes). Returns `undefined` when the job does not exist or when an
|
|
626
|
+
* `ownerId` filter is set and the job belongs to a different owner — this
|
|
627
|
+
* mirrors the manager-level "not found" pattern used by `cancel`.
|
|
628
|
+
*
|
|
629
|
+
* - `offset < startOffset` returns the retained tail with `truncated: true`.
|
|
630
|
+
* - `offset > nextOffset` clamps to `nextOffset` and returns an empty text
|
|
631
|
+
* slice with `truncated: false`.
|
|
632
|
+
* - Assembled text slices the leading retained chunk at a UTF-8 codepoint
|
|
633
|
+
* boundary when needed, so multibyte characters cannot be split.
|
|
634
|
+
*/
|
|
635
|
+
readOutputSince(jobId: string, offset: number, filter?: AsyncJobFilter): AsyncJobOutputSlice | undefined {
|
|
636
|
+
const job = this.#jobs.get(jobId);
|
|
637
|
+
if (!job) return undefined;
|
|
638
|
+
if (filter?.ownerId && job.ownerId !== filter.ownerId) return undefined;
|
|
639
|
+
|
|
640
|
+
const state = this.#outputState.get(jobId);
|
|
641
|
+
if (!state) {
|
|
642
|
+
return {
|
|
643
|
+
jobId,
|
|
644
|
+
status: job.status,
|
|
645
|
+
text: "",
|
|
646
|
+
startOffset: 0,
|
|
647
|
+
nextOffset: 0,
|
|
648
|
+
truncated: false,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const requestedOffset = Math.max(0, Math.floor(offset));
|
|
653
|
+
if (requestedOffset >= state.nextOffset) {
|
|
654
|
+
return {
|
|
655
|
+
jobId,
|
|
656
|
+
status: job.status,
|
|
657
|
+
text: "",
|
|
658
|
+
startOffset: state.startOffset,
|
|
659
|
+
nextOffset: state.nextOffset,
|
|
660
|
+
truncated: false,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const truncated = requestedOffset < state.startOffset;
|
|
665
|
+
const effectiveOffset = truncated ? state.startOffset : requestedOffset;
|
|
666
|
+
const parts: string[] = [];
|
|
667
|
+
for (const chunk of state.chunks) {
|
|
668
|
+
if (chunk.endByte <= effectiveOffset) continue;
|
|
669
|
+
if (effectiveOffset > chunk.startByte) {
|
|
670
|
+
parts.push(sliceTextFromUtf8ByteOffset(chunk.text, effectiveOffset - chunk.startByte));
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
parts.push(chunk.text);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
jobId,
|
|
678
|
+
status: job.status,
|
|
679
|
+
text: parts.join(""),
|
|
680
|
+
startOffset: state.startOffset,
|
|
681
|
+
nextOffset: state.nextOffset,
|
|
682
|
+
truncated,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Register an owner-scoped cleanup callback. Returns an unregister function.
|
|
688
|
+
*
|
|
689
|
+
* Used by Cron* tools to clear session-scoped timers when the owning agent
|
|
690
|
+
* is torn down. Invoked by `runOwnerCleanups({ ownerId })` before
|
|
691
|
+
* `cancelAll({ ownerId })` so timers cannot register new jobs during
|
|
692
|
+
* teardown.
|
|
693
|
+
*/
|
|
694
|
+
registerOwnerCleanup(ownerId: string, cleanup: () => void): () => void {
|
|
695
|
+
if (!ownerId) {
|
|
696
|
+
throw new Error("registerOwnerCleanup requires a non-empty ownerId");
|
|
697
|
+
}
|
|
698
|
+
let bag = this.#ownerCleanups.get(ownerId);
|
|
699
|
+
if (!bag) {
|
|
700
|
+
bag = new Set();
|
|
701
|
+
this.#ownerCleanups.set(ownerId, bag);
|
|
702
|
+
}
|
|
703
|
+
bag.add(cleanup);
|
|
704
|
+
return () => {
|
|
705
|
+
const current = this.#ownerCleanups.get(ownerId);
|
|
706
|
+
if (!current) return;
|
|
707
|
+
current.delete(cleanup);
|
|
708
|
+
if (current.size === 0) this.#ownerCleanups.delete(ownerId);
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Run and clear every registered cleanup for the given filter. Idempotent
|
|
714
|
+
* and error-isolated: a throwing cleanup does not prevent siblings from
|
|
715
|
+
* running and never escalates to the caller.
|
|
716
|
+
*/
|
|
717
|
+
runOwnerCleanups(filter?: AsyncJobFilter): void {
|
|
718
|
+
const ownerId = filter?.ownerId;
|
|
719
|
+
const targets: Array<[string, Set<() => void>]> = [];
|
|
720
|
+
if (ownerId) {
|
|
721
|
+
const bag = this.#ownerCleanups.get(ownerId);
|
|
722
|
+
if (bag) targets.push([ownerId, bag]);
|
|
723
|
+
} else {
|
|
724
|
+
for (const entry of this.#ownerCleanups.entries()) targets.push(entry);
|
|
725
|
+
}
|
|
726
|
+
for (const [id, bag] of targets) {
|
|
727
|
+
const callbacks = Array.from(bag);
|
|
728
|
+
bag.clear();
|
|
729
|
+
this.#ownerCleanups.delete(id);
|
|
730
|
+
for (const cleanup of callbacks) {
|
|
731
|
+
try {
|
|
732
|
+
cleanup();
|
|
733
|
+
} catch (error) {
|
|
734
|
+
logger.warn("Async job owner cleanup failed", {
|
|
735
|
+
ownerId: id,
|
|
736
|
+
error: error instanceof Error ? error.message : String(error),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
this.#purgeOwnerSubagentState(ownerId);
|
|
742
|
+
}
|
|
743
|
+
|
|
240
744
|
getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState {
|
|
241
745
|
const deliveries = this.#filterDeliveries(filter);
|
|
242
746
|
const inFlightDeliveries = this.#filterInFlightDeliveries(filter);
|
|
@@ -357,6 +861,10 @@ export class AsyncJobManager {
|
|
|
357
861
|
async dispose(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
358
862
|
this.#disposed = true;
|
|
359
863
|
this.#clearEvictionTimers();
|
|
864
|
+
// Run-and-clear any remaining owner cleanups before tearing down jobs so
|
|
865
|
+
// late-arriving timers cannot register fresh work against a disposed
|
|
866
|
+
// manager. Errors in cleanup callbacks are logged but never escalated.
|
|
867
|
+
this.runOwnerCleanups();
|
|
360
868
|
this.cancelAll();
|
|
361
869
|
await this.waitForAll();
|
|
362
870
|
const drained = await this.drainDeliveries({ timeoutMs: options?.timeoutMs ?? 3_000 });
|
|
@@ -366,6 +874,12 @@ export class AsyncJobManager {
|
|
|
366
874
|
this.#inFlightDeliveries.length = 0;
|
|
367
875
|
this.#suppressedDeliveries.clear();
|
|
368
876
|
this.#watchedJobs.clear();
|
|
877
|
+
this.#outputState.clear();
|
|
878
|
+
this.#ownerCleanups.clear();
|
|
879
|
+
this.#subagentRecords.clear();
|
|
880
|
+
this.#liveHandles.clear();
|
|
881
|
+
this.#resumeDescriptors.clear();
|
|
882
|
+
this.#resumeQueue.length = 0;
|
|
369
883
|
return drained;
|
|
370
884
|
}
|
|
371
885
|
|
|
@@ -399,6 +913,7 @@ export class AsyncJobManager {
|
|
|
399
913
|
this.#jobs.delete(jobId);
|
|
400
914
|
this.#suppressedDeliveries.delete(jobId);
|
|
401
915
|
this.#watchedJobs.delete(jobId);
|
|
916
|
+
this.#outputState.delete(jobId);
|
|
402
917
|
return;
|
|
403
918
|
}
|
|
404
919
|
const existing = this.#evictionTimers.get(jobId);
|
|
@@ -410,6 +925,7 @@ export class AsyncJobManager {
|
|
|
410
925
|
this.#jobs.delete(jobId);
|
|
411
926
|
this.#suppressedDeliveries.delete(jobId);
|
|
412
927
|
this.#watchedJobs.delete(jobId);
|
|
928
|
+
this.#outputState.delete(jobId);
|
|
413
929
|
}, this.#retentionMs);
|
|
414
930
|
timer.unref();
|
|
415
931
|
this.#evictionTimers.set(jobId, timer);
|
package/src/cli/agents-cli.ts
CHANGED
|
@@ -64,6 +64,9 @@ function toFrontmatter(agent: AgentDefinition): Record<string, unknown> {
|
|
|
64
64
|
if (agent.thinkingLevel) frontmatter.thinkingLevel = agent.thinkingLevel;
|
|
65
65
|
if (agent.output !== undefined) frontmatter.output = agent.output;
|
|
66
66
|
if (agent.blocking) frontmatter.blocking = true;
|
|
67
|
+
if (agent.bashAllowedPrefixes && agent.bashAllowedPrefixes.length > 0) {
|
|
68
|
+
frontmatter.bashAllowedPrefixes = agent.bashAllowedPrefixes;
|
|
69
|
+
}
|
|
67
70
|
|
|
68
71
|
return frontmatter;
|
|
69
72
|
}
|