@bastani/atomic 0.9.4-alpha.10 → 0.9.4-alpha.11

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/builtin/cursor/package.json +2 -2
  3. package/dist/builtin/intercom/package.json +1 -1
  4. package/dist/builtin/mcp/package.json +1 -1
  5. package/dist/builtin/subagents/CHANGELOG.md +7 -0
  6. package/dist/builtin/subagents/package.json +1 -1
  7. package/dist/builtin/subagents/src/slash/slash-commands.ts +2 -2
  8. package/dist/builtin/subagents/src/tui/render-result-compact.ts +3 -2
  9. package/dist/builtin/subagents/src/tui/render-result.ts +3 -3
  10. package/dist/builtin/subagents/src/tui/render-widget-graph.ts +4 -3
  11. package/dist/builtin/subagents/src/tui/render-widget.ts +10 -4
  12. package/dist/builtin/web-access/package.json +1 -1
  13. package/dist/builtin/workflows/CHANGELOG.md +6 -0
  14. package/dist/builtin/workflows/package.json +1 -1
  15. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +4 -0
  16. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -0
  17. package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +12 -1
  18. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +2 -0
  19. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +6 -0
  20. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +4 -0
  21. package/dist/builtin/workflows/src/tui/workflow-attach-pane-types.ts +4 -0
  22. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -9
  23. package/dist/core/agent-session-events.d.ts.map +1 -1
  24. package/dist/core/agent-session-events.js +2 -0
  25. package/dist/core/agent-session-events.js.map +1 -1
  26. package/dist/core/agent-session-methods.d.ts +4 -1
  27. package/dist/core/agent-session-methods.d.ts.map +1 -1
  28. package/dist/core/agent-session-methods.js.map +1 -1
  29. package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
  30. package/dist/core/agent-session-tool-registry.js +4 -0
  31. package/dist/core/agent-session-tool-registry.js.map +1 -1
  32. package/dist/core/agent-session.d.ts +4 -1
  33. package/dist/core/agent-session.d.ts.map +1 -1
  34. package/dist/core/agent-session.js +4 -0
  35. package/dist/core/agent-session.js.map +1 -1
  36. package/dist/core/async/format.d.ts +3 -0
  37. package/dist/core/async/format.d.ts.map +1 -0
  38. package/dist/core/async/format.js +50 -0
  39. package/dist/core/async/format.js.map +1 -0
  40. package/dist/core/async/job-manager.d.ts +39 -0
  41. package/dist/core/async/job-manager.d.ts.map +1 -0
  42. package/dist/core/async/job-manager.js +254 -0
  43. package/dist/core/async/job-manager.js.map +1 -0
  44. package/dist/core/async/session-manager.d.ts +18 -0
  45. package/dist/core/async/session-manager.d.ts.map +1 -0
  46. package/dist/core/async/session-manager.js +61 -0
  47. package/dist/core/async/session-manager.js.map +1 -0
  48. package/dist/core/async/types.d.ts +23 -0
  49. package/dist/core/async/types.d.ts.map +1 -0
  50. package/dist/core/async/types.js +2 -0
  51. package/dist/core/async/types.js.map +1 -0
  52. package/dist/core/tools/bash-async-execution.d.ts +25 -0
  53. package/dist/core/tools/bash-async-execution.d.ts.map +1 -0
  54. package/dist/core/tools/bash-async-execution.js +71 -0
  55. package/dist/core/tools/bash-async-execution.js.map +1 -0
  56. package/dist/core/tools/bash-async-jobs.d.ts +10 -0
  57. package/dist/core/tools/bash-async-jobs.d.ts.map +1 -1
  58. package/dist/core/tools/bash-async-jobs.js +18 -2
  59. package/dist/core/tools/bash-async-jobs.js.map +1 -1
  60. package/dist/core/tools/bash-async-output.d.ts +3 -1
  61. package/dist/core/tools/bash-async-output.d.ts.map +1 -1
  62. package/dist/core/tools/bash-async-output.js +4 -1
  63. package/dist/core/tools/bash-async-output.js.map +1 -1
  64. package/dist/core/tools/bash.d.ts +10 -4
  65. package/dist/core/tools/bash.d.ts.map +1 -1
  66. package/dist/core/tools/bash.js +20 -38
  67. package/dist/core/tools/bash.js.map +1 -1
  68. package/docs/tools.md +4 -2
  69. package/npm-shrinkwrap.json +23 -23
  70. package/package.json +2 -2
