@try-works/dsh-recursive-mode 0.4.7 → 0.4.8

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/README.md CHANGED
@@ -386,10 +386,17 @@ from `session/event`, with a cheap shape test first, because that event fires fo
386
386
  observer **waits** — bounded, polling, with a timeout — and reports "no settlement yet" only after actually
387
387
  waiting. The park remains as the fallback, so an unobserved round is never an approval.
388
388
 
389
- **Action records are written for every delegation**, and they carry `Execution Mode` and `Status`. After a
390
- failed delegation, they now also carry **`Failure:`** — because `Status: failed` on its own cannot distinguish a
391
- child that ran and failed from a child that never started. That distinction cost three rounds of investigation
392
- before the field existed.
389
+ **Action records are written for every delegation**, and they carry `Execution Mode` and a `Status` with
390
+ **three** values, not two: `accepted`, `failed`, and `parked (still running; no settlement yet)`. The third
391
+ state exists because a round whose child had not settled is **not** a failure — the delegation interface says
392
+ so, and `recursive_review` already reports it as `parked` — and a binary status forced it to read as one: a live
393
+ run's record said `Status: failed` with *"the child never reported, or never ran"*, the main agent concluded its
394
+ reviewer was dead and obtained the review another way, and the child replied eighteen minutes later. A `failed`
395
+ record carries **`Failure:`** with its cause; a parked record carries **`Parked:`** instead — stating only what
396
+ is known ("no settlement had landed when the wait ended … the child may still be working"), naming the
397
+ `childId`, and saying that resuming with that id is the next step. `operations/operations.jsonl` records the
398
+ same distinction (`parked` rather than `unaccepted`), because the operation log had the identical conflation.
399
+ That distinction cost three rounds of investigation before the third state existed.
393
400
 
394
401
  ---
395
402
 
@@ -336,9 +336,40 @@ export interface ActionRecordInput {
336
336
  * nothing else: a delegation that FAILED and a delegation that NEVER HAPPENED read identically, which is what
337
337
  * let me conclude for three rounds that the host was not scheduling children. The caller ALREADY passed
338
338
  * `stopReason`, and the live record said `n/a` — because there was no result to take a stop reason from.
339
+ *
340
+ * ⚠ AND IT IS EMITTED ONLY FOR A GENUINE FAILURE. A PARKED round is neither accepted nor failed, so it
341
+ * carries {@link ActionRecordInput.parked} instead: calling a live child a failure is the defect this
342
+ * field's own history is made of.
339
343
  */
340
344
  failure?: string;
345
+ /**
346
+ * ⚠ THE THIRD STATE, AND WHY `success` COULD NOT CARRY IT.
347
+ *
348
+ * A continuable round that has not settled is NOT a failure — `ContinuableDelegationLike.parked` says so in
349
+ * its own doc comment, and the tool result already surfaces it. The RECORD did not: a live run's child was
350
+ * parked, the record said `Status: failed` with "the child never reported, or never ran", the main agent
351
+ * read that as a dead child and obtained the review elsewhere — while the child was still working and
352
+ * replied eighteen minutes later. `success: false` cannot express "still in flight", because a genuinely
353
+ * dead child also produces `success: false`, so a second field is required rather than a cleverer boolean.
354
+ *
355
+ * When true, the record says `parked` and carries a `Parked:` line (never a `Failure:` line) whose text
356
+ * states only what is KNOWN, names the `childId`, and names the resume step. It takes precedence over
357
+ * `success`: an unobserved round is never an acceptance, whatever a caller passes alongside it.
358
+ */
359
+ parked?: boolean;
341
360
  }
361
+ /**
362
+ * The status a record states, in ONE place, because the three states are the fix and a second writer would
363
+ * drift from this one.
364
+ *
365
+ * The wording is chosen for two readers at once. The MODEL reads it to decide whether to resume or to give up
366
+ * and obtain the result another way — the exact decision the live defect got wrong — so the token must not
367
+ * read as a failure and must not need the rest of the document to be understood. A HUMAN reading the run tree
368
+ * months later needs to tell "died" from "still working" at a glance. Hence `parked (still running; no
369
+ * settlement yet)`: a third token rather than a renamed second, qualified with the two facts that separate it
370
+ * from `failed`, and short enough to sit in a status line.
371
+ */
372
+ export declare function actionRecordStatus(input: Pick<ActionRecordInput, 'success' | 'parked'>): string;
342
373
  /**
343
374
  * Write a durable action record under subagents/ in the shape this repo's own
344
375
  * linter accepts (ts-lint.ts lintSubagentActionRecordFile — every top-level .md
@@ -351,7 +382,10 @@ export interface ActionRecordInput {
351
382
  * `Code Refs` strictly inside ## Inputs Provided. The linter resolves each of
352
383
  * those through the heading body, so a field under another heading is not
353
384
  * found at all.
354
- * A success:false attempt is written with a failed status and is NOT accepted.
385
+ * A success:false attempt is written with a failed status and is NOT accepted. A round that PARKED is written
386
+ * with a status of its own (`parked (still running; no settlement yet)`, see {@link actionRecordStatus}) and a
387
+ * `Parked:` line instead of a `Failure:` one, because no settlement is not a death: it is the caller's signal to
388
+ * resume the same child. Only a genuine failure carries `Failure:`.
355
389
  */
