@gilbertgt/dsh-plan-orchestrator 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { A as validatePlanArtifact, C as git, D as normalizeOwnedPath, E as extractPlanArtifact, O as parseValidationCommand, S as fullHead, _ as readJson, b as changedPaths, c as trustedIssueTexts, d as hashBytes, f as assertOwnedPaths, g as confined, h as atomicJson, i as ghRepository, k as schedulerPathIdentity, l as assertTrustedReceipts, m as RunStore, n as ghPreflight, o as prepareIssueBranch, p as disjointOwnership, s as remotePr, t as fetchIssue, u as receiptIndex, v as stateRoot, w as repoRoot, x as decodeUtf8Strict, y as assertRepoPathsConfined } from "./github-Cy4Pm35G.js";
2
- import { n as validateFixedRoute, r as eligibleTransportFailure, t as installPlannerRoute } from "./planner-route-B9z5GziX.js";
2
+ import { n as validateFixedRoute, r as eligibleTransportFailure, t as installPlannerRoute } from "./planner-route-Z9MMuA1e.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { execFile } from "node:child_process";
5
5
  import { promisify } from "node:util";
@@ -862,6 +862,23 @@ const defaultRoute = () => ({
862
862
  mode: "current",
863
863
  fallbacks: []
864
864
  });