@@ -0,0 +1,61 @@
1
+ import { AsyncJobManager } from "./job-manager.js";
2
+ const STREAMING_DELIVERY_POLL_MS = 10;
3
+ function scheduleBoundaryCheck(callback) {
4
+ const timer = setTimeout(callback, STREAMING_DELIVERY_POLL_MS);
5
+ timer.unref?.();
6
+ return timer;
7
+ }
8
+ function waitForStreamingBoundary(session, isStale) {
9
+ return new Promise((resolve) => {
10
+ let timer;
11
+ const settle = (value) => {
12
+ if (timer)
13
+ clearTimeout(timer);
14
+ resolve(value);
15
+ };
16
+ const check = () => {
17
+ timer = undefined;
18
+ if (isStale()) {
19
+ settle("stale");
20
+ return;
21
+ }
22
+ if (session.isStreaming !== true) {
23
+ settle("ready");
24
+ return;
25
+ }
26
+ timer = scheduleBoundaryCheck(check);
27
+ };
28
+ check();
29
+ });
30
+ }
31
+ export function createSessionAsyncDeliveryHandler(session, manager, sessionId) {
32
+ return async (message) => {
33
+ const isStale = () => manager?.disposed === true ||
34
+ manager?.isDeliverySuppressed(message.details.jobId) === true ||
35
+ (sessionId !== undefined && manager?.isSessionDisposed(sessionId) === true);
36
+ if (await waitForStreamingBoundary(session, isStale) === "stale")
37
+ return;
38
+ if (isStale())
39
+ return;
40
+ await session.sendCustomMessage(message, { deliverAs: "followUp", triggerTurn: true });
41
+ };
42
+ }
43
+ export function createSessionAsyncJobManager(session) {
44
+ const existing = AsyncJobManager.instance();
45
+ if (existing)
46
+ return { manager: existing, owns: false, sessionId: existing.registerSession() };
47
+ let manager;
48
+ manager = new AsyncJobManager({
49
+ onJobComplete: (message) => createSessionAsyncDeliveryHandler(session, manager)(message),
50
+ });
51
+ AsyncJobManager.setInstance(manager);
52
+ return { manager, owns: true, sessionId: manager.registerSession() };
53
+ }
54
+ export function disposeSessionAsyncJobManager(manager, sessionId) {
55
+ if (!manager || !sessionId)
56
+ return;
57
+ manager.releaseSession(sessionId);
58
+ if (manager.disposed && AsyncJobManager.instance() === manager)
59
+ AsyncJobManager.setInstance(undefined);
60
+ }
61
+ //# sourceMappingURL=session-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager.js","sourceRoot":"","sources":["../../../src/core/async/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CustomMessage } from \"../messages.js\";\nimport type { SendMessageOptions } from \"../extensions/index.js\";\nimport { AsyncJobManager } from \"./job-manager.js\";\nimport type { AsyncJobDeliveryHandler, AsyncJobDeliveryMessage } from \"./types.js\";\n\nexport interface SessionAsyncJobManagerHandle {\n\tmanager: AsyncJobManager;\n\towns: boolean;\n\tsessionId: symbol;\n}\ninterface AsyncDeliverySession {\n\treadonly isStreaming?: boolean;\n\tsendCustomMessage<T>(\n\t\tmessage: Pick<CustomMessage<T>, \"customType\" | \"content\" | \"display\" | \"details\">,\n\t\toptions?: SendMessageOptions,\n\t): Promise<void>;\n}\n\nconst STREAMING_DELIVERY_POLL_MS = 10;\n\nfunction scheduleBoundaryCheck(callback: () => void): NodeJS.Timeout {\n\tconst timer = setTimeout(callback, STREAMING_DELIVERY_POLL_MS);\n\ttimer.unref?.();\n\treturn timer;\n}\n\nfunction waitForStreamingBoundary(session: AsyncDeliverySession, isStale: () => boolean): Promise<\"ready\" | \"stale\"> {\n\treturn new Promise((resolve) => {\n\t\tlet timer: NodeJS.Timeout | undefined;\n\t\tconst settle = (value: \"ready\" | \"stale\") => {\n\t\t\tif (timer) clearTimeout(timer);\n\t\t\tresolve(value);\n\t\t};\n\t\tconst check = () => {\n\t\t\ttimer = undefined;\n\t\t\tif (isStale()) {\n\t\t\t\tsettle(\"stale\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (session.isStreaming !== true) {\n\t\t\t\tsettle(\"ready\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttimer = scheduleBoundaryCheck(check);\n\t\t};\n\t\tcheck();\n\t});\n}\n\nexport function createSessionAsyncDeliveryHandler(session: AsyncDeliverySession, manager?: AsyncJobManager, sessionId?: symbol): AsyncJobDeliveryHandler {\n\treturn async (message: AsyncJobDeliveryMessage) => {\n\t\tconst isStale = () =>\n\t\t\tmanager?.disposed === true ||\n\t\t\tmanager?.isDeliverySuppressed(message.details.jobId) === true ||\n\t\t\t(sessionId !== undefined && manager?.isSessionDisposed(sessionId) === true);\n\t\tif (await waitForStreamingBoundary(session, isStale) === \"stale\") return;\n\t\tif (isStale()) return;\n\t\tawait session.sendCustomMessage(message, { deliverAs: \"followUp\", triggerTurn: true });\n\t};\n}\n\nexport function createSessionAsyncJobManager(session: AsyncDeliverySession): SessionAsyncJobManagerHandle {\n\tconst existing = AsyncJobManager.instance();\n\tif (existing) return { manager: existing, owns: false, sessionId: existing.registerSession() };\n\tlet manager: AsyncJobManager;\n\tmanager = new AsyncJobManager({\n\t\tonJobComplete: (message) => createSessionAsyncDeliveryHandler(session, manager)(message),\n\t});\n\tAsyncJobManager.setInstance(manager);\n\treturn { manager, owns: true, sessionId: manager.registerSession() };\n}\n\nexport function disposeSessionAsyncJobManager(manager: AsyncJobManager | undefined, sessionId: symbol | undefined): void {\n\tif (!manager || !sessionId) return;\n\tmanager.releaseSession(sessionId);\n\tif (manager.disposed && AsyncJobManager.instance() === manager) AsyncJobManager.setInstance(undefined);\n}\n\n"]}
@@ -0,0 +1,23 @@
1
+ import type { ManagedBashJob } from "../tools/bash-async-jobs.js";
2
+ export type AsyncJobStatus = "running" | "completed" | "failed";
3
+ export interface AsyncJobDeliveryDetails {
4
+ jobId: string;
5
+ type: "bash";
6
+ status: AsyncJobStatus;
7
+ command: string;
8
+ exitCode?: number | null;
9
+ fullOutputPath?: string;
10
+ wallTimeMs?: number;
11
+ }
12
+ export interface AsyncJobDeliveryMessage {
13
+ customType: "async-job-result";
14
+ content: string;
15
+ display: true;
16
+ details: AsyncJobDeliveryDetails;
17
+ }
18
+ export type AsyncJobDeliveryCallback = (message: AsyncJobDeliveryMessage) => void | Promise<void>;
19
+ export type AsyncJobDeliveryHandler = (message: AsyncJobDeliveryMessage) => void | Promise<void>;
20
+ export interface ManagedAsyncBashJob extends ManagedBashJob {
21
+ status: AsyncJobStatus;
22
+ }
23
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/async/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAElE,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEhE,MAAM,WAAW,uBAAuB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACvC,UAAU,EAAE,kBAAkB,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,uBAAuB,CAAC;CACjC;AAED,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAElG,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,EAAE,uBAAuB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEjG,MAAM,WAAW,mBAAoB,SAAQ,cAAc;IAC1D,MAAM,EAAE,cAAc,CAAC;CACvB","sourcesContent":["import type { ManagedBashJob } from \"../tools/bash-async-jobs.js\";\n\nexport type AsyncJobStatus = \"running\" | \"completed\" | \"failed\";\n\nexport interface AsyncJobDeliveryDetails {\n\tjobId: string;\n\ttype: \"bash\";\n\tstatus: AsyncJobStatus;\n\tcommand: string;\n\texitCode?: number | null;\n\tfullOutputPath?: string;\n\twallTimeMs?: number;\n}\n\nexport interface AsyncJobDeliveryMessage {\n\tcustomType: \"async-job-result\";\n\tcontent: string;\n\tdisplay: true;\n\tdetails: AsyncJobDeliveryDetails;\n}\n\nexport type AsyncJobDeliveryCallback = (message: AsyncJobDeliveryMessage) => void | Promise<void>;\n\nexport type AsyncJobDeliveryHandler = (message: AsyncJobDeliveryMessage) => void | Promise<void>;\n\nexport interface ManagedAsyncBashJob extends ManagedBashJob {\n\tstatus: AsyncJobStatus;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/async/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ManagedBashJob } from \"../tools/bash-async-jobs.js\";\n\nexport type AsyncJobStatus = \"running\" | \"completed\" | \"failed\";\n\nexport interface AsyncJobDeliveryDetails {\n\tjobId: string;\n\ttype: \"bash\";\n\tstatus: AsyncJobStatus;\n\tcommand: string;\n\texitCode?: number | null;\n\tfullOutputPath?: string;\n\twallTimeMs?: number;\n}\n\nexport interface AsyncJobDeliveryMessage {\n\tcustomType: \"async-job-result\";\n\tcontent: string;\n\tdisplay: true;\n\tdetails: AsyncJobDeliveryDetails;\n}\n\nexport type AsyncJobDeliveryCallback = (message: AsyncJobDeliveryMessage) => void | Promise<void>;\n\nexport type AsyncJobDeliveryHandler = (message: AsyncJobDeliveryMessage) => void | Promise<void>;\n\nexport interface ManagedAsyncBashJob extends ManagedBashJob {\n\tstatus: AsyncJobStatus;\n}\n"]}
@@ -0,0 +1,25 @@
1
+ import type { AsyncJobManager } from "../async/job-manager.js";
2
+ import type { AsyncJobDeliveryHandler } from "../async/types.js";
3
+ import type { BashOperations, BashToolDetails } from "./bash.js";
4
+ interface StartAsyncBashCommandOptions {
5
+ command: string;
6
+ cwd: string;
7
+ env: NodeJS.ProcessEnv;
8
+ pty?: boolean;
9
+ timeoutSeconds: number;
10
+ requestedTimeoutSeconds?: number;
11
+ signal?: AbortSignal;
12
+ operations: BashOperations;
13
+ manager?: AsyncJobManager;
14
+ deliveryHandler?: AsyncJobDeliveryHandler;
15
+ sessionId?: symbol;
16
+ }
17
+ export declare function startAsyncBashCommand(options: StartAsyncBashCommandOptions): Promise<{
18
+ content: Array<{
19
+ type: "text";
20
+ text: string;
21
+ }>;
22
+ details: BashToolDetails;
23
+ }>;
24
+ export {};
25
+ //# sourceMappingURL=bash-async-execution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bash-async-execution.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash-async-execution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAIjE,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjE,UAAU,4BAA4B;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,EAAE,cAAc,CAAC;IAC3B,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC;IAC3F,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,EAAE,eAAe,CAAC;CACzB,CAAC,CA2DD","sourcesContent":["import type { AsyncJobManager } from \"../async/job-manager.js\";\nimport type { AsyncJobDeliveryHandler } from \"../async/types.js\";\nimport { invalidateNativeSearchCache } from \"./search-native.js\";\nimport { createAsyncOutputAppender } from \"./bash-async-output.js\";\nimport { createManagedBashJob, discardManagedBashJob, formatAsyncJobError } from \"./bash-async-jobs.js\";\nimport type { BashOperations, BashToolDetails } from \"./bash.js\";\n\ninterface StartAsyncBashCommandOptions {\n\tcommand: string;\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n\tpty?: boolean;\n\ttimeoutSeconds: number;\n\trequestedTimeoutSeconds?: number;\n\tsignal?: AbortSignal;\n\toperations: BashOperations;\n\tmanager?: AsyncJobManager;\n\tdeliveryHandler?: AsyncJobDeliveryHandler;\n\tsessionId?: symbol;\n}\n\nexport async function startAsyncBashCommand(options: StartAsyncBashCommandOptions): Promise<{\n\tcontent: Array<{ type: \"text\"; text: string }>;\n\tdetails: BashToolDetails;\n}> {\n\tif (options.manager?.atCapacity) throw new Error(\"Background job limit reached. Wait for running jobs to finish or cancel one.\");\n\tconst job = createManagedBashJob(options.command, options.cwd, options.timeoutSeconds, options.requestedTimeoutSeconds);\n\ttry {\n\t\toptions.manager?.registerBashJob(job, options.deliveryHandler, options.sessionId);\n\t} catch (registerError) {\n\t\t// The job was inserted into the managed map but never started; drop it so\n\t\t// it cannot linger as a permanently-\"running\" zombie entry (see\n\t\t// discardManagedBashJob). Registration failures (disposed manager/session,\n\t\t// capacity race) still surface to the caller as tool errors.\n\t\tdiscardManagedBashJob(job.jobId);\n\t\tthrow registerError;\n\t}\n\tconst appendAsyncOutput = createAsyncOutputAppender(job, { persistAfterBytes: 12_000 });\n\tconst onParentAbort = () => {\n\t\toptions.manager?.acknowledgeDeliveries([job.jobId]);\n\t\tjob.abortController?.abort();\n\t};\n\tif (options.signal?.aborted) onParentAbort();\n\telse options.signal?.addEventListener(\"abort\", onParentAbort, { once: true });\n\tvoid (async () => {\n\t\tlet error: Error | string | undefined;\n\t\tlet exitCode: number | null | undefined;\n\t\ttry {\n\t\t\texitCode = (await options.operations.exec(options.command, options.cwd, {\n\t\t\t\tonData: appendAsyncOutput.append,\n\t\t\t\ttimeout: options.timeoutSeconds,\n\t\t\t\tenv: options.env,\n\t\t\t\tpty: options.pty,\n\t\t\t\tsignal: job.abortController?.signal,\n\t\t\t})).exitCode;\n\t\t} catch (execError) {\n\t\t\terror = execError instanceof Error ? execError : String(execError);\n\t\t}\n\t\ttry {\n\t\t\tawait appendAsyncOutput.close();\n\t\t} catch (closeError) {\n\t\t\terror ??= closeError instanceof Error ? closeError : String(closeError);\n\t\t}\n\t\tif (error !== undefined) {\n\t\t\tjob.status = \"failed\";\n\t\t\tjob.error = job.abortController?.signal.aborted ? \"aborted\" : formatAsyncJobError(error);\n\t\t} else {\n\t\t\tjob.exitCode = exitCode;\n\t\t\tjob.status = exitCode && exitCode !== 0 ? \"failed\" : \"completed\";\n\t\t}\n\t\tjob.endedAt = Date.now();\n\t\tinvalidateNativeSearchCache();\n\t\toptions.manager?.completeBashJob(job);\n\t\toptions.signal?.removeEventListener(\"abort\", onParentAbort);\n\t})();\n\treturn {\n\t\tcontent: [{ type: \"text\", text: `Started async bash command ${job.jobId}: ${options.command}\\nPoll with bash({ command: \"__atomic_bash_job ${job.jobId}\" }); cancel with bash({ command: \"__atomic_bash_job_cancel ${job.jobId}\" })` }],\n\t\tdetails: {\n\t\t\tasync: { jobId: job.jobId, type: \"bash\", state: \"running\", command: options.command, status: \"running\" },\n\t\t\ttimeoutSeconds: options.timeoutSeconds,\n\t\t\t...(options.requestedTimeoutSeconds !== undefined ? { requestedTimeoutSeconds: options.requestedTimeoutSeconds } : {}),\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,71 @@
1
+ import { invalidateNativeSearchCache } from "./search-native.js";
2
+ import { createAsyncOutputAppender } from "./bash-async-output.js";
3
+ import { createManagedBashJob, discardManagedBashJob, formatAsyncJobError } from "./bash-async-jobs.js";
4
+ export async function startAsyncBashCommand(options) {
5
+ if (options.manager?.atCapacity)
6
+ throw new Error("Background job limit reached. Wait for running jobs to finish or cancel one.");
7
+ const job = createManagedBashJob(options.command, options.cwd, options.timeoutSeconds, options.requestedTimeoutSeconds);
8
+ try {
9
+ options.manager?.registerBashJob(job, options.deliveryHandler, options.sessionId);
10
+ }
11
+ catch (registerError) {
12
+ // The job was inserted into the managed map but never started; drop it so
13
+ // it cannot linger as a permanently-"running" zombie entry (see
14
+ // discardManagedBashJob). Registration failures (disposed manager/session,
15
+ // capacity race) still surface to the caller as tool errors.
16
+ discardManagedBashJob(job.jobId);
17
+ throw registerError;
18
+ }
19
+ const appendAsyncOutput = createAsyncOutputAppender(job, { persistAfterBytes: 12_000 });
20
+ const onParentAbort = () => {
21
+ options.manager?.acknowledgeDeliveries([job.jobId]);
22
+ job.abortController?.abort();
23
+ };
24
+ if (options.signal?.aborted)
25
+ onParentAbort();
26
+ else
27
+ options.signal?.addEventListener("abort", onParentAbort, { once: true });
28
+ void (async () => {
29
+ let error;
30
+ let exitCode;
31
+ try {
32
+ exitCode = (await options.operations.exec(options.command, options.cwd, {
33
+ onData: appendAsyncOutput.append,
34
+ timeout: options.timeoutSeconds,
35
+ env: options.env,
36
+ pty: options.pty,
37
+ signal: job.abortController?.signal,
38
+ })).exitCode;
39
+ }
40
+ catch (execError) {
41
+ error = execError instanceof Error ? execError : String(execError);
42
+ }
43
+ try {
44
+ await appendAsyncOutput.close();
45
+ }
46
+ catch (closeError) {
47
+ error ??= closeError instanceof Error ? closeError : String(closeError);
48
+ }
49
+ if (error !== undefined) {
50
+ job.status = "failed";
51
+ job.error = job.abortController?.signal.aborted ? "aborted" : formatAsyncJobError(error);
52
+ }
53
+ else {
54
+ job.exitCode = exitCode;
55
+ job.status = exitCode && exitCode !== 0 ? "failed" : "completed";
56
+ }
57
+ job.endedAt = Date.now();
58
+ invalidateNativeSearchCache();
59
+ options.manager?.completeBashJob(job);
60
+ options.signal?.removeEventListener("abort", onParentAbort);
61
+ })();
62
+ return {
63
+ content: [{ type: "text", text: `Started async bash command ${job.jobId}: ${options.command}\nPoll with bash({ command: "__atomic_bash_job ${job.jobId}" }); cancel with bash({ command: "__atomic_bash_job_cancel ${job.jobId}" })` }],
64
+ details: {
65
+ async: { jobId: job.jobId, type: "bash", state: "running", command: options.command, status: "running" },
66
+ timeoutSeconds: options.timeoutSeconds,
67
+ ...(options.requestedTimeoutSeconds !== undefined ? { requestedTimeoutSeconds: options.requestedTimeoutSeconds } : {}),
68
+ },
69
+ };
70
+ }
71
+ //# sourceMappingURL=bash-async-execution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bash-async-execution.js","sourceRoot":"","sources":["../../../src/core/tools/bash-async-execution.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AsyncJobManager } from \"../async/job-manager.js\";\nimport type { AsyncJobDeliveryHandler } from \"../async/types.js\";\nimport { invalidateNativeSearchCache } from \"./search-native.js\";\nimport { createAsyncOutputAppender } from \"./bash-async-output.js\";\nimport { createManagedBashJob, discardManagedBashJob, formatAsyncJobError } from \"./bash-async-jobs.js\";\nimport type { BashOperations, BashToolDetails } from \"./bash.js\";\n\ninterface StartAsyncBashCommandOptions {\n\tcommand: string;\n\tcwd: string;\n\tenv: NodeJS.ProcessEnv;\n\tpty?: boolean;\n\ttimeoutSeconds: number;\n\trequestedTimeoutSeconds?: number;\n\tsignal?: AbortSignal;\n\toperations: BashOperations;\n\tmanager?: AsyncJobManager;\n\tdeliveryHandler?: AsyncJobDeliveryHandler;\n\tsessionId?: symbol;\n}\n\nexport async function startAsyncBashCommand(options: StartAsyncBashCommandOptions): Promise<{\n\tcontent: Array<{ type: \"text\"; text: string }>;\n\tdetails: BashToolDetails;\n}> {\n\tif (options.manager?.atCapacity) throw new Error(\"Background job limit reached. Wait for running jobs to finish or cancel one.\");\n\tconst job = createManagedBashJob(options.command, options.cwd, options.timeoutSeconds, options.requestedTimeoutSeconds);\n\ttry {\n\t\toptions.manager?.registerBashJob(job, options.deliveryHandler, options.sessionId);\n\t} catch (registerError) {\n\t\t// The job was inserted into the managed map but never started; drop it so\n\t\t// it cannot linger as a permanently-\"running\" zombie entry (see\n\t\t// discardManagedBashJob). Registration failures (disposed manager/session,\n\t\t// capacity race) still surface to the caller as tool errors.\n\t\tdiscardManagedBashJob(job.jobId);\n\t\tthrow registerError;\n\t}\n\tconst appendAsyncOutput = createAsyncOutputAppender(job, { persistAfterBytes: 12_000 });\n\tconst onParentAbort = () => {\n\t\toptions.manager?.acknowledgeDeliveries([job.jobId]);\n\t\tjob.abortController?.abort();\n\t};\n\tif (options.signal?.aborted) onParentAbort();\n\telse options.signal?.addEventListener(\"abort\", onParentAbort, { once: true });\n\tvoid (async () => {\n\t\tlet error: Error | string | undefined;\n\t\tlet exitCode: number | null | undefined;\n\t\ttry {\n\t\t\texitCode = (await options.operations.exec(options.command, options.cwd, {\n\t\t\t\tonData: appendAsyncOutput.append,\n\t\t\t\ttimeout: options.timeoutSeconds,\n\t\t\t\tenv: options.env,\n\t\t\t\tpty: options.pty,\n\t\t\t\tsignal: job.abortController?.signal,\n\t\t\t})).exitCode;\n\t\t} catch (execError) {\n\t\t\terror = execError instanceof Error ? execError : String(execError);\n\t\t}\n\t\ttry {\n\t\t\tawait appendAsyncOutput.close();\n\t\t} catch (closeError) {\n\t\t\terror ??= closeError instanceof Error ? closeError : String(closeError);\n\t\t}\n\t\tif (error !== undefined) {\n\t\t\tjob.status = \"failed\";\n\t\t\tjob.error = job.abortController?.signal.aborted ? \"aborted\" : formatAsyncJobError(error);\n\t\t} else {\n\t\t\tjob.exitCode = exitCode;\n\t\t\tjob.status = exitCode && exitCode !== 0 ? \"failed\" : \"completed\";\n\t\t}\n\t\tjob.endedAt = Date.now();\n\t\tinvalidateNativeSearchCache();\n\t\toptions.manager?.completeBashJob(job);\n\t\toptions.signal?.removeEventListener(\"abort\", onParentAbort);\n\t})();\n\treturn {\n\t\tcontent: [{ type: \"text\", text: `Started async bash command ${job.jobId}: ${options.command}\\nPoll with bash({ command: \"__atomic_bash_job ${job.jobId}\" }); cancel with bash({ command: \"__atomic_bash_job_cancel ${job.jobId}\" })` }],\n\t\tdetails: {\n\t\t\tasync: { jobId: job.jobId, type: \"bash\", state: \"running\", command: options.command, status: \"running\" },\n\t\t\ttimeoutSeconds: options.timeoutSeconds,\n\t\t\t...(options.requestedTimeoutSeconds !== undefined ? { requestedTimeoutSeconds: options.requestedTimeoutSeconds } : {}),\n\t\t},\n\t};\n}\n"]}
@@ -13,8 +13,18 @@ export interface ManagedBashJob {
13
13
  requestedTimeoutSeconds?: number;
14
14
  abortController?: AbortController;
15
15
  }