356
390
  export declare function writeActionRecord(input: ActionRecordInput): string;
357
391
  /**
package/lib/index.js CHANGED
@@ -8779,7 +8779,7 @@ async function delegateContinuable(input) {
8779
8779
  const observed = await input.awaitRoundResult(childId, lastMessageId);
8780
8780
  if (observed === null) return {
8781
8781
  ok: false,
8782
- reason: "no settlement has landed for round " + (round + 1) + " yet (the child is still working)",
8782
+ reason: "no settlement has landed for round " + (round + 1) + " yet, so the round is PARKED, not failed: the child may still be working. Resume it on a later turn with childId " + String(childId) + " (the `resumeChild` argument) — do not start a second child.",
8783
8783
  childId,
8784
8784
  messageIds,
8785
8785
  rounds,
@@ -8993,6 +8993,21 @@ function validateReferences(root, references) {
8993
8993
  checked
8994
8994
  };
8995
8995
  }
8996
+ /**
8997
+ * The status a record states, in ONE place, because the three states are the fix and a second writer would
8998
+ * drift from this one.
8999
+ *
9000
+ * The wording is chosen for two readers at once. The MODEL reads it to decide whether to resume or to give up
9001
+ * and obtain the result another way — the exact decision the live defect got wrong — so the token must not
9002
+ * read as a failure and must not need the rest of the document to be understood. A HUMAN reading the run tree
9003
+ * months later needs to tell "died" from "still working" at a glance. Hence `parked (still running; no
9004
+ * settlement yet)`: a third token rather than a renamed second, qualified with the two facts that separate it
9005
+ * from `failed`, and short enough to sit in a status line.
9006
+ */
9007
+ function actionRecordStatus(input) {
9008
+ if (input.parked === true) return "parked (still running; no settlement yet)";
9009
+ return input.success ? "accepted" : "failed";
9010
+ }
8996
9011
  function slugify(value) {
8997
9012
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "action";
8998
9013
  }
