harness-dispatch 0.8.0 → 0.10.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.
Files changed (174) hide show
  1. package/CHANGELOG.md +1074 -2
  2. package/README.md +92 -36
  3. package/config.default.yaml +19 -3
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js.map +1 -1
  6. package/dist/billing.d.ts +24 -0
  7. package/dist/billing.d.ts.map +1 -1
  8. package/dist/billing.js +24 -0
  9. package/dist/billing.js.map +1 -1
  10. package/dist/bin.d.ts.map +1 -1
  11. package/dist/bin.js +299 -48
  12. package/dist/bin.js.map +1 -1
  13. package/dist/circuit-breaker.d.ts +9 -1
  14. package/dist/circuit-breaker.d.ts.map +1 -1
  15. package/dist/circuit-breaker.js +15 -1
  16. package/dist/circuit-breaker.js.map +1 -1
  17. package/dist/client-register.d.ts +11 -1
  18. package/dist/client-register.d.ts.map +1 -1
  19. package/dist/client-register.js +15 -6
  20. package/dist/client-register.js.map +1 -1
  21. package/dist/config/coercions.d.ts +5 -0
  22. package/dist/config/coercions.d.ts.map +1 -1
  23. package/dist/config/coercions.js +8 -0
  24. package/dist/config/coercions.js.map +1 -1
  25. package/dist/config/protocol.js.map +1 -1
  26. package/dist/config/validation.d.ts +12 -0
  27. package/dist/config/validation.d.ts.map +1 -1
  28. package/dist/config/validation.js +133 -7
  29. package/dist/config/validation.js.map +1 -1
  30. package/dist/config.d.ts +15 -5
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +199 -152
  33. package/dist/config.js.map +1 -1
  34. package/dist/configure-yaml.d.ts +2 -0
  35. package/dist/configure-yaml.d.ts.map +1 -1
  36. package/dist/configure-yaml.js +114 -4
  37. package/dist/configure-yaml.js.map +1 -1
  38. package/dist/dispatch-log.d.ts.map +1 -1
  39. package/dist/dispatch-log.js +3 -1
  40. package/dist/dispatch-log.js.map +1 -1
  41. package/dist/dispatchers/generic-cli.d.ts.map +1 -1
  42. package/dist/dispatchers/generic-cli.js +281 -112
  43. package/dist/dispatchers/generic-cli.js.map +1 -1
  44. package/dist/dispatchers/openai-compatible.d.ts +0 -5
  45. package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
  46. package/dist/dispatchers/openai-compatible.js +150 -83
  47. package/dist/dispatchers/openai-compatible.js.map +1 -1
  48. package/dist/dispatchers/shared/harness-login.d.ts +25 -0
  49. package/dist/dispatchers/shared/harness-login.d.ts.map +1 -0
  50. package/dist/dispatchers/shared/harness-login.js +73 -0
  51. package/dist/dispatchers/shared/harness-login.js.map +1 -0
  52. package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -1
  53. package/dist/dispatchers/shared/stream-subprocess.js +71 -58
  54. package/dist/dispatchers/shared/stream-subprocess.js.map +1 -1
  55. package/dist/file-lock.d.ts +29 -1
  56. package/dist/file-lock.d.ts.map +1 -1
  57. package/dist/file-lock.js +53 -20
  58. package/dist/file-lock.js.map +1 -1
  59. package/dist/harness-presets.d.ts +51 -0
  60. package/dist/harness-presets.d.ts.map +1 -0
  61. package/dist/harness-presets.js +154 -0
  62. package/dist/harness-presets.js.map +1 -0
  63. package/dist/http/parse.d.ts +8 -1
  64. package/dist/http/parse.d.ts.map +1 -1
  65. package/dist/http/parse.js +10 -3
  66. package/dist/http/parse.js.map +1 -1
  67. package/dist/http/server.d.ts +2 -0
  68. package/dist/http/server.d.ts.map +1 -1
  69. package/dist/http/server.js +188 -16
  70. package/dist/http/server.js.map +1 -1
  71. package/dist/job-runner.js +6 -0
  72. package/dist/job-runner.js.map +1 -1
  73. package/dist/jobs/context.d.ts +0 -7
  74. package/dist/jobs/context.d.ts.map +1 -1
  75. package/dist/jobs/context.js +95 -7
  76. package/dist/jobs/context.js.map +1 -1
  77. package/dist/jobs/lifecycle.d.ts +100 -0
  78. package/dist/jobs/lifecycle.d.ts.map +1 -0
  79. package/dist/jobs/lifecycle.js +263 -0
  80. package/dist/jobs/lifecycle.js.map +1 -0
  81. package/dist/jobs/read.d.ts +17 -0
  82. package/dist/jobs/read.d.ts.map +1 -0
  83. package/dist/jobs/read.js +103 -0
  84. package/dist/jobs/read.js.map +1 -0
  85. package/dist/jobs/run.d.ts +45 -0
  86. package/dist/jobs/run.d.ts.map +1 -0
  87. package/dist/jobs/run.js +351 -0
  88. package/dist/jobs/run.js.map +1 -0
  89. package/dist/jobs/start.d.ts +9 -0
  90. package/dist/jobs/start.d.ts.map +1 -0
  91. package/dist/jobs/start.js +126 -0
  92. package/dist/jobs/start.js.map +1 -0
  93. package/dist/jobs/store.d.ts.map +1 -1
  94. package/dist/jobs/store.js +13 -3
  95. package/dist/jobs/store.js.map +1 -1
  96. package/dist/jobs/supervisor.d.ts +151 -0
  97. package/dist/jobs/supervisor.d.ts.map +1 -0
  98. package/dist/jobs/supervisor.js +679 -0
  99. package/dist/jobs/supervisor.js.map +1 -0
  100. package/dist/jobs/types.d.ts +8 -1
  101. package/dist/jobs/types.d.ts.map +1 -1
  102. package/dist/jobs.d.ts +15 -218
  103. package/dist/jobs.d.ts.map +1 -1
  104. package/dist/jobs.js +14 -1317
  105. package/dist/jobs.js.map +1 -1
  106. package/dist/leaderboard.d.ts.map +1 -1
  107. package/dist/leaderboard.js +44 -28
  108. package/dist/leaderboard.js.map +1 -1
  109. package/dist/mcp/config-hot-reload.d.ts +14 -3
  110. package/dist/mcp/config-hot-reload.d.ts.map +1 -1
  111. package/dist/mcp/config-hot-reload.js +27 -6
  112. package/dist/mcp/config-hot-reload.js.map +1 -1
  113. package/dist/mcp/dispatcher-factory.d.ts.map +1 -1
  114. package/dist/mcp/dispatcher-factory.js +15 -4
  115. package/dist/mcp/dispatcher-factory.js.map +1 -1
  116. package/dist/mcp/resources.d.ts.map +1 -1
  117. package/dist/mcp/resources.js +3 -2
  118. package/dist/mcp/resources.js.map +1 -1
  119. package/dist/mcp/tool-schemas.d.ts +1 -1
  120. package/dist/mcp/tool-schemas.d.ts.map +1 -1
  121. package/dist/mcp/tool-schemas.js +6 -3
  122. package/dist/mcp/tool-schemas.js.map +1 -1
  123. package/dist/mcp/tools.d.ts +20 -6
  124. package/dist/mcp/tools.d.ts.map +1 -1
  125. package/dist/mcp/tools.js +92 -14
  126. package/dist/mcp/tools.js.map +1 -1
  127. package/dist/mcp-clients.d.ts +9 -0
  128. package/dist/mcp-clients.d.ts.map +1 -1
  129. package/dist/mcp-clients.js +4 -0
  130. package/dist/mcp-clients.js.map +1 -1
  131. package/dist/observability/spans.d.ts.map +1 -1
  132. package/dist/observability/spans.js +10 -2
  133. package/dist/observability/spans.js.map +1 -1
  134. package/dist/quota.d.ts.map +1 -1
  135. package/dist/quota.js +16 -2
  136. package/dist/quota.js.map +1 -1
  137. package/dist/redaction.d.ts +90 -0
  138. package/dist/redaction.d.ts.map +1 -0
  139. package/dist/redaction.js +221 -0
  140. package/dist/redaction.js.map +1 -0
  141. package/dist/route-policy.d.ts +2 -1
  142. package/dist/route-policy.d.ts.map +1 -1
  143. package/dist/route-policy.js +54 -0
  144. package/dist/route-policy.js.map +1 -1
  145. package/dist/router.d.ts +12 -23
  146. package/dist/router.d.ts.map +1 -1
  147. package/dist/router.js +86 -223
  148. package/dist/router.js.map +1 -1
  149. package/dist/safety.d.ts.map +1 -1
  150. package/dist/safety.js +7 -1
  151. package/dist/safety.js.map +1 -1
  152. package/dist/state-dir.d.ts +10 -0
  153. package/dist/state-dir.d.ts.map +1 -1
  154. package/dist/state-dir.js +27 -1
  155. package/dist/state-dir.js.map +1 -1
  156. package/dist/status.d.ts +14 -3
  157. package/dist/status.d.ts.map +1 -1
  158. package/dist/status.js +36 -3
  159. package/dist/status.js.map +1 -1
  160. package/dist/types.d.ts +24 -1
  161. package/dist/types.d.ts.map +1 -1
  162. package/dist/workspace-lock.d.ts +20 -0
  163. package/dist/workspace-lock.d.ts.map +1 -1
  164. package/dist/workspace-lock.js +63 -0
  165. package/dist/workspace-lock.js.map +1 -1
  166. package/dist/workspace-resolve.d.ts +0 -9
  167. package/dist/workspace-resolve.d.ts.map +1 -1
  168. package/dist/workspace-resolve.js +115 -183
  169. package/dist/workspace-resolve.js.map +1 -1
  170. package/dist/workspaces.d.ts +15 -0
  171. package/dist/workspaces.d.ts.map +1 -1
  172. package/dist/workspaces.js +330 -90
  173. package/dist/workspaces.js.map +1 -1
  174. package/package.json +5 -4