16
+ export declare const MAX_MANAGED_BASH_JOBS = 100;
17
+ export declare const COMPLETED_JOB_TTL_MS: number;
16
18
  export declare function formatAsyncJobError(error: unknown): string;
17
19
  export declare function createManagedBashJob(command: string, cwd: string, timeoutSeconds?: number, requestedTimeoutSeconds?: number): ManagedBashJob;
20
+ /**
21
+ * Remove a managed job that never actually started executing (for example when
22
+ * async-manager registration fails after the map insert). Without this, the
23
+ * entry would stay "running" forever: TTL cleanup only evicts settled jobs, so
24
+ * the zombie would linger until the max-jobs overflow forcibly removed it.
25
+ */
26
+ export declare function discardManagedBashJob(jobId: string): void;
27
+ export declare function listManagedBashJobIds(): string[];
18
28
  export declare function getManagedBashJob(jobId: string): ManagedBashJob | undefined;
19
29
  export declare function abortManagedBashJob(jobId: string): ManagedBashJob | undefined;
20
30
  //# sourceMappingURL=bash-async-jobs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bash-async-jobs.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash-async-jobs.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,eAAe,CAAC,EAAE,eAAe,CAAC;CAClC;AAKD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAI1D;AAgBD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,uBAAuB,CAAC,EAAE,MAAM,GAAG,cAAc,CAO5I;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAM3E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAK7E","sourcesContent":["import { rmSync } from \"node:fs\";\n\nexport interface ManagedBashJob {\n\tjobId: string;\n\tcommand: string;\n\tcwd: string;\n\tstatus: \"running\" | \"completed\" | \"failed\";\n\toutput: string;\n\tfullOutputPath?: string;\n\texitCode?: number | null;\n\terror?: string;\n\tstartedAt: number;\n\tendedAt?: number;\n\ttimeoutSeconds?: number;\n\trequestedTimeoutSeconds?: number;\n\tabortController?: AbortController;\n}\n\nconst MAX_MANAGED_BASH_JOBS = 100;\nconst COMPLETED_JOB_TTL_MS = 30 * 60 * 1000;\nconst managedBashJobs = new Map<string, ManagedBashJob>();\nexport function formatAsyncJobError(error: unknown): string {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (message.startsWith(\"timeout:\")) return `Command timed out after ${message.slice(\"timeout:\".length)} seconds`;\n\treturn message;\n}\nfunction deleteJobOutput(job: ManagedBashJob): void {\n\tif (!job.fullOutputPath) return;\n\ttry { rmSync(job.fullOutputPath, { force: true }); } catch { /* best-effort temp cleanup */ }\n}\n\nfunction cleanupManagedBashJobs(now = Date.now()): void {\n\tfor (const [jobId, job] of managedBashJobs) if (job.status !== \"running\" && job.endedAt !== undefined && now - job.endedAt > COMPLETED_JOB_TTL_MS) { deleteJobOutput(job); managedBashJobs.delete(jobId); }\n\twhile (managedBashJobs.size > MAX_MANAGED_BASH_JOBS) {\n\t\tconst oldest = [...managedBashJobs.values()].sort((a, b) => (a.endedAt ?? a.startedAt) - (b.endedAt ?? b.startedAt))[0];\n\t\tif (!oldest) break;\n\t\tif (oldest.status === \"running\") oldest.abortController?.abort(); else deleteJobOutput(oldest);\n\t\tmanagedBashJobs.delete(oldest.jobId);\n\t}\n}\n\nexport function createManagedBashJob(command: string, cwd: string, timeoutSeconds?: number, requestedTimeoutSeconds?: number): ManagedBashJob {\n\tcleanupManagedBashJobs();\n\tconst jobId = `bash-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n\tconst job: ManagedBashJob = { jobId, command, cwd, status: \"running\", output: \"\", startedAt: Date.now(), timeoutSeconds, requestedTimeoutSeconds, abortController: new AbortController() };\n\tmanagedBashJobs.set(jobId, job);\n\tcleanupManagedBashJobs();\n\treturn job;\n}\n\nexport function getManagedBashJob(jobId: string): ManagedBashJob | undefined {\n\tcleanupManagedBashJobs();\n\tconst job = managedBashJobs.get(jobId);\n\tif (!job) return undefined;\n\tconst { abortController: _abortController, ...snapshot } = job;\n\treturn { ...snapshot };\n}\n\nexport function abortManagedBashJob(jobId: string): ManagedBashJob | undefined {\n\tconst job = managedBashJobs.get(jobId);\n\tif (!job) return undefined;\n\tjob.abortController?.abort();\n\treturn job;\n}\n"]}