865
+ /**
866
+ * Rebuild one route choice with every optional field present only when it holds
867
+ * a real value. Every DSH boundary this plugin crosses — `Session.append`,
868
+ * subagent descriptor snapshots, `llm.resolveCallConfig` and SDK child options —
869
+ * validates lossless JSON, where an explicitly `undefined` property is rejected
870
+ * while an absent one is fine. An unresolved (inherited) route legitimately has
871
+ * no provider/model yet, so those are omitted too rather than written as
872
+ * `undefined`; `routeChoices` filters such a candidate out before use.
873
+ */
874
+ function losslessRouteChoice(choice) {
875
+ const route = {};
876
+ if (choice.provider !== void 0) route.provider = choice.provider;
877
+ if (choice.model !== void 0) route.model = choice.model;
878
+ if (choice.reasoningEffort !== void 0) route.reasoningEffort = choice.reasoningEffort;
879
+ if (choice.maxTokens !== void 0) route.maxTokens = choice.maxTokens;
880
+ return route;
881
+ }
865
882
  const DEFAULT_SETTINGS = Object.freeze({
866
883
  enabled: true,
867
884
  roles: {
@@ -1311,6 +1328,39 @@ var PlannerReadOnlyGuard = class {
1311
1328
  function isPlanxEvent(event) {
1312
1329
  return Boolean(event && typeof event.type === "string" && event.type.startsWith("planx/") && event.data && typeof event.data.runId === "string");
1313
1330
  }
1331
+ /**
1332
+ * Reject the payload shapes this plugin is known to produce accidentally.
1333
+ *
1334
+ * This is deliberately NOT a reimplementation of the DSH lossless-JSON
1335
+ * validator: it catches an explicitly `undefined` optional property (the shape
1336
+ * that caused the PREFLIGHT wedge), plus non-finite numbers, non-JSON
1337
+ * primitives, circular references, sparse arrays, symbol keys and non-plain
1338
+ * objects. It does not claim to match every DSH rule. `Session.append` remains
1339
+ * the authoritative boundary; this guard exists only so a bad payload fails
1340
+ * earlier with a path that names the offending field.
1341
+ */
1342
+ function assertNoUndefinedEventData(value, label) {
1343
+ const seen = /* @__PURE__ */ new Set();
1344
+ const walk = (node, path) => {
1345
+ if (node === void 0) throw new Error(`${label} carries an undefined value at ${path}; omit the optional property instead`);
1346
+ if (typeof node === "number" && !Number.isFinite(node)) throw new Error(`${label} carries a non-finite number at ${path}`);
1347
+ if (typeof node === "bigint" || typeof node === "function" || typeof node === "symbol") throw new Error(`${label} carries a non-JSON ${typeof node} at ${path}`);
1348
+ if (typeof node !== "object" || node === null) return;
1349
+ if (seen.has(node)) throw new Error(`${label} carries a circular reference at ${path}`);
1350
+ seen.add(node);
1351
+ if (Array.isArray(node)) {
1352
+ if (node.length !== Object.keys(node).length) throw new Error(`${label} carries a sparse array at ${path}`);
1353
+ node.forEach((item, index) => walk(item, `${path}[${index}]`));
1354
+ } else {
1355
+ const prototype = Object.getPrototypeOf(node);
1356
+ if (prototype !== Object.prototype && prototype !== null) throw new Error(`${label} carries a non-plain object at ${path}`);
1357
+ if (Object.getOwnPropertySymbols(node).length) throw new Error(`${label} carries a symbol key at ${path}`);
1358
+ for (const [key, item] of Object.entries(node)) walk(item, `${path}.${key}`);
1359
+ }
1360
+ seen.delete(node);
1361
+ };
1362
+ walk(value, label);
1363
+ }
1314
1364
  function reducePlanxEvents(sessionId, events) {
1315
1365
  const rows = /* @__PURE__ */ new Map();
1316
1366
  for (const event of events) {
@@ -1523,6 +1573,28 @@ async function cleanupRunWorktrees(repo, runId, root = stateRoot()) {
1523
1573
  }
1524
1574
  //#endregion
1525
1575
  //#region src/orchestration/service.ts
1576
+ const TERMINAL_PHASES = /* @__PURE__ */ new Set([
1577
+ "COMPLETE",
1578
+ "BLOCKED",
1579
+ "FAILED",
1580
+ "INTERRUPTED",
1581
+ "CANCELLED"
1582
+ ]);
1583
+ /** Runner errors matching this shape are safety boundaries, not plain faults. */
1584
+ const BLOCKING_FAILURE = /(drift|ownership|blocked|inconclusive|unsafe|conflict|escape|checkpoint|tampered|stale)/i;
1585
+ function failureMessage(error) {
1586
+ if (error instanceof Error && error.message) return error.message;
1587
+ if (typeof error === "string" && error) return error;
1588
+ try {
1589
+ const encoded = JSON.stringify(error);
1590
+ if (encoded && encoded !== "{}" && encoded !== "null") return encoded;
1591
+ } catch {}
1592
+ return "unknown orchestration failure";
1593
+ }
1594
+ function terminalPhaseFor(aborted, message) {
1595
+ if (aborted) return "CANCELLED";
1596
+ return BLOCKING_FAILURE.test(message) ? "BLOCKED" : "FAILED";
1597
+ }
1526
1598
  var OrchestrationService = class {
1527
1599
  store;
1528
1600
  runner;
@@ -1618,7 +1690,7 @@ var OrchestrationService = class {
1618
1690
  this.append(session, "planx/run-phase", {
1619
1691
  runId,
1620
1692
  phase: view.phase,
1621
- message: data.message,
1693
+ ...data.message !== void 0 ? { message: String(data.message) } : {},
1622
1694
  reviewRound: view.reviewRound,
1623
1695
  at: now
1624
1696
  });
@@ -1704,7 +1776,15 @@ var OrchestrationService = class {
1704
1776
  return false;
1705
1777
  }
1706
1778
  if (pending.cancelled || this.#pending.get(sessionId) !== pending) return false;
1707
- const started = await this.start(pending.launch, pending.runId);
1779
+ let started;
1780
+ try {
1781
+ started = await this.start(pending.launch, pending.runId);
1782
+ } catch (error) {
1783
+ const message = `maintenance launch failed: ${failureMessage(error)}`;
1784
+ if (this.#pending.get(sessionId) === pending) this.#pending.delete(sessionId);
1785
+ await this.finalize(pending.launch, pending.runId, terminalPhaseFor(false, message), message).catch(() => {});
1786
+ return false;
1787
+ }
1708
1788
  if (pending.cancelled) {
1709
1789
  if (this.#pending.get(sessionId) === pending) this.#pending.delete(sessionId);
1710
1790
  return false;
@@ -1793,7 +1873,15 @@ var OrchestrationService = class {
1793
1873
  message: `Safe resume from ${diagnosis.resumeFrom}`,
1794
1874
  at: now
1795
1875
  });
1796
- await this.start(launch, runId);
1876
+ try {
1877
+ await this.start(launch, runId);
1878
+ } catch (error) {
1879
+ await this.finalize(launch, runId, terminalPhaseFor(false, failureMessage(error)), failureMessage(error));
1880
+ return {
1881
+ ok: false,
1882
+ reason: failureMessage(error)
1883
+ };
1884
+ }
1797
1885
  return {
1798
1886
  ok: true,
1799
1887
  resumeFrom: diagnosis.resumeFrom
@@ -1837,40 +1925,103 @@ var OrchestrationService = class {
1837
1925
  return true;
1838
1926
  }
1839
1927
  async run(launch, runId, signal) {
1840
- const initial = await this.store.readManifest(launch.sessionId, runId);
1841
- initial.phase = launch.resumeFrom ?? "PREFLIGHT";
1842
- initial.terminal = false;
1843
- await this.store.writeManifest(initial);
1844
- this.recordEvent("phase", {
1845
- runId,
1846
- phase: initial.phase
1847
- });
1848
1928
  let terminalPhase = "COMPLETE";
1849
1929
  let terminalMessage;
1850
1930
  let thrown;
1931
+ let failed = false;
1932
+ try {
1933
+ try {
1934
+ const initial = await this.store.readManifest(launch.sessionId, runId);
1935
+ initial.phase = launch.resumeFrom ?? "PREFLIGHT";
1936
+ initial.terminal = false;
1937
+ await this.store.writeManifest(initial);
1938
+ this.recordEvent("phase", {
1939
+ runId,
1940
+ phase: initial.phase
1941
+ });
1942
+ } catch (error) {
1943
+ thrown = error;
1944
+ failed = true;
1945
+ terminalMessage = failureMessage(error);
1946
+ terminalPhase = terminalPhaseFor(signal.aborted, terminalMessage);
1947
+ }
1948
+ if (!failed) try {
1949
+ await this.runner(launch, runId, signal);
1950
+ if (signal.aborted && terminalPhase === "COMPLETE") {
1951
+ terminalMessage = "run aborted before completion";
1952
+ terminalPhase = "CANCELLED";
1953
+ }
1954
+ } catch (error) {
1955
+ thrown = error;
1956
+ failed = true;
1957
+ terminalMessage = failureMessage(error);
1958
+ terminalPhase = terminalPhaseFor(signal.aborted, terminalMessage);
1959
+ }
1960
+ } finally {
1961
+ await this.finalize(launch, runId, terminalPhase, terminalMessage);
1962
+ }
1963
+ if (failed) throw thrown;
1964
+ }
1965
+ /**
1966
+ * Best-effort terminal manifest write. Returns the failure instead of
1967
+ * throwing so the caller can decide how to converge.
1968
+ */
1969
+ async writeTerminal(launch, runId, phase) {
1851
1970
  try {
1852
- await this.runner(launch, runId, signal);
1971
+ const latest = await this.store.readManifest(launch.sessionId, runId);
1972
+ latest.phase = phase;
1973
+ latest.terminal = true;
1974
+ await this.store.writeManifest(latest);
1975
+ return { ok: true };
1853
1976
  } catch (error) {
1854
- thrown = error;
1855
- terminalMessage = error.message;
1856
- terminalPhase = signal.aborted ? "CANCELLED" : /(drift|ownership|blocked|inconclusive|unsafe|conflict|escape|checkpoint|tampered|stale)/i.test(terminalMessage) ? "BLOCKED" : "FAILED";
1977
+ return {
1978
+ ok: false,
1979
+ error
1980
+ };
1857
1981
  }
1858
- const latest = await this.store.readManifest(launch.sessionId, runId);
1859
- latest.phase = terminalPhase;
1860
- latest.terminal = true;
1861
- await this.store.writeManifest(latest);
1862
- this.recordEvent("phase", {
1863
- runId,
1864
- phase: terminalPhase,
1865
- message: terminalMessage
1866
- });
1867
- this.append(launch.agent.session, "planx/run-terminal", {
1868
- runId,
1869
- phase: terminalPhase,
1870
- ...terminalMessage ? { message: terminalMessage } : {},
1871
- at: (/* @__PURE__ */ new Date()).toISOString()
1872
- });
1873
- if (thrown !== void 0) throw thrown;
1982
+ }
1983
+ /**
1984
+ * Persist the terminal state. Never throws: a session whose append is
1985
+ * unavailable must still not be left with a non-terminal manifest.
1986
+ */
1987
+ async finalize(launch, runId, phase, message) {
1988
+ if (!TERMINAL_PHASES.has(phase)) throw new Error(`plan-orchestrator refusing non-terminal finalize phase: ${phase}`);
1989
+ const at = (/* @__PURE__ */ new Date()).toISOString();
1990
+ let effectivePhase = phase;
1991
+ let effectiveMessage = message;
1992
+ const written = await this.writeTerminal(launch, runId, effectivePhase);
1993
+ if (!written.ok) {
1994
+ effectivePhase = "FAILED";
1995
+ effectiveMessage = `terminal manifest persistence failed: ${failureMessage(written.error)}`;
1996
+ const retry = await this.writeTerminal(launch, runId, effectivePhase);
1997
+ if (!retry.ok) effectiveMessage = `${effectiveMessage}; retry failed: ${failureMessage(retry.error)}`;
1998
+ try {
1999
+ launch.agent?.session?.append?.("planx/finalize-error", {
2000
+ runId,
2001
+ phase: effectivePhase,
2002
+ message: effectiveMessage,
2003
+ at
2004
+ });
2005
+ } catch {}
2006
+ try {
2007
+ this.failView(runId, "FAILED", effectiveMessage, at);
2008
+ } catch {}
2009
+ }
2010
+ try {
2011
+ this.recordEvent("phase", {
2012
+ runId,
2013
+ phase: effectivePhase,
2014
+ ...effectiveMessage !== void 0 ? { message: effectiveMessage } : {}
2015
+ });
2016
+ } catch {}
2017
+ try {
2018
+ this.append(launch.agent.session, "planx/run-terminal", {
2019
+ runId,
2020
+ phase: effectivePhase,
2021
+ ...effectiveMessage ? { message: effectiveMessage } : {},
2022
+ at
2023
+ });
2024
+ } catch {}
1874
2025
  }
1875
2026
  async persistApproval(launch, runId, now) {
1876
2027
  const dir = this.store.runDir(launch.sessionId, runId);
@@ -1918,21 +2069,24 @@ var OrchestrationService = class {
1918
2069
  }
1919
2070
  append(session, type, data) {
1920
2071
  if (!session || typeof session.append !== "function") throw new Error(`plan-orchestrator cannot persist ${type}: session append unavailable`);
2072
+ assertNoUndefinedEventData(data, type);
1921
2073
  session.append(type, data);
1922
2074
  }
1923
- failView(runId, phase, message) {
2075
+ failView(runId, phase, message, at = (/* @__PURE__ */ new Date()).toISOString()) {
1924
2076
  const view = this.#views.get(runId);
1925
2077
  if (!view) return;
1926
2078
  view.phase = phase;
1927
2079
  view.status = phase;
1928
2080
  view.message = message;
1929
- view.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1930
- this.append(this.#agents.get(view.sessionId)?.session, "planx/run-terminal", {
1931
- runId,
1932
- phase,
1933
- message,
1934
- at: view.updatedAt
1935
- });
2081
+ view.updatedAt = at;
2082
+ try {
2083
+ this.append(this.#agents.get(view.sessionId)?.session, "planx/run-terminal", {
2084
+ runId,
2085
+ phase,
2086
+ message,
2087
+ at
2088
+ });
2089
+ } catch {}
1936
2090
  }
1937
2091
  async latestReview(dir, round) {
1938
2092
  for (let index = round; index >= 0; index--) {
@@ -2607,9 +2761,9 @@ function routeChoices(route, current) {
2607
2761
  return [route.mode === "current" ? current : {
2608
2762
  provider: route.provider,
2609
2763
  model: route.model,
2610
- reasoningEffort: route.reasoningEffort,
2611
- maxTokens: route.maxTokens
2612
- }, ...route.fallbacks].filter((v, i, a) => v.provider && v.model && a.findIndex((x) => x.provider === v.provider && x.model === v.model && x.reasoningEffort === v.reasoningEffort) === i);
2764
+ ...route.reasoningEffort !== void 0 ? { reasoningEffort: route.reasoningEffort } : {},
2765
+ ...route.maxTokens !== void 0 ? { maxTokens: route.maxTokens } : {}
2766
+ }, ...route.fallbacks].map(losslessRouteChoice).filter((v, i, a) => v.provider && v.model && a.findIndex((x) => x.provider === v.provider && x.model === v.model && x.reasoningEffort === v.reasoningEffort) === i);
2613
2767
  }
2614
2768
  async function preflightRoute(llm, choice) {
2615
2769
  if (typeof llm?.resolveCallConfig !== "function") throw new Error("llm.resolveCallConfig unavailable");
@@ -2747,12 +2901,25 @@ function aggregateUsage(samples) {
2747
2901
  //#endregion
2748
2902
  //#region src/orchestration/engine.ts
2749
2903
  const unionOwnership = (plan) => [...new Set(plan.tasks.flatMap((task) => task.modify))].sort();
2750
- const currentRoute = (agent) => ({
2751
- provider: agent.options?.provider,
2752
- model: agent.options?.model,
2753
- reasoningEffort: agent.options?.reasoningEffort,
2754
- maxTokens: agent.options?.maxTokens
2755
- });
2904
+ /**
2905
+ * Absent route fields must not exist at all. An explicit `reasoningEffort:
2906
+ * undefined` is rejected by every DSH lossless-JSON boundary (subagent
2907
+ * descriptors, the session log, SDK child options), so each property is
2908
+ * constructed only when the live agent actually carries a value — including
2909
+ * provider/model, which an unresolved inherited agent legitimately lacks.
2910
+ */
2911
+ const currentRoute = (agent) => {
2912
+ const provider = agent?.options?.provider;
2913
+ const model = agent?.options?.model;
2914
+ const reasoningEffort = agent?.options?.reasoningEffort;
2915
+ const maxTokens = agent?.options?.maxTokens;
2916
+ return {
2917
+ ...provider !== void 0 ? { provider: String(provider) } : {},
2918
+ ...model !== void 0 ? { model: String(model) } : {},
2919
+ ...reasoningEffort !== void 0 ? { reasoningEffort: String(reasoningEffort) } : {},
2920
+ ...maxTokens !== void 0 ? { maxTokens: Number(maxTokens) } : {}
2921
+ };
2922
+ };
2756
2923
  async function resolvedChoices(ctx, settings, role, agent) {
2757
2924
  const candidates = routeChoices(settings.roles[role], currentRoute(agent));
2758
2925
  const good = [];
@@ -3715,7 +3882,7 @@ function apply(ctx) {
3715
3882
  c.inject(["connection"], (scope) => scope.effect(() => {
3716
3883
  let disposed = false;
3717
3884
  let disposeTransport;
3718
- import("./rpc-server-CGs2ndXP.js").then(({ registerRpc }) => {
3885
+ import("./rpc-server-Bdtz9NqI.js").then(({ registerRpc }) => {
3719
3886
  if (disposed) return;
3720
3887
  disposeTransport = registerRpc(scope.connection, {
3721
3888
  ctx: scope,
@@ -11,10 +11,10 @@ async function validateFixedRoute(llm, route) {
11
11
  if (!route.provider || !route.model) throw new Error("fixed route needs provider and model");
12
12
  const requested = {
13
13
  provider: route.provider,
14
- model: route.model,
15
- ...route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {},
16
- ...route.maxTokens ? { maxTokens: route.maxTokens } : {}
14
+ model: route.model
17
15
  };
16
+ if (route.reasoningEffort !== void 0) requested.reasoningEffort = route.reasoningEffort;
17
+ if (route.maxTokens !== void 0) requested.maxTokens = route.maxTokens;
18
18
  if (typeof llm?.resolveCallConfig !== "function") throw new Error("DSH llm.resolveCallConfig is unavailable");
19
19
  await llm.resolveCallConfig(requested);
20
20
  for (const fallback of route.fallbacks) await llm.resolveCallConfig(fallback);
@@ -27,15 +27,15 @@ function routeAt(route, current, index) {
27
27
  return {
28
28
  provider: current.provider,
29
29
  model: current.model,
30
- reasoningEffort: current.reasoningEffort,
31
- maxTokens: current.maxTokens
30
+ ...current.reasoningEffort !== void 0 ? { reasoningEffort: current.reasoningEffort } : {},
31
+ ...current.maxTokens !== void 0 ? { maxTokens: current.maxTokens } : {}
32
32
  };
33
33
  }
34
34
  return {
35
35
  provider: route.provider,
36
36
  model: route.model,
37
- reasoningEffort: route.reasoningEffort,
38
- maxTokens: route.maxTokens
37
+ ...route.reasoningEffort !== void 0 ? { reasoningEffort: route.reasoningEffort } : {},
38
+ ...route.maxTokens !== void 0 ? { maxTokens: route.maxTokens } : {}
39
39
  };
40
40
  }
41
41
  return route.fallbacks[index - 1];
@@ -51,12 +51,13 @@ function installPlannerRoute(ctx, getRoute, isPlanActive, isEnabled = () => true
51
51
  const selected = routeAt(getRoute(agent), current, fallbackIndex.get(agent) ?? 0);
52
52
  if (!selected) return current;
53
53
  await ctx.llm.resolveCallConfig(selected);
54
+ const { reasoningEffort: _inheritedEffort, maxTokens: inheritedMaxTokens, ...rest } = current;
54
55
  return {
55
- ...current,
56
+ ...rest,
56
57
  provider: selected.provider,
57
58
  model: selected.model,
58
- reasoningEffort: selected.reasoningEffort,
59
- maxTokens: selected.maxTokens ?? current.maxTokens
59
+ ...selected.reasoningEffort !== void 0 ? { reasoningEffort: selected.reasoningEffort } : {},
60
+ ...selected.maxTokens !== void 0 ? { maxTokens: selected.maxTokens } : inheritedMaxTokens !== void 0 ? { maxTokens: inheritedMaxTokens } : {}
60
61
  };
61
62
  });
62
63
  const assembly = ctx.on("system-prompt/assemble", async (assembled, context, next) => {
@@ -1,4 +1,4 @@
1
- import { n as validateFixedRoute } from "./planner-route-B9z5GziX.js";
1
+ import { n as validateFixedRoute } from "./planner-route-Z9MMuA1e.js";
2
2
  //#region src/contract/rpc.ts
3
3
  const RPC_METHODS = [
4
4
  "model-catalog",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilbertgt/dsh-plan-orchestrator",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Product-grade orchestration layer for DeepSeek Harness native Plan Mode",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",