@@ -15,9 +15,13 @@ import { assertValidJobId, jobsRoot, readJson } from "./store.js";
15
15
  * Every character here is a character the delegate's model must read before it
16
16
  * reaches the actual instruction, and agent CLIs are already carrying a system
17
17
  * prompt and file contents. 24k is roughly six pages: enough for several prior
18
- * results, small enough that it cannot crowd out the task itself. Oldest
19
- * entries are truncated first, since the most recent step is usually the one
20
- * being built on.
18
+ * results, small enough that it cannot crowd out the task itself.
19
+ *
20
+ * Entries are filled in the order the CALLER listed them, and it is the LAST
21
+ * ones that get truncated or omitted when the budget runs out. This comment
22
+ * claimed the opposite — "oldest entries are truncated first" — which was
23
+ * never true of the loop below and would be the wrong policy to infer: the
24
+ * caller controls the order, so put the job you most want carried first.
21
25
  */
22
26
  /** Newline, named so the templates below stay readable. */
23
27
  const NL = "\n";
@@ -36,12 +40,63 @@ function clip(text, limit) {
36
40
  * a delegate told "here is what came before" while a step is quietly missing
37
41
  * would reason from an incomplete picture and never know.
38
42
  */
43
+ /**
44
+ * Where a prior job ran, as a header suffix — or "" when it cannot be read.
45
+ *
46
+ * `contextJobs` takes any jobId from the machine-wide jobs root and inlines
47
+ * that job's prompt and output verbatim, with no working-directory scoping.
48
+ * Reproduced: a job recorded against one project chained cleanly into a
49
+ * dispatch for another, its prompt and output carried across with nothing
50
+ * saying they came from somewhere else.
51
+ *
52
+ * Deliberately DISCLOSED rather than blocked. Chaining across projects is a
53
+ * legitimate thing to want — a review job informing work in a sibling
54
+ * checkout — and the jobId has to be passed explicitly by the orchestrator, so
55
+ * this is a missing guardrail, not an injection route. What was actually
56
+ * wrong is that neither the orchestrator nor the delegate could SEE it: an
57
+ * agent that passed the wrong id got another project's source in its prompt
58
+ * and no hint of it. Naming the directory is also plainly useful when the
59
+ * chaining is intended.
60
+ */
61
+ async function ranIn(jobId) {
62
+ try {
63
+ const manifest = await readJson(path.join(jobsRoot(), jobId, "manifest.json"));
64
+ const dir = manifest.workingDir;
65
+ return typeof dir === "string" && dir !== "" ? `, ran in ${dir}` : "";
66
+ }
67
+ catch {
68
+ return "";
69
+ }
70
+ }
71
+ /**
72
+ * A prior job's PARTIAL output, when it has no result.json.
73
+ *
74
+ * Returns undefined when there is nothing on disk, so the caller can fall
75
+ * through to its "no result available" wording — the partial log is the
76
+ * salvage path, not a replacement for a real result.
77
+ */
78
+ async function partialSection(jobId) {
79
+ const partialPath = path.join(jobsRoot(), jobId, "output", "stdout.partial.log");
80
+ const partial = await readFile(partialPath, "utf8").catch(() => undefined);
81
+ if (partial === undefined || partial.trim() === "")
82
+ return undefined;
83
+ const priorPrompt = await readFile(path.join(jobsRoot(), jobId, "prompt.md"), "utf8").catch(() => "(prompt unavailable)");
84
+ return [
85
+ `### ${jobId} (INCOMPLETE — no final result; this is how far it got${await ranIn(jobId)})`,
86
+ "",
87
+ "Task it was given:",
88
+ clip(priorPrompt.trim(), 1_000),
89
+ "",
90
+ "Partial output before it stopped:",
91
+ clip(partial.trim(), MAX_CONTEXT_CHARS_PER_JOB),
92
+ ].join(NL);
93
+ }
39
94
  export async function buildContextPreamble(contextJobs) {
40
95
  if (contextJobs.length === 0)
41
96
  return "";
42
97
  const sections = [];
43
98
  let budget = MAX_CONTEXT_CHARS;
44
- for (const jobId of contextJobs) {
99
+ for (const [index, jobId] of contextJobs.entries()) {
45
100
  let section;
46
101
  try {
47
102
  assertValidJobId(jobId);
@@ -50,7 +105,7 @@ export async function buildContextPreamble(contextJobs) {
50
105
  const priorPrompt = await readFile(path.join(jobDir, "prompt.md"), "utf8").catch(() => "(prompt unavailable)");
51
106
  const output = payload.result?.output ?? "";
52
107
  section = [
53
- `### ${jobId} (${payload.result?.success === false ? "FAILED" : "completed"})`,
108
+ `### ${jobId} (${payload.result?.success === false ? "FAILED" : "completed"}${await ranIn(jobId)})`,
54
109
  "",
55
110
  "Task it was given:",
56
111
  clip(priorPrompt.trim(), 1_000),
@@ -60,14 +115,47 @@ export async function buildContextPreamble(contextJobs) {
60
115
  ].join(NL);
61
116
  }
62
117
  catch {
63
- section = `### ${jobId}${NL}${NL}(no result available this job is unknown, still running, or was pruned)`;
118
+ // No result.jsonbut a job whose supervisor died leaves its progress
119
+ // in stdout.partial.log, and chaining on "what the last job got to" is
120
+ // exactly what a caller wants after an orphaned run. Reporting "no
121
+ // result available" while that file sits on disk discards the trail
122
+ // PRODUCT.md names as the thing that must never be lost.
123
+ section = await partialSection(jobId).catch(() => undefined) ??
124
+ `### ${jobId}${NL}${NL}(no result available — this job is unknown, still running, or was pruned)`;
64
125
  }
65
126
  if (section.length > budget)
66
127
  section = clip(section, Math.max(0, budget));
67
128
  budget -= section.length;
68
129
  sections.push(section);
69
- if (budget <= 0)
130
+ if (budget <= 0) {
131
+ // Name what did not fit, rather than stopping silently.
132
+ //
133
+ // This loop dropped every remaining job with no header and no note —
134
+ // measured with five 8KB results: three appeared, jobs 4 and 5 were
135
+ // absent from the output entirely. That contradicts this module's own
136
+ // contract two functions up: "Unknown or unfinished jobs are reported
137
+ // inline rather than skipped silently: a delegate ... would reason from
138
+ // an incomplete picture and never know." A job dropped for want of
139
+ // budget is exactly that case, and it is worse than an unknown one,
140
+ // because the caller explicitly asked for it.
141
+ // Slice from the LOOP's position, not `indexOf`.
142
+ //
143
+ // `indexOf` returns the FIRST occurrence, so a repeated jobId made the
144
+ // notice describe the wrong point in the list: with [j1,j2,j1,j3,j4] it
145
+ // named four jobs omitted — including j1 and j2, whose output was
146
+ // rendered directly above it — and told the delegate to fetch them.
147
+ // Only j3 and j4 were actually dropped. A duplicate id is a plausible
148
+ // thing for an orchestrator to pass, and the notice was added in this
149
+ // same release, so this is that fix being wrong one case over.
150
+ const dropped = contextJobs.slice(index + 1);
151
+ if (dropped.length > 0) {
152
+ sections.push(`### ${dropped.length} earlier job(s) omitted` +
153
+ `${NL}${NL}The ${MAX_CONTEXT_CHARS}-character context budget ran out before these: ` +
154
+ `${dropped.join(", ")}. Their output is NOT below. Ask for it with job_status if ` +
155
+ `you need it.`);
156
+ }
70
157
  break;
158
+ }
71
159
  }
72
160
  return [
73
161
  "## Context from earlier delegated work",
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/jobs/context.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/jobs/context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The lifecycle verbs: stop a run, resolve its workspace, run it again.
3
+ *
4
+ * These sit above start/read because a retry starts a new job and a cancel
5
+ * reads one, so the dependency runs one way — lifecycle imports start, never
6
+ * the reverse.
7
+ */
8
+ import type { JobDeps, JobStatus } from "./types.js";
9
+ /** What a cancel request actually did — the caller needs to tell these apart. */
10
+ export interface CancelOutcome {
11
+ jobId: string;
12
+ /**
13
+ * `cancelled` — it was queued and never started; stopped outright.
14
+ * `cancelling` — it is running; teardown requested and lands shortly.
15
+ * `already_finished` — it had already reached a terminal state; nothing done.
16
+ */
17
+ outcome: "cancelled" | "cancelling" | "already_finished";
18
+ status: JobStatus["status"];
19
+ message: string;
20
+ }
21
+ /**
22
+ * Ask a job to stop.
23
+ *
24
+ * Cancellation cannot be a signal here: jobs run inside POOLED supervisors,
25
+ * and the only pid recorded against a job belongs to a process that is also
26
+ * running other jobs, so signalling it would cancel work nobody asked to
27
+ * cancel. Instead this writes a marker the run itself honours — it drops out
28
+ * of its event stream, which triggers the dispatcher's teardown (killTree on
29
+ * the agent CLI and its children) and releases the workspace lock through the
30
+ * same path a normal finish uses.
31
+ *
32
+ * Two consequences worth stating plainly, because a caller who assumes
33
+ * otherwise will be surprised:
34
+ *
35
+ * 1. It is not instantaneous. A running job stops within about a second;
36
+ * `cancelling` means requested, not done. Poll job_status to see it land.
37
+ * 2. Work already done is NOT undone. A cancelled agent may have already
38
+ * edited files in the workspace, and those edits stay. Cancelling stops
39
+ * further work; it is not a rollback.
40
+ *
41
+ * A cancelled run is deliberately not recorded as a failure: the route's
42
+ * circuit breaker and failure count never see it, because the caller changing
43
+ * their mind says nothing about whether the route works.
44
+ */
45
+ export declare function cancelJob(jobId: string, reason?: string): Promise<CancelOutcome>;
46
+ /**
47
+ * Inspect or resolve the isolated workspace a finished job left behind.
48
+ *
49
+ * Looks the job up the same way job_status does, then hands off to
50
+ * workspace-resolve.ts. Kept here so the caller only ever needs a jobId —
51
+ * where the workspace lives, and which policy produced it, are details
52
+ * recorded in the job's own result.
53
+ */
54
+ export declare function resolveJobWorkspace(jobId: string, action: "diff" | "apply" | "discard", opts?: {
55
+ force?: boolean;
56
+ }): Promise<unknown>;
57
+ export interface RetryOutcome {
58
+ jobId: string;
59
+ retryOf: string;
60
+ service?: string;
61
+ reusedFrom: {
62
+ prompt: boolean;
63
+ files: number;
64
+ workingDir: string;
65
+ };
66
+ /**
67
+ * The original's model, when the retry's route does not declare it.
68
+ *
69
+ * `retryJob` has always SET this and the MCP tool description has always
70
+ * documented it, but the interface never listed it — the object is built
71
+ * with a conditional spread, which TypeScript does not excess-property
72
+ * check. So a typed caller could not see a field that ships, and the tests
73
+ * asserting it only compiled because the tests were not typechecked.
74
+ */
75
+ droppedModel?: string;
76
+ message: string;
77
+ }
78
+ /**
79
+ * Run a finished job's task again.
80
+ *
81
+ * The last verb missing from the job lifecycle: you could start work, watch
82
+ * it, stop it, and resolve its workspace — but if it failed, reproducing it
83
+ * meant reconstructing the prompt, the file list, the working directory and
84
+ * the hints by hand, from a job record that already holds all four. The
85
+ * machinery to execute a job bundle existed (executeJobDir) and simply was
86
+ * not reachable from outside.
87
+ *
88
+ * The prompt is taken from prompt.md, which is the FROZEN prompt — including
89
+ * any context preamble the original dispatch rendered in. A retry therefore
90
+ * reproduces what the delegate actually saw, not what the caller typed.
91
+ *
92
+ * `service` retargets the attempt, which is the common case rather than an
93
+ * afterthought: the reason a run failed is often the route, not the task
94
+ * ("codex hit its usage limit — try claude"). Omit it to reuse the original
95
+ * route, or to let the router pick again if the original had none.
96
+ */
97
+ export declare function retryJob(jobId: string, deps: JobDeps, opts?: {
98
+ service?: string;
99
+ }): Promise<RetryOutcome>;
100
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../src/jobs/lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqBH,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACrD,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,OAAO,EAAE,WAAW,GAAG,YAAY,GAAG,kBAAkB,CAAC;IACzD,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAqFtF;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,EACpC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAC7B,OAAO,CAAC,OAAO,CAAC,CAsBlB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE;QAAE,MAAM,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,QAAQ,CAC5B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,OAAO,EACb,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9B,OAAO,CAAC,YAAY,CAAC,CAoFvB"}
@@ -0,0 +1,263 @@
1
+ /**
2
+ * The lifecycle verbs: stop a run, resolve its workspace, run it again.
3
+ *
4
+ * These sit above start/read because a retry starts a new job and a cancel
5
+ * reads one, so the dependency runs one way — lifecycle imports start, never
6
+ * the reverse.
7
+ */
8
+ import { getAsyncJob } from "./read.js";
9
+ import { startAsyncJobTracked } from "./start.js";
10
+ import { readFile } from "node:fs/promises";
11
+ import path from "node:path";
12
+ import { declaresModel } from "../router.js";
13
+ import { applyWorkspace, discardWorkspace, isResolvable, workspaceDiff, } from "../workspace-resolve.js";
14
+ import { requestCancel, jobsRoot, readJson, timestamp, updateStatus, } from "./store.js";
15
+ /**
16
+ * Ask a job to stop.
17
+ *
18
+ * Cancellation cannot be a signal here: jobs run inside POOLED supervisors,
19
+ * and the only pid recorded against a job belongs to a process that is also
20
+ * running other jobs, so signalling it would cancel work nobody asked to
21
+ * cancel. Instead this writes a marker the run itself honours — it drops out
22
+ * of its event stream, which triggers the dispatcher's teardown (killTree on
23
+ * the agent CLI and its children) and releases the workspace lock through the
24
+ * same path a normal finish uses.
25
+ *
26
+ * Two consequences worth stating plainly, because a caller who assumes
27
+ * otherwise will be surprised:
28
+ *
29
+ * 1. It is not instantaneous. A running job stops within about a second;
30
+ * `cancelling` means requested, not done. Poll job_status to see it land.
31
+ * 2. Work already done is NOT undone. A cancelled agent may have already
32
+ * edited files in the workspace, and those edits stay. Cancelling stops
33
+ * further work; it is not a rollback.
34
+ *
35
+ * A cancelled run is deliberately not recorded as a failure: the route's
36
+ * circuit breaker and failure count never see it, because the caller changing
37
+ * their mind says nothing about whether the route works.
38
+ */
39
+ export async function cancelJob(jobId, reason) {
40
+ const job = await getAsyncJob(jobId); // throws the friendly "No such job" for a stranger
41
+ const current = job.status.status;
42
+ // There are TWO kinds of orphaned job and they need opposite answers.
43
+ //
44
+ // WRITTEN — `drainSlotQueue` marks a slot-queued job orphaned on disk when
45
+ // the server exits before it ever starts. That job is genuinely
46
+ // terminal: its own error text says it is not resumed automatically and
47
+ // to use retry_job. Cancelling it is a no-op that would leave a marker
48
+ // nothing reads.
49
+ // DERIVED — `withOrphanCheck` reports a job orphaned when its heartbeat
50
+ // goes stale, while the FILE still says `queued` or `running`. That job
51
+ // is not inert: once the dead owner's claim ages out, claimNextJob will
52
+ // pick it up and run it. Answering "had already finished; nothing to
53
+ // cancel" was wrong about work that could still start, and left the
54
+ // caller no way to stop it.
55
+ //
56
+ // So the raw status decides, not the derived one. `getAsyncJob` has already
57
+ // applied the orphan check, which is why this re-reads the file.
58
+ const rawStatus = await readJson(path.join(jobsRoot(), jobId, "status.json")).catch(() => undefined);
59
+ const terminalOnDisk = rawStatus?.status === "orphaned";
60
+ if (current === "completed" ||
61
+ current === "failed" ||
62
+ current === "cancelled" ||
63
+ terminalOnDisk) {
64
+ return {
65
+ jobId,
66
+ outcome: "already_finished",
67
+ status: current,
68
+ message: `Job ${jobId} had already finished (${current}); nothing to cancel.`,
69
+ };
70
+ }
71
+ const jobDir = path.join(jobsRoot(), jobId);
72
+ await requestCancel(jobDir, reason);
73
+ // A job still waiting for a slot has no runner to notice the marker, so
74
+ // stop it here. claimNextJob also refuses to claim a marked job, which
75
+ // closes the window where a supervisor picks it up between these two steps.
76
+ // `orphaned` joins `queued` here rather than falling through to "the runner
77
+ // will notice the marker": an orphaned job has no live runner BY
78
+ // DEFINITION, so nothing would ever act on the marker and the job would sit
79
+ // at "cancelling" forever. The marker written above still matters — it is
80
+ // what stops a supervisor reclaiming it — but the status has to be settled
81
+ // here, by the only process still involved.
82
+ if (current === "queued" || current === "orphaned") {
83
+ await updateStatus(jobDir, {
84
+ ...job.status,
85
+ status: "cancelled",
86
+ updatedAt: timestamp(),
87
+ success: false,
88
+ error: reason !== undefined ? `Cancelled: ${reason}` : "Cancelled before it started.",
89
+ });
90
+ // Deliberately NOT draining the slot queue here. Freeing this job's slot
91
+ // makes room for a waiting one, but drainSlotQueue can SPAWN supervisor
92
+ // processes, and a cancel — the operation whose whole point is to stop
93
+ // work — must not start any. Every dispatch and every runner exit already
94
+ // drains, which is the same "resumes on the next event" contract the
95
+ // queue documents elsewhere.
96
+ return {
97
+ jobId,
98
+ outcome: "cancelled",
99
+ status: "cancelled",
100
+ message: current === "orphaned"
101
+ ? `Job ${jobId} was orphaned — the process running it is gone — and is now ` +
102
+ `marked cancelled, so no supervisor can reclaim it. Any partial output it ` +
103
+ `wrote is still available from job_status.`
104
+ : `Job ${jobId} was waiting for a slot and has been cancelled; it never started.`,
105
+ };
106
+ }
107
+ return {
108
+ jobId,
109
+ outcome: "cancelling",
110
+ status: current,
111
+ message: `Cancellation requested for ${jobId}. The run stops within a second or so — poll ` +
112
+ `job_status to confirm. Any files the agent already changed are NOT reverted.`,
113
+ };
114
+ }
115
+ /**
116
+ * Inspect or resolve the isolated workspace a finished job left behind.
117
+ *
118
+ * Looks the job up the same way job_status does, then hands off to
119
+ * workspace-resolve.ts. Kept here so the caller only ever needs a jobId —
120
+ * where the workspace lives, and which policy produced it, are details
121
+ * recorded in the job's own result.
122
+ */
123
+ export async function resolveJobWorkspace(jobId, action, opts = {}) {
124
+ const job = await getAsyncJob(jobId);
125
+ const run = job.result?.result?.workspace;
126
+ if (!isResolvable(run)) {
127
+ // Read through a separate binding: the type guard narrows `run` to never
128
+ // on this branch, which would make the diagnostic unable to say WHICH
129
+ // policy the caller actually got.
130
+ const raw = job.result?.result?.workspace;
131
+ const policy = raw?.policy ?? "shared";
132
+ throw new Error(`Job ${jobId} has no isolated workspace to ${action} (workspace policy: ${policy}). ` +
133
+ `Only 'copy' and 'git_worktree' dispatches produce one — a 'shared' or ` +
134
+ `'shared_locked' run edited ${raw?.originalWorkingDir ?? "the working directory"} ` +
135
+ `directly, so there is nothing separate to inspect, apply or throw away.`);
136
+ }
137
+ const jobDir = path.join(jobsRoot(), jobId);
138
+ if (action === "diff")
139
+ return workspaceDiff(jobId, jobDir, run);
140
+ if (action === "apply")
141
+ return applyWorkspace(jobId, jobDir, run, opts);
142
+ // force reaches discard too: it now refuses to destroy work the project
143
+ // does not have, and the caller needs the same override apply offers.
144
+ return discardWorkspace(jobId, run, opts);
145
+ }
146
+ /**
147
+ * Run a finished job's task again.
148
+ *
149
+ * The last verb missing from the job lifecycle: you could start work, watch
150
+ * it, stop it, and resolve its workspace — but if it failed, reproducing it
151
+ * meant reconstructing the prompt, the file list, the working directory and
152
+ * the hints by hand, from a job record that already holds all four. The
153
+ * machinery to execute a job bundle existed (executeJobDir) and simply was
154
+ * not reachable from outside.
155
+ *
156
+ * The prompt is taken from prompt.md, which is the FROZEN prompt — including
157
+ * any context preamble the original dispatch rendered in. A retry therefore
158
+ * reproduces what the delegate actually saw, not what the caller typed.
159
+ *
160
+ * `service` retargets the attempt, which is the common case rather than an
161
+ * afterthought: the reason a run failed is often the route, not the task
162
+ * ("codex hit its usage limit — try claude"). Omit it to reuse the original
163
+ * route, or to let the router pick again if the original had none.
164
+ */
165
+ export async function retryJob(jobId, deps, opts = {}) {
166
+ const prior = await getAsyncJob(jobId); // friendly "No such job" for a stranger
167
+ const state = prior.status.status;
168
+ if (state === "running" || state === "queued") {
169
+ throw new Error(`Job ${jobId} is still ${state}. Let it finish, or cancel it first with ` +
170
+ `cancel_job — retrying a live run would leave two attempts racing on the ` +
171
+ `same working directory.`);
172
+ }
173
+ // A DERIVED orphan produces exactly the outcome the message above forbids,
174
+ // and this guard could not see it.
175
+ //
176
+ // `getAsyncJob` reports orphaned when the heartbeat is stale, but the status
177
+ // FILE still says `queued`, and `claimNextJob` filters on the raw file — so
178
+ // a supervisor can still claim the original while the retry runs. The guard
179
+ // reads the derived status, which is neither `running` nor `queued`, so it
180
+ // let the retry through. `cancelJob` was given this reasoning when it learnt
181
+ // to tell the two kinds of orphan apart; retry was not, so the fix stopped
182
+ // one square short.
183
+ //
184
+ // Marking the original cancelled is what closes it: claimNextJob refuses a
185
+ // marked job, so the retry becomes the only attempt. A job orphaned while
186
+ // RUNNING has no live runner either, so the marker is equally correct there
187
+ // and simply has nothing left to interrupt.
188
+ if (state === "orphaned") {
189
+ const rawStatus = await readJson(path.join(jobsRoot(), jobId, "status.json")).catch(() => undefined);
190
+ if (rawStatus?.status === "queued" || rawStatus?.status === "running") {
191
+ await cancelJob(jobId, `superseded by a retry`);
192
+ }
193
+ }
194
+ const manifest = prior.manifest;
195
+ const prompt = await readFile(manifest.promptPath, "utf8");
196
+ if (opts.service !== undefined && !(opts.service in deps.holder.state.config.services)) {
197
+ throw new Error(`Unknown service: ${opts.service}. Valid route ids: ` +
198
+ `${Object.keys(deps.holder.state.config.services).join(", ")}.`);
199
+ }
200
+ const service = opts.service ?? manifest.service;
201
+ const { hints, droppedModel } = hintsForRetry(manifest.hints, prior.status.route ?? manifest.service, service, deps.holder.state.config.services);
202
+ const { status } = await startAsyncJobTracked(deps, {
203
+ prompt,
204
+ files: manifest.files.map((f) => f.originalPath),
205
+ workingDir: manifest.workingDir,
206
+ retryOf: jobId,
207
+ ...(hints !== undefined ? { hints } : {}),
208
+ ...(manifest.workspacePolicy !== undefined
209
+ ? { workspacePolicy: manifest.workspacePolicy }
210
+ : {}),
211
+ ...(service !== undefined ? { service } : {}),
212
+ });
213
+ return {
214
+ jobId: status.jobId,
215
+ retryOf: jobId,
216
+ ...(service !== undefined ? { service } : {}),
217
+ ...(droppedModel !== undefined ? { droppedModel } : {}),
218
+ reusedFrom: {
219
+ prompt: true,
220
+ files: manifest.files.length,
221
+ workingDir: manifest.workingDir,
222
+ },
223
+ message: `Started ${status.jobId} from ${jobId}'s prompt, files and working directory` +
224
+ `${opts.service !== undefined ? `, retargeted to ${opts.service}` : ""}. ` +
225
+ `${droppedModel !== undefined
226
+ ? `Left behind the original's model "${droppedModel}", which ${service} does ` +
227
+ `not declare — a model name belongs to the route it was chosen for. Pass ` +
228
+ `hints.model on a fresh dispatch if you want a specific model here. `
229
+ : ""}` +
230
+ `Check it with job_status; the original job is untouched.`,
231
+ };
232
+ }
233
+ /**
234
+ * Carry the original's hints into a retry — except a model that belonged to
235
+ * the route being left behind.
236
+ *
237
+ * Retrying somewhere else is the documented reason this tool exists ("the task
238
+ * was fine and the route was not"), and reusing the model verbatim defeated
239
+ * exactly that case: model names are route-scoped, so the retry failed for the
240
+ * same reason as the original. Observed end to end — a Cursor run that died on
241
+ * `Cannot use this model` was retried onto Claude and died on
242
+ * `unrecognized_model`, having never reached the task.
243
+ *
244
+ * Narrow on purpose. The model is kept when the retry stays on the original's
245
+ * route (that is a plain "try again"), and when the new route declares it
246
+ * anyway. Only a model the destination does not know is dropped, and the
247
+ * caller is told — a silently changed model is the failure this project keeps
248
+ * finding, so it is reported in the response rather than inferred from a
249
+ * different result.
250
+ */
251
+ function hintsForRetry(hints, priorRoute, service, services) {
252
+ if (hints === undefined)
253
+ return { hints };
254
+ const model = hints.model;
255
+ if (model === undefined || service === undefined || service === priorRoute)
256
+ return { hints };
257
+ const target = services[service];
258
+ if (target === undefined || declaresModel(target, model))
259
+ return { hints };
260
+ const { model: _dropped, ...rest } = hints;
261
+ return { hints: rest, droppedModel: model };
262
+ }
263
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../src/jobs/lifecycle.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Reading a job back: one with its partial output, or the recent list.
3
+ *
4
+ * Separate from start.ts because the supervisor reads jobs while deciding
5
+ * what to run, and start.ts needs the supervisor — keeping the reads beside
6
+ * the start verbs closed that loop.
7
+ */
8
+ import type { JobManifest, JobResultPayload, JobStatus } from "./types.js";
9
+ export declare function getAsyncJob(jobId: string): Promise<{
10
+ manifest: JobManifest;
11
+ status: JobStatus;
12
+ result?: JobResultPayload;
13
+ /** Tail of live stdout/stderr while the job is still running. */
14
+ partialOutput?: string;
15
+ }>;
16
+ export declare function listAsyncJobs(): Promise<JobStatus[]>;
17
+ //# sourceMappingURL=read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../src/jobs/read.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG3E,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACxD,QAAQ,EAAE,WAAW,CAAC;IACtB,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,iEAAiE;IACjE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC,CAsED;AAED,wBAAsB,aAAa,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAgB1D"}