1
+ {"version":3,"file":"bash-async-jobs.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash-async-jobs.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,eAAe,CAAC,EAAE,eAAe,CAAC;CAClC;AAED,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,eAAO,MAAM,oBAAoB,QAAiB,CAAC;AAEnD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAI1D;AAgBD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,EAAE,uBAAuB,CAAC,EAAE,MAAM,GAAG,cAAc,CAO5I;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAKzD;AAED,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAEhD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAM3E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAK7E","sourcesContent":["import { rmSync } from \"node:fs\";\n\nexport interface ManagedBashJob {\n\tjobId: string;\n\tcommand: string;\n\tcwd: string;\n\tstatus: \"running\" | \"completed\" | \"failed\";\n\toutput: string;\n\tfullOutputPath?: string;\n\texitCode?: number | null;\n\terror?: string;\n\tstartedAt: number;\n\tendedAt?: number;\n\ttimeoutSeconds?: number;\n\trequestedTimeoutSeconds?: number;\n\tabortController?: AbortController;\n}\n\nexport const MAX_MANAGED_BASH_JOBS = 100;\nexport const COMPLETED_JOB_TTL_MS = 30 * 60 * 1000;\nconst managedBashJobs = new Map<string, ManagedBashJob>();\nexport function formatAsyncJobError(error: unknown): string {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (message.startsWith(\"timeout:\")) return `Command timed out after ${message.slice(\"timeout:\".length)} seconds`;\n\treturn message;\n}\nfunction deleteJobOutput(job: ManagedBashJob): void {\n\tif (!job.fullOutputPath) return;\n\ttry { rmSync(job.fullOutputPath, { force: true }); } catch { /* best-effort temp cleanup */ }\n}\n\nfunction cleanupManagedBashJobs(now = Date.now()): void {\n\tfor (const [jobId, job] of managedBashJobs) if (job.status !== \"running\" && job.endedAt !== undefined && now - job.endedAt > COMPLETED_JOB_TTL_MS) { deleteJobOutput(job); managedBashJobs.delete(jobId); }\n\twhile (managedBashJobs.size > MAX_MANAGED_BASH_JOBS) {\n\t\tconst oldest = [...managedBashJobs.values()].sort((a, b) => (a.endedAt ?? a.startedAt) - (b.endedAt ?? b.startedAt))[0];\n\t\tif (!oldest) break;\n\t\tif (oldest.status === \"running\") oldest.abortController?.abort(); else deleteJobOutput(oldest);\n\t\tmanagedBashJobs.delete(oldest.jobId);\n\t}\n}\n\nexport function createManagedBashJob(command: string, cwd: string, timeoutSeconds?: number, requestedTimeoutSeconds?: number): ManagedBashJob {\n\tcleanupManagedBashJobs();\n\tconst jobId = `bash-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n\tconst job: ManagedBashJob = { jobId, command, cwd, status: \"running\", output: \"\", startedAt: Date.now(), timeoutSeconds, requestedTimeoutSeconds, abortController: new AbortController() };\n\tmanagedBashJobs.set(jobId, job);\n\tcleanupManagedBashJobs();\n\treturn job;\n}\n\n/**\n * Remove a managed job that never actually started executing (for example when\n * async-manager registration fails after the map insert). Without this, the\n * entry would stay \"running\" forever: TTL cleanup only evicts settled jobs, so\n * the zombie would linger until the max-jobs overflow forcibly removed it.\n */\nexport function discardManagedBashJob(jobId: string): void {\n\tconst job = managedBashJobs.get(jobId);\n\tif (!job) return;\n\tdeleteJobOutput(job);\n\tmanagedBashJobs.delete(jobId);\n}\n\nexport function listManagedBashJobIds(): string[] {\n\treturn [...managedBashJobs.keys()];\n}\n\nexport function getManagedBashJob(jobId: string): ManagedBashJob | undefined {\n\tcleanupManagedBashJobs();\n\tconst job = managedBashJobs.get(jobId);\n\tif (!job) return undefined;\n\tconst { abortController: _abortController, ...snapshot } = job;\n\treturn { ...snapshot };\n}\n\nexport function abortManagedBashJob(jobId: string): ManagedBashJob | undefined {\n\tconst job = managedBashJobs.get(jobId);\n\tif (!job) return undefined;\n\tjob.abortController?.abort();\n\treturn job;\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  import { rmSync } from "node:fs";
2
- const MAX_MANAGED_BASH_JOBS = 100;
3
- const COMPLETED_JOB_TTL_MS = 30 * 60 * 1000;
2
+ export const MAX_MANAGED_BASH_JOBS = 100;
3
+ export const COMPLETED_JOB_TTL_MS = 30 * 60 * 1000;
4
4
  const managedBashJobs = new Map();
5
5
  export function formatAsyncJobError(error) {
6
6
  const message = error instanceof Error ? error.message : String(error);
@@ -41,6 +41,22 @@ export function createManagedBashJob(command, cwd, timeoutSeconds, requestedTime
41
41
  cleanupManagedBashJobs();
42
42
  return job;
43
43
  }
44
+ /**
45
+ * Remove a managed job that never actually started executing (for example when
46
+ * async-manager registration fails after the map insert). Without this, the
47
+ * entry would stay "running" forever: TTL cleanup only evicts settled jobs, so
48
+ * the zombie would linger until the max-jobs overflow forcibly removed it.
49
+ */
50
+ export function discardManagedBashJob(jobId) {
51
+ const job = managedBashJobs.get(jobId);
52
+ if (!job)
53
+ return;
54
+ deleteJobOutput(job);
55
+ managedBashJobs.delete(jobId);
56
+ }
57
+ export function listManagedBashJobIds() {
58
+ return [...managedBashJobs.keys()];
59
+ }
44
60
  export function getManagedBashJob(jobId) {
45
61
  cleanupManagedBashJobs();
46
62
  const job = managedBashJobs.get(jobId);
@@ -1 +1 @@
1
- {"version":3,"file":"bash-async-jobs.js","sourceRoot":"","sources":["../../../src/core/tools/bash-async-jobs.ts"],"names":[],"mappings":"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","sourcesContent":["import { rmSync } from \"node:fs\";\n\nexport interface ManagedBashJob {\n\tjobId: string;\n\tcommand: string;\n\tcwd: string;\n\tstatus: \"running\" | \"completed\" | \"failed\";\n\toutput: string;\n\tfullOutputPath?: string;\n\texitCode?: number | null;\n\terror?: string;\n\tstartedAt: number;\n\tendedAt?: number;\n\ttimeoutSeconds?: number;\n\trequestedTimeoutSeconds?: number;\n\tabortController?: AbortController;\n}\n\nconst MAX_MANAGED_BASH_JOBS = 100;\nconst COMPLETED_JOB_TTL_MS = 30 * 60 * 1000;\nconst managedBashJobs = new Map<string, ManagedBashJob>();\nexport function formatAsyncJobError(error: unknown): string {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (message.startsWith(\"timeout:\")) return `Command timed out after ${message.slice(\"timeout:\".length)} seconds`;\n\treturn message;\n}\nfunction deleteJobOutput(job: ManagedBashJob): void {\n\tif (!job.fullOutputPath) return;\n\ttry { rmSync(job.fullOutputPath, { force: true }); } catch { /* best-effort temp cleanup */ }\n}\n\nfunction cleanupManagedBashJobs(now = Date.now()): void {\n\tfor (const [jobId, job] of managedBashJobs) if (job.status !== \"running\" && job.endedAt !== undefined && now - job.endedAt > COMPLETED_JOB_TTL_MS) { deleteJobOutput(job); managedBashJobs.delete(jobId); }\n\twhile (managedBashJobs.size > MAX_MANAGED_BASH_JOBS) {\n\t\tconst oldest = [...managedBashJobs.values()].sort((a, b) => (a.endedAt ?? a.startedAt) - (b.endedAt ?? b.startedAt))[0];\n\t\tif (!oldest) break;\n\t\tif (oldest.status === \"running\") oldest.abortController?.abort(); else deleteJobOutput(oldest);\n\t\tmanagedBashJobs.delete(oldest.jobId);\n\t}\n}\n\nexport function createManagedBashJob(command: string, cwd: string, timeoutSeconds?: number, requestedTimeoutSeconds?: number): ManagedBashJob {\n\tcleanupManagedBashJobs();\n\tconst jobId = `bash-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n\tconst job: ManagedBashJob = { jobId, command, cwd, status: \"running\", output: \"\", startedAt: Date.now(), timeoutSeconds, requestedTimeoutSeconds, abortController: new AbortController() };\n\tmanagedBashJobs.set(jobId, job);\n\tcleanupManagedBashJobs();\n\treturn job;\n}\n\nexport function getManagedBashJob(jobId: string): ManagedBashJob | undefined {\n\tcleanupManagedBashJobs();\n\tconst job = managedBashJobs.get(jobId);\n\tif (!job) return undefined;\n\tconst { abortController: _abortController, ...snapshot } = job;\n\treturn { ...snapshot };\n}\n\nexport function abortManagedBashJob(jobId: string): ManagedBashJob | undefined {\n\tconst job = managedBashJobs.get(jobId);\n\tif (!job) return undefined;\n\tjob.abortController?.abort();\n\treturn job;\n}\n"]}
1
+ {"version":3,"file":"bash-async-jobs.js","sourceRoot":"","sources":["../../../src/core/tools/bash-async-jobs.ts"],"names":[],"mappings":"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","sourcesContent":["import { rmSync } from \"node:fs\";\n\nexport interface ManagedBashJob {\n\tjobId: string;\n\tcommand: string;\n\tcwd: string;\n\tstatus: \"running\" | \"completed\" | \"failed\";\n\toutput: string;\n\tfullOutputPath?: string;\n\texitCode?: number | null;\n\terror?: string;\n\tstartedAt: number;\n\tendedAt?: number;\n\ttimeoutSeconds?: number;\n\trequestedTimeoutSeconds?: number;\n\tabortController?: AbortController;\n}\n\nexport const MAX_MANAGED_BASH_JOBS = 100;\nexport const COMPLETED_JOB_TTL_MS = 30 * 60 * 1000;\nconst managedBashJobs = new Map<string, ManagedBashJob>();\nexport function formatAsyncJobError(error: unknown): string {\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (message.startsWith(\"timeout:\")) return `Command timed out after ${message.slice(\"timeout:\".length)} seconds`;\n\treturn message;\n}\nfunction deleteJobOutput(job: ManagedBashJob): void {\n\tif (!job.fullOutputPath) return;\n\ttry { rmSync(job.fullOutputPath, { force: true }); } catch { /* best-effort temp cleanup */ }\n}\n\nfunction cleanupManagedBashJobs(now = Date.now()): void {\n\tfor (const [jobId, job] of managedBashJobs) if (job.status !== \"running\" && job.endedAt !== undefined && now - job.endedAt > COMPLETED_JOB_TTL_MS) { deleteJobOutput(job); managedBashJobs.delete(jobId); }\n\twhile (managedBashJobs.size > MAX_MANAGED_BASH_JOBS) {\n\t\tconst oldest = [...managedBashJobs.values()].sort((a, b) => (a.endedAt ?? a.startedAt) - (b.endedAt ?? b.startedAt))[0];\n\t\tif (!oldest) break;\n\t\tif (oldest.status === \"running\") oldest.abortController?.abort(); else deleteJobOutput(oldest);\n\t\tmanagedBashJobs.delete(oldest.jobId);\n\t}\n}\n\nexport function createManagedBashJob(command: string, cwd: string, timeoutSeconds?: number, requestedTimeoutSeconds?: number): ManagedBashJob {\n\tcleanupManagedBashJobs();\n\tconst jobId = `bash-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n\tconst job: ManagedBashJob = { jobId, command, cwd, status: \"running\", output: \"\", startedAt: Date.now(), timeoutSeconds, requestedTimeoutSeconds, abortController: new AbortController() };\n\tmanagedBashJobs.set(jobId, job);\n\tcleanupManagedBashJobs();\n\treturn job;\n}\n\n/**\n * Remove a managed job that never actually started executing (for example when\n * async-manager registration fails after the map insert). Without this, the\n * entry would stay \"running\" forever: TTL cleanup only evicts settled jobs, so\n * the zombie would linger until the max-jobs overflow forcibly removed it.\n */\nexport function discardManagedBashJob(jobId: string): void {\n\tconst job = managedBashJobs.get(jobId);\n\tif (!job) return;\n\tdeleteJobOutput(job);\n\tmanagedBashJobs.delete(jobId);\n}\n\nexport function listManagedBashJobIds(): string[] {\n\treturn [...managedBashJobs.keys()];\n}\n\nexport function getManagedBashJob(jobId: string): ManagedBashJob | undefined {\n\tcleanupManagedBashJobs();\n\tconst job = managedBashJobs.get(jobId);\n\tif (!job) return undefined;\n\tconst { abortController: _abortController, ...snapshot } = job;\n\treturn { ...snapshot };\n}\n\nexport function abortManagedBashJob(jobId: string): ManagedBashJob | undefined {\n\tconst job = managedBashJobs.get(jobId);\n\tif (!job) return undefined;\n\tjob.abortController?.abort();\n\treturn job;\n}\n"]}
@@ -6,5 +6,7 @@ export interface BashAsyncOutputAppender {
6
6
  append(chunk: Buffer): void;
7
7
  close(): Promise<void>;
8
8
  }
9
- export declare function createAsyncOutputAppender(job: BashAsyncOutputTarget): BashAsyncOutputAppender;
9
+ export declare function createAsyncOutputAppender(job: BashAsyncOutputTarget, options?: {
10
+ persistAfterBytes?: number;
11
+ }): BashAsyncOutputAppender;
10
12
  //# sourceMappingURL=bash-async-output.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bash-async-output.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash-async-output.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACvC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAeD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,qBAAqB,GAAG,uBAAuB,CAmD7F","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { APP_NAME } from \"../../config.ts\";\nimport { stripAnsi } from \"../../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../../utils/shell.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize } from \"./truncate.ts\";\n\nexport interface BashAsyncOutputTarget {\n\toutput: string;\n\tfullOutputPath?: string;\n}\n\nexport interface BashAsyncOutputAppender {\n\tappend(chunk: Buffer): void;\n\tclose(): Promise<void>;\n}\n\nfunction outputPath(): string {\n\treturn join(tmpdir(), `${APP_NAME}-bash-async-${randomBytes(8).toString(\"hex\")}.log`);\n}\n\nfunction byteLength(text: string): number { return Buffer.byteLength(text, \"utf8\"); }\nfunction sanitizeDecodedOutput(text: string): string { return sanitizeBinaryOutput(stripAnsi(text)).replace(/\\r/g, \"\"); }\nfunction utf8Prefix(text: string, maxBytes: number): string {\n\tif (byteLength(text) <= maxBytes) return text;\n\tlet end = text.length;\n\twhile (end > 0 && byteLength(text.slice(0, end)) > maxBytes) end--;\n\treturn text.slice(0, end);\n}\n\nexport function createAsyncOutputAppender(job: BashAsyncOutputTarget): BashAsyncOutputAppender {\n\tlet outputBytes = 0;\n\tlet truncated = false;\n\tlet fullOutputStream: WriteStream | undefined;\n\tlet bufferedChunks: Buffer[] = [];\n\tconst decoder = new TextDecoder();\n\n\tconst ensureFullOutputStream = (): WriteStream => {\n\t\tif (fullOutputStream) return fullOutputStream;\n\t\tjob.fullOutputPath = outputPath();\n\t\tfullOutputStream = createWriteStream(job.fullOutputPath);\n\t\tfor (const chunk of bufferedChunks) fullOutputStream.write(chunk);\n\t\tbufferedChunks = [];\n\t\treturn fullOutputStream;\n\t};\n\tconst appendDecodedText = (decoded: string): void => {\n\t\tif (truncated || decoded.length === 0) return;\n\t\tconst text = sanitizeDecodedOutput(decoded);\n\t\tif (text.length === 0) return;\n\t\tconst bytes = byteLength(text);\n\t\tif (outputBytes + bytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureFullOutputStream();\n\t\t\tconst remaining = Math.max(0, DEFAULT_MAX_BYTES - outputBytes);\n\t\t\tif (remaining > 0) job.output += utf8Prefix(text, remaining);\n\t\t\tjob.output += `\\n[Output truncated at ${formatSize(DEFAULT_MAX_BYTES)} for async job polling. Full output: ${job.fullOutputPath}]`;\n\t\t\toutputBytes += bytes;\n\t\t\ttruncated = true;\n\t\t\treturn;\n\t\t}\n\t\toutputBytes += bytes;\n\t\tjob.output += text;\n\t};\n\n\treturn {\n\t\tappend(chunk) {\n\t\t\tif (fullOutputStream) fullOutputStream.write(chunk);\n\t\t\telse bufferedChunks.push(chunk);\n\t\t\tappendDecodedText(decoder.decode(chunk, { stream: true }));\n\t\t},\n\t\tasync close() {\n\t\t\tappendDecodedText(decoder.decode());\n\t\t\tif (!fullOutputStream) return;\n\t\t\tconst stream = fullOutputStream;\n\t\t\tfullOutputStream = undefined;\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tstream.once(\"error\", reject);\n\t\t\t\tstream.once(\"finish\", resolve);\n\t\t\t\tstream.end();\n\t\t\t});\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"bash-async-output.d.ts","sourceRoot":"","sources":["../../../src/core/tools/bash-async-output.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACvC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAeD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,qBAAqB,EAAE,OAAO,CAAC,EAAE;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,uBAAuB,CAqDvI","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { APP_NAME } from \"../../config.ts\";\nimport { stripAnsi } from \"../../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../../utils/shell.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize } from \"./truncate.ts\";\n\nexport interface BashAsyncOutputTarget {\n\toutput: string;\n\tfullOutputPath?: string;\n}\n\nexport interface BashAsyncOutputAppender {\n\tappend(chunk: Buffer): void;\n\tclose(): Promise<void>;\n}\n\nfunction outputPath(): string {\n\treturn join(tmpdir(), `${APP_NAME}-bash-async-${randomBytes(8).toString(\"hex\")}.log`);\n}\n\nfunction byteLength(text: string): number { return Buffer.byteLength(text, \"utf8\"); }\nfunction sanitizeDecodedOutput(text: string): string { return sanitizeBinaryOutput(stripAnsi(text)).replace(/\\r/g, \"\"); }\nfunction utf8Prefix(text: string, maxBytes: number): string {\n\tif (byteLength(text) <= maxBytes) return text;\n\tlet end = text.length;\n\twhile (end > 0 && byteLength(text.slice(0, end)) > maxBytes) end--;\n\treturn text.slice(0, end);\n}\n\nexport function createAsyncOutputAppender(job: BashAsyncOutputTarget, options?: { persistAfterBytes?: number }): BashAsyncOutputAppender {\n\tconst persistAfterBytes = options?.persistAfterBytes ?? DEFAULT_MAX_BYTES;\n\tlet outputBytes = 0;\n\tlet truncated = false;\n\tlet fullOutputStream: WriteStream | undefined;\n\tlet bufferedChunks: Buffer[] = [];\n\tconst decoder = new TextDecoder();\n\n\tconst ensureFullOutputStream = (): WriteStream => {\n\t\tif (fullOutputStream) return fullOutputStream;\n\t\tjob.fullOutputPath = outputPath();\n\t\tfullOutputStream = createWriteStream(job.fullOutputPath);\n\t\tfor (const chunk of bufferedChunks) fullOutputStream.write(chunk);\n\t\tbufferedChunks = [];\n\t\treturn fullOutputStream;\n\t};\n\tconst appendDecodedText = (decoded: string): void => {\n\t\tif (truncated || decoded.length === 0) return;\n\t\tconst text = sanitizeDecodedOutput(decoded);\n\t\tif (text.length === 0) return;\n\t\tconst bytes = byteLength(text);\n\t\tif (outputBytes + bytes > persistAfterBytes) ensureFullOutputStream();\n\t\tif (outputBytes + bytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureFullOutputStream();\n\t\t\tconst remaining = Math.max(0, DEFAULT_MAX_BYTES - outputBytes);\n\t\t\tif (remaining > 0) job.output += utf8Prefix(text, remaining);\n\t\t\tjob.output += `\\n[Output truncated at ${formatSize(DEFAULT_MAX_BYTES)} for async job polling. Full output: ${job.fullOutputPath}]`;\n\t\t\toutputBytes += bytes;\n\t\t\ttruncated = true;\n\t\t\treturn;\n\t\t}\n\t\toutputBytes += bytes;\n\t\tjob.output += text;\n\t};\n\n\treturn {\n\t\tappend(chunk) {\n\t\t\tif (fullOutputStream) fullOutputStream.write(chunk);\n\t\t\telse bufferedChunks.push(chunk);\n\t\t\tappendDecodedText(decoder.decode(chunk, { stream: true }));\n\t\t},\n\t\tasync close() {\n\t\t\tappendDecodedText(decoder.decode());\n\t\t\tif (!fullOutputStream) return;\n\t\t\tconst stream = fullOutputStream;\n\t\t\tfullOutputStream = undefined;\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tstream.once(\"error\", reject);\n\t\t\t\tstream.once(\"finish\", resolve);\n\t\t\t\tstream.end();\n\t\t\t});\n\t\t},\n\t};\n}\n"]}
@@ -19,7 +19,8 @@ function utf8Prefix(text, maxBytes) {
19
19
  end--;
20
20
  return text.slice(0, end);
21
21
  }
22
- export function createAsyncOutputAppender(job) {
22
+ export function createAsyncOutputAppender(job, options) {
23
+ const persistAfterBytes = options?.persistAfterBytes ?? DEFAULT_MAX_BYTES;
23
24
  let outputBytes = 0;
24
25
  let truncated = false;
25
26
  let fullOutputStream;
@@ -42,6 +43,8 @@ export function createAsyncOutputAppender(job) {
42
43
  if (text.length === 0)
43
44
  return;
44
45
  const bytes = byteLength(text);
46
+ if (outputBytes + bytes > persistAfterBytes)
47
+ ensureFullOutputStream();
45
48
  if (outputBytes + bytes > DEFAULT_MAX_BYTES) {
46
49
  ensureFullOutputStream();
47
50
  const remaining = Math.max(0, DEFAULT_MAX_BYTES - outputBytes);
@@ -1 +1 @@
1
- {"version":3,"file":"bash-async-output.js","sourceRoot":"","sources":["../../../src/core/tools/bash-async-output.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { APP_NAME } from \"../../config.ts\";\nimport { stripAnsi } from \"../../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../../utils/shell.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize } from \"./truncate.ts\";\n\nexport interface BashAsyncOutputTarget {\n\toutput: string;\n\tfullOutputPath?: string;\n}\n\nexport interface BashAsyncOutputAppender {\n\tappend(chunk: Buffer): void;\n\tclose(): Promise<void>;\n}\n\nfunction outputPath(): string {\n\treturn join(tmpdir(), `${APP_NAME}-bash-async-${randomBytes(8).toString(\"hex\")}.log`);\n}\n\nfunction byteLength(text: string): number { return Buffer.byteLength(text, \"utf8\"); }\nfunction sanitizeDecodedOutput(text: string): string { return sanitizeBinaryOutput(stripAnsi(text)).replace(/\\r/g, \"\"); }\nfunction utf8Prefix(text: string, maxBytes: number): string {\n\tif (byteLength(text) <= maxBytes) return text;\n\tlet end = text.length;\n\twhile (end > 0 && byteLength(text.slice(0, end)) > maxBytes) end--;\n\treturn text.slice(0, end);\n}\n\nexport function createAsyncOutputAppender(job: BashAsyncOutputTarget): BashAsyncOutputAppender {\n\tlet outputBytes = 0;\n\tlet truncated = false;\n\tlet fullOutputStream: WriteStream | undefined;\n\tlet bufferedChunks: Buffer[] = [];\n\tconst decoder = new TextDecoder();\n\n\tconst ensureFullOutputStream = (): WriteStream => {\n\t\tif (fullOutputStream) return fullOutputStream;\n\t\tjob.fullOutputPath = outputPath();\n\t\tfullOutputStream = createWriteStream(job.fullOutputPath);\n\t\tfor (const chunk of bufferedChunks) fullOutputStream.write(chunk);\n\t\tbufferedChunks = [];\n\t\treturn fullOutputStream;\n\t};\n\tconst appendDecodedText = (decoded: string): void => {\n\t\tif (truncated || decoded.length === 0) return;\n\t\tconst text = sanitizeDecodedOutput(decoded);\n\t\tif (text.length === 0) return;\n\t\tconst bytes = byteLength(text);\n\t\tif (outputBytes + bytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureFullOutputStream();\n\t\t\tconst remaining = Math.max(0, DEFAULT_MAX_BYTES - outputBytes);\n\t\t\tif (remaining > 0) job.output += utf8Prefix(text, remaining);\n\t\t\tjob.output += `\\n[Output truncated at ${formatSize(DEFAULT_MAX_BYTES)} for async job polling. Full output: ${job.fullOutputPath}]`;\n\t\t\toutputBytes += bytes;\n\t\t\ttruncated = true;\n\t\t\treturn;\n\t\t}\n\t\toutputBytes += bytes;\n\t\tjob.output += text;\n\t};\n\n\treturn {\n\t\tappend(chunk) {\n\t\t\tif (fullOutputStream) fullOutputStream.write(chunk);\n\t\t\telse bufferedChunks.push(chunk);\n\t\t\tappendDecodedText(decoder.decode(chunk, { stream: true }));\n\t\t},\n\t\tasync close() {\n\t\t\tappendDecodedText(decoder.decode());\n\t\t\tif (!fullOutputStream) return;\n\t\t\tconst stream = fullOutputStream;\n\t\t\tfullOutputStream = undefined;\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tstream.once(\"error\", reject);\n\t\t\t\tstream.once(\"finish\", resolve);\n\t\t\t\tstream.end();\n\t\t\t});\n\t\t},\n\t};\n}\n"]}
1
+ {"version":3,"file":"bash-async-output.js","sourceRoot":"","sources":["../../../src/core/tools/bash-async-output.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream, type WriteStream } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { APP_NAME } from \"../../config.ts\";\nimport { stripAnsi } from \"../../utils/ansi.ts\";\nimport { sanitizeBinaryOutput } from \"../../utils/shell.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize } from \"./truncate.ts\";\n\nexport interface BashAsyncOutputTarget {\n\toutput: string;\n\tfullOutputPath?: string;\n}\n\nexport interface BashAsyncOutputAppender {\n\tappend(chunk: Buffer): void;\n\tclose(): Promise<void>;\n}\n\nfunction outputPath(): string {\n\treturn join(tmpdir(), `${APP_NAME}-bash-async-${randomBytes(8).toString(\"hex\")}.log`);\n}\n\nfunction byteLength(text: string): number { return Buffer.byteLength(text, \"utf8\"); }\nfunction sanitizeDecodedOutput(text: string): string { return sanitizeBinaryOutput(stripAnsi(text)).replace(/\\r/g, \"\"); }\nfunction utf8Prefix(text: string, maxBytes: number): string {\n\tif (byteLength(text) <= maxBytes) return text;\n\tlet end = text.length;\n\twhile (end > 0 && byteLength(text.slice(0, end)) > maxBytes) end--;\n\treturn text.slice(0, end);\n}\n\nexport function createAsyncOutputAppender(job: BashAsyncOutputTarget, options?: { persistAfterBytes?: number }): BashAsyncOutputAppender {\n\tconst persistAfterBytes = options?.persistAfterBytes ?? DEFAULT_MAX_BYTES;\n\tlet outputBytes = 0;\n\tlet truncated = false;\n\tlet fullOutputStream: WriteStream | undefined;\n\tlet bufferedChunks: Buffer[] = [];\n\tconst decoder = new TextDecoder();\n\n\tconst ensureFullOutputStream = (): WriteStream => {\n\t\tif (fullOutputStream) return fullOutputStream;\n\t\tjob.fullOutputPath = outputPath();\n\t\tfullOutputStream = createWriteStream(job.fullOutputPath);\n\t\tfor (const chunk of bufferedChunks) fullOutputStream.write(chunk);\n\t\tbufferedChunks = [];\n\t\treturn fullOutputStream;\n\t};\n\tconst appendDecodedText = (decoded: string): void => {\n\t\tif (truncated || decoded.length === 0) return;\n\t\tconst text = sanitizeDecodedOutput(decoded);\n\t\tif (text.length === 0) return;\n\t\tconst bytes = byteLength(text);\n\t\tif (outputBytes + bytes > persistAfterBytes) ensureFullOutputStream();\n\t\tif (outputBytes + bytes > DEFAULT_MAX_BYTES) {\n\t\t\tensureFullOutputStream();\n\t\t\tconst remaining = Math.max(0, DEFAULT_MAX_BYTES - outputBytes);\n\t\t\tif (remaining > 0) job.output += utf8Prefix(text, remaining);\n\t\t\tjob.output += `\\n[Output truncated at ${formatSize(DEFAULT_MAX_BYTES)} for async job polling. Full output: ${job.fullOutputPath}]`;\n\t\t\toutputBytes += bytes;\n\t\t\ttruncated = true;\n\t\t\treturn;\n\t\t}\n\t\toutputBytes += bytes;\n\t\tjob.output += text;\n\t};\n\n\treturn {\n\t\tappend(chunk) {\n\t\t\tif (fullOutputStream) fullOutputStream.write(chunk);\n\t\t\telse bufferedChunks.push(chunk);\n\t\t\tappendDecodedText(decoder.decode(chunk, { stream: true }));\n\t\t},\n\t\tasync close() {\n\t\t\tappendDecodedText(decoder.decode());\n\t\t\tif (!fullOutputStream) return;\n\t\t\tconst stream = fullOutputStream;\n\t\t\tfullOutputStream = undefined;\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tstream.once(\"error\", reject);\n\t\t\t\tstream.once(\"finish\", resolve);\n\t\t\t\tstream.end();\n\t\t\t});\n\t\t},\n\t};\n}\n"]}
@@ -1,5 +1,7 @@
1
1
  import type { AgentTool } from "@earendil-works/pi-agent-core";
2
2
  import { type Static, Type } from "typebox";
3
+ import type { AsyncJobManager } from "../async/job-manager.js";
4
+ import type { AsyncJobDeliveryMessage } from "../async/types.js";
3
5
  import type { BashResult } from "../bash-executor.ts";
4
6
  import type { ToolDefinition } from "../extensions/types.ts";
5
7
  import { type BashInterceptorRule } from "./bash-interceptor.ts";
@@ -54,19 +56,23 @@ export interface BashInterceptorResult {
54
56
  }
55
57
  export type BashInterceptor = (context: BashSpawnContext) => Promise<BashInterceptorResult | undefined> | BashInterceptorResult | undefined;
56
58
  export interface BashToolOptions {
57
- /** Custom operations for command execution. Default: local shell */
58
59
  operations?: BashOperations;
59
- /** Command prefix prepended to every command (for example shell setup commands) */
60
+ /** Prefix prepended to every shell command before execution. */
60
61
  commandPrefix?: string;
61
- /** Optional explicit shell path from settings */
62
+ /** Override shell executable resolution for local bash operations. */
62
63
  shellPath?: string;
63
- /** Hook to adjust command, cwd, or env before execution */
64
+ /** Last-mile hook for rewriting the command/cwd/env spawn context. */
64
65
  spawnHook?: BashSpawnHook;
66
+ /** Optional command interceptor used by extensions and parity tests. */
65
67
  interceptor?: BashInterceptor;
66
68
  interceptorEnabled?: boolean | (() => boolean);
67
69
  availableTools?: string[];
68
70
  interceptorRules?: BashInterceptorRule[];
71
+ /** Enable background bash jobs and session-managed async result delivery. */
69
72
  asyncEnabled?: boolean;
73
+ asyncJobManager?: AsyncJobManager;
74
+ asyncJobDeliveryHandler?: (message: AsyncJobDeliveryMessage) => void | Promise<void>;
75
+ asyncJobSessionId?: symbol;
70
76
  }
71
77
  type BashRenderState = {
72
78
  startedAt: number | undefined;