@@ -9008,7 +9023,10 @@ function slugify(value) {
9008
9023
  * `Code Refs` strictly inside ## Inputs Provided. The linter resolves each of
9009
9024
  * those through the heading body, so a field under another heading is not
9010
9025
  * found at all.
9011
- * A success:false attempt is written with a failed status and is NOT accepted.
9026
+ * A success:false attempt is written with a failed status and is NOT accepted. A round that PARKED is written
9027
+ * with a status of its own (`parked (still running; no settlement yet)`, see {@link actionRecordStatus}) and a
9028
+ * `Parked:` line instead of a `Failure:` one, because no settlement is not a death: it is the caller's signal to
9029
+ * resume the same child. Only a genuine failure carries `Failure:`.
9012
9030
  */
9013
9031
  function writeActionRecord(input) {
9014
9032
  const { root, runId } = input;
@@ -9046,8 +9064,8 @@ function writeActionRecord(input) {
9046
9064
  "- Phase: " + input.phase,
9047
9065
  "- Purpose: " + input.purpose,
9048
9066
  "- Execution Mode: " + input.executionMode,
9049
- "- Status: " + (input.success ? "accepted" : "failed"),
9050
- ...input.success === false && input.failure !== void 0 ? ["- Failure: " + input.failure] : [],
9067
+ "- Status: " + actionRecordStatus(input),
9068
+ ...input.success === false && input.failure !== void 0 ? [input.parked === true ? "- Parked: " + input.failure : "- Failure: " + input.failure] : [],
9051
9069
  "- Stop Reason: " + (input.stopReason ?? "n/a"),
9052
9070
  "- Timestamp: " + (/* @__PURE__ */ new Date()).toISOString(),
9053
9071
  "",
@@ -10827,7 +10845,10 @@ var RecursiveRuntime = class extends Service {
10827
10845
  } else if (continuable.ok && continuable.rounds.length > 0) {
10828
10846
  result = continuable.rounds[continuable.rounds.length - 1].result ?? null;
10829
10847
  if (!result) error = "continuable child produced no final result";
10830
- } else error = continuable.reason ?? "continuable delegation failed";
10848
+ } else {
10849
+ result = continuable.rounds[continuable.rounds.length - 1]?.result ?? null;
10850
+ if (result === null) error = continuable.reason ?? "continuable delegation failed";
10851
+ }
10831
10852
  } else try {
10832
10853
  result = await delegate({
10833
10854
  subagents,
@@ -10852,7 +10873,7 @@ var RecursiveRuntime = class extends Service {
10852
10873
  id: operation,
10853
10874
  act: "delegate-review",
10854
10875
  at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
10855
- outcome: evaluation.accepted ? "accepted" : "unaccepted",
10876
+ outcome: parked ? "parked" : evaluation.accepted ? "accepted" : "unaccepted",
10856
10877
  phase: input.phase
10857
10878
  });
10858
10879
  const actionRecordPath = writeActionRecord({
@@ -10871,7 +10892,8 @@ var RecursiveRuntime = class extends Service {
10871
10892
  findings: evaluation.accepted && result?.structured ? [result.structured?.verdict ?? "accepted"] : void 0,
10872
10893
  success: evaluation.accepted,
10873
10894
  stopReason: result?.stopReason,
10874
- failure: evaluation.accepted ? void 0 : result == null ? input.mode !== "one-shot" ? "the continuable start was made and NO SETTLEMENT arrived within the wait (the child never reported, or never ran); tier " + decision.tier + ", provider " + (decision.provider ?? "none chosen") + ", names on offer [" + (this.lastProviderNames.join(", ") || "none") + "]; parent id " + (input.parent?.id ?? "none") + ", parent session keys [" + (input.parent === void 0 ? "no parent" : Object.keys(input.parent).join(", ")) + "]" : "no delegate result was produced by the one-shot path; tier " + decision.tier + ", provider " + (decision.provider ?? "none chosen") : "the delegation returned without acceptance; stop reason " + (result.stopReason ?? "none reported")
10895
+ ...parked ? { parked: true } : {},
10896
+ failure: evaluation.accepted ? void 0 : parked ? "no settlement had landed when the wait ended, so this round is PARKED, not failed: nothing was accepted and nothing was refused, and the child may still be working. The next step is to RESUME this round, not to re-dispatch it or replace the child: call `recursive_review` again on a later turn with childId " + String(continuable?.childId ?? input.childId) + " (the child the round was started for, which stays resumable). Diagnostics: tier " + decision.tier + ", provider " + (decision.provider ?? "none chosen") + ", names on offer [" + (this.lastProviderNames.join(", ") || "none") + "]; parent id " + (input.parent?.id ?? "none") + ", parent session keys [" + (input.parent === void 0 ? "no parent" : Object.keys(input.parent).join(", ")) + "]" : result == null ? input.mode !== "one-shot" ? "the continuable start was made and NO SETTLEMENT arrived within the wait (the child never reported, or never ran); tier " + decision.tier + ", provider " + (decision.provider ?? "none chosen") + ", names on offer [" + (this.lastProviderNames.join(", ") || "none") + "]; parent id " + (input.parent?.id ?? "none") + ", parent session keys [" + (input.parent === void 0 ? "no parent" : Object.keys(input.parent).join(", ")) + "]" : "no delegate result was produced by the one-shot path; tier " + decision.tier + ", provider " + (decision.provider ?? "none chosen") : "the delegation returned without acceptance; stop reason " + (result.stopReason ?? "none reported") + (result.success === false ? " (the child itself reported success:false)" : "")
10875
10897
  });
10876
10898
  const delegationMode = continuable !== null ? continuable.fellBackToOneShot ? "continuable-unavailable" : "continuable" : result !== null ? "one-shot" : "none";
10877
10899
  return {
@@ -14793,4 +14815,4 @@ function apply(ctx, config) {
14793
14815
  });
14794
14816
  }
14795
14817
  //#endregion
14796
- export { Config, DEFAULT_BUDGETS, DEFAULT_ENFORCEMENT, OPTIONAL_PHASES, PHASES, PHASE_POSITIONS, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, apply, auditToPass, buildDelegationPrompt, buildReviewBundle, buildWorkSlice, builtInToolPolicy, capabilityProbe, childScratchPath, coerceAskToDecision, contentSha256, contractDigest, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, currentPhaseArtifact, defaultReviewToolFilter, delegate, delegateContinuable, delegationDecisionBasis, delegationError, detectTamper, discoverRuns, drainContinuableChildren, drainContinuableDescendants, escapeRegExp, evaluateDelegationResult, evaluateToolGuard, foldDiagnostics, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, inject, interruptContinuable, invalidateReceipt, isCoreArtifact, isTaskClaimedBy, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, parseReplyVerdict, pendingWork, phaseIndex, phasePosition, probeCapabilities, readReceipt, readRepairFromReply, readRepairFromStructured, readVerdictFromReply, readVerdictFromStructured, receiptPath, referencesFromResult, registerRecursiveSkill, remainingDepthFor, renderPhaseTail, renderRecursivePolicy, renderStableContract, renderTaskHistory, replyPath, resetFoldCache, resolveEnforcementConfig, resolveRole, resolveRunDir, resolveToolPolicyForGuard, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, tamperCandidatePath, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
14818
+ export { Config, DEFAULT_BUDGETS, DEFAULT_ENFORCEMENT, OPTIONAL_PHASES, PHASES, PHASE_POSITIONS, PHASE_SEQUENCE, RECURSIVE_API_PREFIX, RUN_ARTIFACT_SEQUENCE, RUN_STATES, RecursiveRuntime, actionRecordStatus, apply, auditToPass, buildDelegationPrompt, buildReviewBundle, buildWorkSlice, builtInToolPolicy, capabilityProbe, childScratchPath, coerceAskToDecision, contentSha256, contractDigest, coupleGateBlockToGoal, createChildBrief, createHandoff, createRecursiveCloseoutTool, createRecursiveInitTool, createRecursiveLintTool, createRecursiveLockTool, createRecursivePhaseTool, createRecursiveScratchTool, createRecursiveStatusTool, createRecursiveWorktreeTool, currentPhaseArtifact, defaultReviewToolFilter, delegate, delegateContinuable, delegationDecisionBasis, delegationError, detectTamper, discoverRuns, drainContinuableChildren, drainContinuableDescendants, escapeRegExp, evaluateDelegationResult, evaluateToolGuard, foldDiagnostics, foldRun, foldRunCard, getAllStaleReceipts, getArtifactState, getGateStatus, getLatestRunDirectory, getLockStatus, getMdFieldValue, getNextLegalPhase, getPrerequisiteBlockers, getPrerequisites, getStaleDownstreamPhases, getTodoStats, getWorkflowProfile, inject, interruptContinuable, invalidateReceipt, isCoreArtifact, isTaskClaimedBy, loadRouterPolicy, lockHashFromContent, makeRecursiveRoutes, mountRecursiveRoutesOnce, name, normalizeForLockHash, parseReplyVerdict, pendingWork, phaseIndex, phasePosition, probeCapabilities, readReceipt, readRepairFromReply, readRepairFromStructured, readVerdictFromReply, readVerdictFromStructured, receiptPath, referencesFromResult, registerRecursiveSkill, remainingDepthFor, renderPhaseTail, renderRecursivePolicy, renderStableContract, renderTaskHistory, replyPath, resetFoldCache, resolveEnforcementConfig, resolveRole, resolveRunDir, resolveToolPolicyForGuard, reviewBundleDir, reviewOutputSchema, routerPolicyPath, snapshotWorkspace, tamperCandidatePath, trimMdValue, validateChain, validateReferences, validateTransition, writeActionRecord, writeReceipt };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@try-works/dsh-recursive-mode",
3
3
  "description": "recursive-mode workflow as a DeepSeek Harness bundle: RecursiveRuntime service + 13 recursive_* tools (recursive_status, recursive_init, recursive_lock, recursive_lint, recursive_closeout, recursive_scratch, recursive_worktree, recursive_phase, recursive_audit_team, recursive_review, recursive_delegate, recursive_ask, recursive_preview)",
4
- "version": "0.4.7",
4
+ "version": "0.4.8",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
package/src/delegation.ts CHANGED
@@ -559,9 +559,18 @@ export async function delegateContinuable(input: {
559
559
  // turn-shaped caller resumes on a later turn instead of treating the round
560
560
  // as lost, and so `accepted` stays false — an unobserved round is never an
561
561
  // approval.
562
+ //
563
+ // ⚠ THE SENTENCE NAMES THE CHILD, and that is not decoration. The advice this
564
+ // reason carries ("resume with the SAME child") is UNACTIONABLE without the id,
565
+ // so a caller that reads it and does not also read `childId` can only report the
566
+ // park, not act on it — and the record built from this reason is exactly where a
567
+ // live run read "no settlement" as "the child is dead" and went around its own
568
+ // reviewer. `resumeChild` is the field that consumes this id.
562
569
  return {
563
570
  ok: false,
564
- reason: 'no settlement has landed for round ' + (round + 1) + ' yet (the child is still working)',
571
+ reason: 'no settlement has landed for round ' + (round + 1) + ' yet, so the round is PARKED, not failed: '
572
+ + 'the child may still be working. Resume it on a later turn with childId ' + String(childId)
573
+ + ' (the `resumeChild` argument) — do not start a second child.',
565
574
  childId,
566
575
  messageIds,
567
576
  rounds,
@@ -774,8 +783,43 @@ export interface ActionRecordInput {
774
783
  * nothing else: a delegation that FAILED and a delegation that NEVER HAPPENED read identically, which is what
775
784
  * let me conclude for three rounds that the host was not scheduling children. The caller ALREADY passed
776
785
  * `stopReason`, and the live record said `n/a` — because there was no result to take a stop reason from.
786
+ *
787
+ * ⚠ AND IT IS EMITTED ONLY FOR A GENUINE FAILURE. A PARKED round is neither accepted nor failed, so it
788
+ * carries {@link ActionRecordInput.parked} instead: calling a live child a failure is the defect this
789
+ * field's own history is made of.
777
790
  */
778
791
  failure?: string
792
+ /**
793
+ * ⚠ THE THIRD STATE, AND WHY `success` COULD NOT CARRY IT.
794
+ *
795
+ * A continuable round that has not settled is NOT a failure — `ContinuableDelegationLike.parked` says so in
796
+ * its own doc comment, and the tool result already surfaces it. The RECORD did not: a live run's child was
797
+ * parked, the record said `Status: failed` with "the child never reported, or never ran", the main agent
798
+ * read that as a dead child and obtained the review elsewhere — while the child was still working and
799
+ * replied eighteen minutes later. `success: false` cannot express "still in flight", because a genuinely
800
+ * dead child also produces `success: false`, so a second field is required rather than a cleverer boolean.
801
+ *
802
+ * When true, the record says `parked` and carries a `Parked:` line (never a `Failure:` line) whose text
803
+ * states only what is KNOWN, names the `childId`, and names the resume step. It takes precedence over
804
+ * `success`: an unobserved round is never an acceptance, whatever a caller passes alongside it.
805
+ */
806
+ parked?: boolean
807
+ }
808
+
809
+ /**
810
+ * The status a record states, in ONE place, because the three states are the fix and a second writer would
811
+ * drift from this one.
812
+ *
813
+ * The wording is chosen for two readers at once. The MODEL reads it to decide whether to resume or to give up
814
+ * and obtain the result another way — the exact decision the live defect got wrong — so the token must not
815
+ * read as a failure and must not need the rest of the document to be understood. A HUMAN reading the run tree
816
+ * months later needs to tell "died" from "still working" at a glance. Hence `parked (still running; no
817
+ * settlement yet)`: a third token rather than a renamed second, qualified with the two facts that separate it
818
+ * from `failed`, and short enough to sit in a status line.
819
+ */
820
+ export function actionRecordStatus(input: Pick<ActionRecordInput, 'success' | 'parked'>): string {
821
+ if (input.parked === true) return 'parked (still running; no settlement yet)'
822
+ return input.success ? 'accepted' : 'failed'
779
823
  }
780
824
 
781
825
  function slugify(value: string): string {
@@ -794,7 +838,10 @@ function slugify(value: string): string {
794
838
  * `Code Refs` strictly inside ## Inputs Provided. The linter resolves each of
795
839
  * those through the heading body, so a field under another heading is not
796
840
  * found at all.
797
- * A success:false attempt is written with a failed status and is NOT accepted.
841
+ * A success:false attempt is written with a failed status and is NOT accepted. A round that PARKED is written
842
+ * with a status of its own (`parked (still running; no settlement yet)`, see {@link actionRecordStatus}) and a
843
+ * `Parked:` line instead of a `Failure:` one, because no settlement is not a death: it is the caller's signal to
844
+ * resume the same child. Only a genuine failure carries `Failure:`.
798
845
  */
799
846
  export function writeActionRecord(input: ActionRecordInput): string {
800
847
  const { root, runId } = input
@@ -844,11 +891,18 @@ export function writeActionRecord(input: ActionRecordInput): string {
844
891
  '- Phase: ' + input.phase,
845
892
  '- Purpose: ' + input.purpose,
846
893
  '- Execution Mode: ' + input.executionMode,
847
- '- Status: ' + (input.success ? 'accepted' : 'failed'),
894
+ '- Status: ' + actionRecordStatus(input),
848
895
  // ⚠ EMITTED ONLY WHEN A REASON IS GIVEN, so a caller that says nothing produces the record it always did.
849
896
  // Not politeness: this record's shape is asserted by specs, and a first attempt that always emitted the line
850
897
  // failed 13 tests across 5 files. A change to a shared surface should be additive where it can be.
851
- ...(input.success === false && input.failure !== undefined ? ['- Failure: ' + input.failure] : []),
898
+ //
899
+ // ⚠ AND `parked` IS THE ONE STATE THAT MUST NOT WEAR THE `Failure:` LABEL. This line used to be
900
+ // `input.success === false && input.failure !== undefined`, which is true of a PARKED round as well — so a
901
+ // live child that was still working was recorded, on the same line, as a failure. A parked round is not a
902
+ // failure, so it gets its own field and states only what is established.
903
+ ...(input.success === false && input.failure !== undefined
904
+ ? [input.parked === true ? '- Parked: ' + input.failure : '- Failure: ' + input.failure]
905
+ : []),
852
906
  '- Stop Reason: ' + (input.stopReason ?? 'n/a'),
853
907
  // Timestamp LAST in Metadata. This USED to be load-bearing: `getHeadingBody` ended
854
908
  // its capture with a `\Z` that JavaScript reads as a literal `Z`, so a body was
package/src/runtime.ts CHANGED
@@ -1099,7 +1099,16 @@ export class RecursiveRuntime extends Service {
1099
1099
  result = continuable.rounds[continuable.rounds.length - 1].result ?? null
1100
1100
  if (!result) error = 'continuable child produced no final result'
1101
1101
  } else {
1102
- error = continuable.reason ?? 'continuable delegation failed'
1102
+ // ⚠ A ROUND THAT SETTLED IS A RESULT, EVEN WHEN IT WAS NOT ACCEPTED — and this branch used to
1103
+ // discard it. The condition above requires `continuable.ok`, so a child that REPORTED and was
1104
+ // refused (`success: false`, or a non-completed stop reason) fell through to here: `result` stayed
1105
+ // null, the action record said "NO SETTLEMENT arrived within the wait", and the child's own stop
1106
+ // reason — the one fact that explains the refusal — was dropped on the floor. It is the same defect
1107
+ // as the parked one, one branch over: an absence asserted where the code had evidence. Keeping the
1108
+ // result is what lets the record say "the delegation returned without acceptance; stop reason error"
1109
+ // instead of blaming a wait that ended perfectly well.
1110
+ result = continuable.rounds[continuable.rounds.length - 1]?.result ?? null
1111
+ if (result === null) error = continuable.reason ?? 'continuable delegation failed'
1103
1112
  }
1104
1113
  } else {
1105
1114
  try {
@@ -1147,7 +1156,14 @@ export class RecursiveRuntime extends Service {
1147
1156
  id: operation,
1148
1157
  act: 'delegate-review',
1149
1158
  at: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'),
1150
- outcome: evaluation.accepted ? 'accepted' : 'unaccepted',
1159
+ // ⚠ A PARK IS NOT A REFUSAL, and `unaccepted` said it was. This line used to be
1160
+ // `accepted ? 'accepted' : 'unaccepted'`, so a round that had merely not settled yet was indexed
1161
+ // exactly like a delegation that was evaluated and refused — while the round it really was (still in
1162
+ // flight, resume the same child) was nowhere in the run's own operation log. The defect the action
1163
+ // record had, the log had too. The new value is honest on both readings that matter: it is not
1164
+ // `accepted`, so every retry gate still treats the operation as unfinished and retryable — which is
1165
+ // what a parked round is — and it no longer claims the delegation was judged and rejected.
1166
+ outcome: parked ? 'parked' : (evaluation.accepted ? 'accepted' : 'unaccepted'),
1151
1167
  phase: input.phase,
1152
1168
  })
1153
1169
  }
@@ -1158,8 +1174,8 @@ export class RecursiveRuntime extends Service {
1158
1174
  runId: input.runId,
1159
1175
  subagentId: input.childId,
1160
1176
  phase: input.phase,
1161
- // ⚠ FU-17 — the kind is stated in the record. `Status` says accepted or failed; nothing said whether the
1162
- // child PRODUCED the phase's work or JUDGED it, and a reader of a run could not tell the two apart.
1177
+ // ⚠ FU-17 — the kind is stated in the record. `Status` says accepted, failed or parked; nothing said whether
1178
+ // the child PRODUCED the phase's work or JUDGED it, and a reader of a run could not tell the two apart.
1163
1179
  purpose: input.role + (input.kind === 'work' ? ' (work)' : '') + ' for run ' + input.runId,
1164
1180
  executionMode: decision.tier + (input.mode !== 'one-shot' ? ' (continuable)' : ''),
1165
1181
  artifactPath: input.artifactPath,
@@ -1171,32 +1187,70 @@ export class RecursiveRuntime extends Service {
1171
1187
  findings: evaluation.accepted && result?.structured ? [(result.structured as { verdict?: string })?.verdict ?? 'accepted'] : undefined,
1172
1188
  success: evaluation.accepted,
1173
1189
  stopReason: result?.stopReason,
1190
+ // ⚠ A PARKED ROUND IS RECORDED AS PARKED — the whole defect in one field. `success: evaluation.accepted`
1191
+ // is false for a park (correct: nothing was accepted), and `writeActionRecord` reads this flag to state
1192
+ // the third state instead of collapsing it into `failed`.
1193
+ ...(parked ? { parked: true } : {}),
1174
1194
  // ⚠ FU-9 — AND SAY WHICH KIND OF FAILURE, because the record previously could not. `result == null` means
1175
1195
  // the provider never produced anything at all (never started, or returned nothing) — which is what the
1176
1196
  // live record's `Stop Reason: n/a` was quietly telling me — while a present result that failed to be
1177
1197
  // accepted means a child DID run and its work was refused. Different problems, identical artifacts.
1198
+ //
1199
+ // ⚠ AND `parked` IS BRANCHED FIRST. A parked round produced NO result at all — that IS what parking
1200
+ // means — so without this branch first it fell into the `result == null` text below: "the continuable
1201
+ // start was made and NO SETTLEMENT arrived within the wait (the child never reported, or never ran)".
1202
+ // That is the exact false conclusion this fix exists for, and it was reached whatever the record's
1203
+ // Status said. Order is therefore load-bearing here.
1178
1204
  failure: evaluation.accepted
1179
1205
  ? undefined
1180
- : result == null
1181
- // ⚠ THE TWO STATES ARE NOT THE SAME AND THE MESSAGE USED TO CONFLATE THEM. The one-shot path cannot
1182
- // resolve to nothing — the host's `start` returns a run or throws (assertCapabilities, expectProvider)
1183
- // — so a null result on the CONTINUABLE path means the opposite of what I first wrote: the start WAS
1184
- // made and NO SETTLEMENT ARRIVED within the wait. That distinction cost me two rounds of looking at
1185
- // provider names, so the record now states which path a run took and what it was waiting for.
1186
- ? (input.mode !== 'one-shot'
1187
- ? 'the continuable start was made and NO SETTLEMENT arrived within the wait (the child never reported,'
1188
- + ' or never ran); tier ' + decision.tier + ', provider ' + (decision.provider ?? 'none chosen')
1189
- + ', names on offer [' + (this.lastProviderNames.join(', ') || 'none') + ']'
1190
- // ⚠ FU-9 — THE PARENT IDENTITY, because the host refuses a prompt when it cannot resolve the
1191
- // parent session as a live Agent (`subagent/parent-unavailable`, index.ts L429-436), and the tool
1192
- // builds this handle with a CAST (`exec.agent as unknown as SubagentParentHandle`). A cast is not
1193
- // a contract: if the id here is not the one the host looks up, the refusal is real and the
1194
- // classifier's crash has been hiding it. Printing it here costs nothing and settles the question.
1195
- + '; parent id ' + ((input.parent as { id?: string } | undefined)?.id ?? 'none')
1196
- + ', parent session keys [' + (input.parent === undefined ? 'no parent' : Object.keys(input.parent as object).join(', ')) + ']'
1197
- : 'no delegate result was produced by the one-shot path; tier ' + decision.tier
1198
- + ', provider ' + (decision.provider ?? 'none chosen'))
1199
- : 'the delegation returned without acceptance; stop reason ' + (result.stopReason ?? 'none reported'),
1206
+ : parked
1207
+ // ⚠ WHAT IS KNOWN, AND ONLY WHAT IS KNOWN, WITH THE ID THE READER NEEDS TO ACT.
1208
+ //
1209
+ // The text this replaces said "the child never reported, or never ran" — a CONCLUSION drawn from an
1210
+ // ABSENCE, and it was false: a live child went on to complete three review rounds and reply eighteen
1211
+ // minutes later, while the main agent read `Status: failed`, concluded the child was dead, and
1212
+ // obtained its review by other means. So the parked message asserts nothing about the child's state
1213
+ // beyond "no settlement had landed when the wait ended", keeps "may still be working" as the
1214
+ // possibility it is, and NAMES the childId plus the exact next step, because advice to resume is
1215
+ // unactionable without the id. The identity diagnostics stay, because they are what makes a
1216
+ // misconfigured provider readable — but they are diagnostics, not the reason.
1217
+ ? 'no settlement had landed when the wait ended, so this round is PARKED, not failed: nothing was'
1218
+ + ' accepted and nothing was refused, and the child may still be working. The next step is to RESUME'
1219
+ + ' this round, not to re-dispatch it or replace the child: call `recursive_review` again on a later'
1220
+ + ' turn with childId ' + String(continuable?.childId ?? input.childId) + ' (the child the round was'
1221
+ + ' started for, which stays resumable). Diagnostics: tier ' + decision.tier
1222
+ + ', provider ' + (decision.provider ?? 'none chosen')
1223
+ + ', names on offer [' + (this.lastProviderNames.join(', ') || 'none') + ']'
1224
+ // ⚠ FU-9 — THE PARENT IDENTITY, because the host refuses a prompt when it cannot resolve the
1225
+ // parent session as a live Agent (`subagent/parent-unavailable`, index.ts L429-436), and the tool
1226
+ // builds this handle with a CAST (`exec.agent as unknown as SubagentParentHandle`). A cast is not
1227
+ // a contract: if the id here is not the one the host looks up, the refusal is real and the
1228
+ // classifier's crash has been hiding it. Printing it here costs nothing and settles the question.
1229
+ + '; parent id ' + ((input.parent as { id?: string } | undefined)?.id ?? 'none')
1230
+ + ', parent session keys [' + (input.parent === undefined ? 'no parent' : Object.keys(input.parent as object).join(', ')) + ']'
1231
+ : result == null
1232
+ // ⚠ THE TWO STATES ARE NOT THE SAME AND THE MESSAGE USED TO CONFLATE THEM. The one-shot path cannot
1233
+ // resolve to nothing — the host's `start` returns a run or throws (assertCapabilities, expectProvider)
1234
+ // — so a null result on the CONTINUABLE path means the opposite of what I first wrote: the start WAS
1235
+ // made and NO SETTLEMENT ARRIVED within the wait. That distinction cost me two rounds of looking at
1236
+ // provider names, so the record now states which path a run took and what it was waiting for.
1237
+ // (A park is handled above and never reaches this branch; this one is a continuable round that
1238
+ // produced neither a result nor the park signal, which IS a failure to report.)
1239
+ ? (input.mode !== 'one-shot'
1240
+ ? 'the continuable start was made and NO SETTLEMENT arrived within the wait (the child never reported,'
1241
+ + ' or never ran); tier ' + decision.tier + ', provider ' + (decision.provider ?? 'none chosen')
1242
+ + ', names on offer [' + (this.lastProviderNames.join(', ') || 'none') + ']'
1243
+ // ⚠ FU-9 — THE PARENT IDENTITY, because the host refuses a prompt when it cannot resolve the
1244
+ // parent session as a live Agent (`subagent/parent-unavailable`, index.ts L429-436), and the tool
1245
+ // builds this handle with a CAST (`exec.agent as unknown as SubagentParentHandle`). A cast is not
1246
+ // a contract: if the id here is not the one the host looks up, the refusal is real and the
1247
+ // classifier's crash has been hiding it. Printing it here costs nothing and settles the question.
1248
+ + '; parent id ' + ((input.parent as { id?: string } | undefined)?.id ?? 'none')
1249
+ + ', parent session keys [' + (input.parent === undefined ? 'no parent' : Object.keys(input.parent as object).join(', ')) + ']'
1250
+ : 'no delegate result was produced by the one-shot path; tier ' + decision.tier
1251
+ + ', provider ' + (decision.provider ?? 'none chosen'))
1252
+ : 'the delegation returned without acceptance; stop reason ' + (result.stopReason ?? 'none reported')
1253
+ + (result.success === false ? ' (the child itself reported success:false)' : ''),
1200
1254
  })
1201
1255
 
1202
1256
  // T35: report the mode that ACTUALLY ran, not the one that was asked for. A