nolo-cli 0.115.0-alpha.1 → 0.116.0-alpha.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.
Files changed (63) hide show
  1. package/{_executeModel-RQXGTKAT.js → _executeModel-BPUR2QYH.js} +15 -14
  2. package/{addDialogAgentAction-MCEABSBA.js → addDialogAgentAction-S2NRBCWW.js} +1 -1
  3. package/{agentDeleteCommand-S4GW4PKE.js → agentDeleteCommand-MNAA6MIG.js} +2 -2
  4. package/{agentEmailCommands-M26R6BO4.js → agentEmailCommands-SK34WJKU.js} +14 -14
  5. package/{agentListCommands-3YRR3FOM.js → agentListCommands-I26MKI4T.js} +2 -2
  6. package/{agentMachineCommands-ZFEQOZG3.js → agentMachineCommands-M6QDR6P3.js} +3 -3
  7. package/{agentRecordCommands-LZ4G5INZ.js → agentRecordCommands-HTHLO2MM.js} +14 -14
  8. package/{agentRunCommand-GOP4T77I.js → agentRunCommand-IAIRCMTH.js} +14 -14
  9. package/{agentRuntimeLocal-FXCNGFFT.js → agentRuntimeLocal-3WTYYIT2.js} +2 -2
  10. package/{agentSlice-PKMID4S2.js → agentSlice-TVP3HINW.js} +1 -1
  11. package/{appDeleteCommands-2LIWMMJP.js → appDeleteCommands-SB5TNLKY.js} +2 -2
  12. package/{appDeployCommands-WOTKY2LM.js → appDeployCommands-2IPKZINV.js} +2 -2
  13. package/{appDeployStatusCommands-6QQWIFCK.js → appDeployStatusCommands-K63TFUID.js} +2 -2
  14. package/{appGetCommands-Z4RHYUKS.js → appGetCommands-VU2SLNEH.js} +2 -2
  15. package/{appListCommands-SOBFAS4W.js → appListCommands-S3T4DJ36.js} +2 -2
  16. package/{chatQueueReduxAdapter-KMULSV7M.js → chatQueueReduxAdapter-TWIYKVIZ.js} +3 -3
  17. package/{chunk-2MMIOYAF.js → chunk-3BGIKIPF.js} +12 -12
  18. package/{chunk-SQCF7OWL.js → chunk-3IUWIAHP.js} +1 -1
  19. package/{chunk-SWM3OJR7.js → chunk-3VUWPRFW.js} +5 -4
  20. package/{chunk-RB3EETRZ.js → chunk-52Q2IW2X.js} +429 -53
  21. package/{chunk-TUBKLG3S.js → chunk-5MEYHVIB.js} +1 -1
  22. package/{chunk-6YTOJY35.js → chunk-6D2JFXLY.js} +35 -0
  23. package/{chunk-X767IQBD.js → chunk-DD6XE56O.js} +9 -62
  24. package/{chunk-KEQNY22P.js → chunk-FP76KI2Y.js} +54 -11
  25. package/{chunk-VLTIYV3X.js → chunk-HYJFRTLW.js} +1 -1
  26. package/{chunk-6P47Q5OH.js → chunk-KAUSRTO6.js} +2 -2
  27. package/{chunk-5F3OZ7L2.js → chunk-LMREEBWT.js} +35 -5
  28. package/{chunk-ANFQ4KPL.js → chunk-MZYBSTMH.js} +2 -2
  29. package/chunk-OCZKATRC.js +129 -0
  30. package/{chunk-22CGS5BQ.js → chunk-PMAIQX7A.js} +2 -2
  31. package/{chunk-NGEWETHC.js → chunk-RPJUFEGR.js} +3 -3
  32. package/{chunk-LZLZNZ4H.js → chunk-U4OKDZFH.js} +1 -1
  33. package/{chunk-DE7R7ZAP.js → chunk-UDVH2P4A.js} +3 -3
  34. package/{chunk-QTGQ2LTL.js → chunk-UEVXFHQB.js} +1 -1
  35. package/{chunk-Q4U2WKQY.js → chunk-XP3VVAPG.js} +90 -14
  36. package/{chunk-SBDFSHSS.js → chunk-Z2PV6O2T.js} +9 -0
  37. package/{contentThunks-EHSBUA4D.js → contentThunks-JYWI7G7I.js} +4 -4
  38. package/{createAgentAutomationAction-G2MK4LVL.js → createAgentAutomationAction-Z4SUJ54Y.js} +4 -4
  39. package/{createDialogAction-JH7ZCV5Z.js → createDialogAction-RCHFLVJ2.js} +5 -5
  40. package/{createPageAction-SRPNANEQ.js → createPageAction-YK2253XQ.js} +4 -4
  41. package/{dialogCommands-JCDHSPHY.js → dialogCommands-X4UOZQWX.js} +2 -2
  42. package/{dialogSlice-YUPGRKPA.js → dialogSlice-IVTWLCE2.js} +9 -3
  43. package/{docCreateCommands-HDV5VRAK.js → docCreateCommands-VD6TGHG4.js} +2 -2
  44. package/{docDeleteCommands-77BE7WA3.js → docDeleteCommands-OT3R2G4W.js} +2 -2
  45. package/{docReadCommands-4Z7DUAHQ.js → docReadCommands-2G7BIXSH.js} +2 -2
  46. package/{docStore-F3LR3M6Z.js → docStore-UENKCCDX.js} +1 -1
  47. package/{docUpdateCommands-L45KOOSF.js → docUpdateCommands-45MVJKZA.js} +2 -2
  48. package/{handleSendMessageAction-GCKW2H7N.js → handleSendMessageAction-LM2J4HC3.js} +40 -6
  49. package/index.js +41 -41
  50. package/{machineCommands-N3XF6SYH.js → machineCommands-6N6MQIE6.js} +14 -14
  51. package/{machineWsRunDispatch-UJX7MT2Z.js → machineWsRunDispatch-7MFKVQCR.js} +14 -14
  52. package/package.json +1 -1
  53. package/{readlineWorkspace-FFZEUHR4.js → readlineWorkspace-JWBON5CX.js} +34 -16
  54. package/{removeDialogAgentAction-J6X5QMTA.js → removeDialogAgentAction-W66BJ7CF.js} +1 -1
  55. package/{runAgentClientLoop-UGNX7QRL.js → runAgentClientLoop-TA4SSPHE.js} +12 -12
  56. package/{setDialogExtraReferencesAction-WX5VTY6O.js → setDialogExtraReferencesAction-2ALKG3WA.js} +2 -2
  57. package/{setPrimaryDialogAgentAction-FN22THHT.js → setPrimaryDialogAgentAction-TQZB55F6.js} +1 -1
  58. package/{spaceCommands-TCEN2CRN.js → spaceCommands-6NSAXB7D.js} +2 -2
  59. package/{streamAgentChatTurn-DKVJJ2BL.js → streamAgentChatTurn-X5ENGUIS.js} +76 -18
  60. package/{switchDialogAgentAction-LVTEV3FR.js → switchDialogAgentAction-3KI72HHS.js} +1 -1
  61. package/{toolApiClient-QVJ6HDNN.js → toolApiClient-G3M73RTB.js} +2 -2
  62. package/{tools-IHD7PL23.js → tools-XRQR7ZZR.js} +9 -9
  63. package/{updateDialogTitleAction-QKUFUNTO.js → updateDialogTitleAction-ESUUD3GT.js} +4 -4
@@ -50,7 +50,7 @@ import {
50
50
  requiresBareImageUrl,
51
51
  resolveToolGuidedSections,
52
52
  toBareImageUrlMessages
53
- } from "./chunk-SBDFSHSS.js";
53
+ } from "./chunk-Z2PV6O2T.js";
54
54
  import {
55
55
  convertMessagesToResponsesInput,
56
56
  extractReasoningFromResponseOutput,
@@ -11900,6 +11900,7 @@ var toQueue = /* @__PURE__ */ dual(2, (self, options) => toQueueArray(self.chann
11900
11900
  var runIntoQueue = /* @__PURE__ */ dual(2, (self, queue) => runIntoQueueArray(self.channel, queue));
11901
11901
 
11902
11902
  // packages/agent-runtime/abortableKernel.ts
11903
+ var IDLE_WATCHDOG_SLICES = 20;
11903
11904
  function runAbortableWithTimeout(args2) {
11904
11905
  const taskEffect = Effect_exports.callback(
11905
11906
  (resume) => {
@@ -11935,12 +11936,26 @@ function runAbortableWithTimeout(args2) {
11935
11936
  raced = Effect_exports.raceFirst(taskEffect, abortEffect);
11936
11937
  }
11937
11938
  if (typeof args2.timeoutMs === "number") {
11938
- const inner = raced;
11939
+ const outer = raced;
11939
11940
  const timeoutMs = args2.timeoutMs;
11940
- raced = Effect_exports.gen(function* () {
11941
- const outcome = yield* Effect_exports.timeoutOption(inner, Duration_exports.millis(timeoutMs));
11942
- return Option_exports.isNone(outcome) ? { kind: "timeout" } : outcome.value;
11941
+ const activityVersion = args2.activityVersion;
11942
+ const idleWatchdog = Effect_exports.gen(function* () {
11943
+ const sliceCount = activityVersion ? IDLE_WATCHDOG_SLICES : 1;
11944
+ const baseSliceMs = Math.max(1, Math.floor(timeoutMs / sliceCount));
11945
+ let idleMs = 0;
11946
+ for (; ; ) {
11947
+ const versionAtSliceStart = activityVersion?.();
11948
+ const sliceMs = Math.max(1, Math.min(timeoutMs - idleMs, baseSliceMs));
11949
+ yield* Effect_exports.sleep(Duration_exports.millis(sliceMs));
11950
+ idleMs += sliceMs;
11951
+ if (activityVersion && activityVersion() !== versionAtSliceStart) {
11952
+ idleMs = 0;
11953
+ continue;
11954
+ }
11955
+ if (idleMs >= timeoutMs) return { kind: "timeout" };
11956
+ }
11943
11957
  });
11958
+ raced = Effect_exports.raceFirst(outer, idleWatchdog);
11944
11959
  }
11945
11960
  return args2.runtime ? args2.runtime.runPromise(raced) : Effect_exports.runPromise(raced);
11946
11961
  }
@@ -12627,6 +12642,13 @@ function isColdResume(history, nowMs) {
12627
12642
  }
12628
12643
  async function maybeAutoCompactLocalHistory(args2) {
12629
12644
  const { adapter, dialogId, history } = args2;
12645
+ const emitPhase = (phase) => {
12646
+ try {
12647
+ args2.onPhase?.(phase);
12648
+ } catch {
12649
+ }
12650
+ };
12651
+ emitPhase({ kind: "validating-context" });
12630
12652
  const unchanged = (decision) => ({
12631
12653
  history,
12632
12654
  compressed: false,
@@ -12722,8 +12744,15 @@ async function maybeAutoCompactLocalHistory(args2) {
12722
12744
  if (!plan.shouldCompress) {
12723
12745
  return projectExisting();
12724
12746
  }
12747
+ emitPhase({ kind: "compaction-start" });
12725
12748
  try {
12749
+ if (args2.abortSignal?.aborted) {
12750
+ throw Object.assign(new Error("auto-compaction aborted"), { compactionReason: "aborted" });
12751
+ }
12726
12752
  const provider = await args2.resolveProvider();
12753
+ if (args2.abortSignal?.aborted) {
12754
+ throw Object.assign(new Error("auto-compaction aborted"), { compactionReason: "aborted" });
12755
+ }
12727
12756
  const msgsToCompress = plan.msgsToCompress;
12728
12757
  const messagesText = formatMessagesForSummaryWithTruncation(msgsToCompress);
12729
12758
  const fileOpsText = formatFileOperationsFromMessages(
@@ -12735,20 +12764,55 @@ async function maybeAutoCompactLocalHistory(args2) {
12735
12764
  messagesText,
12736
12765
  fileOpsText
12737
12766
  });
12738
- const result4 = await provider.complete([
12739
- { role: "system", content: COMPACTION_SUMMARY_SYSTEM_PROMPT },
12740
- { role: "user", content: promptContent }
12741
- ]);
12767
+ emitPhase({ kind: "waiting-provider" });
12768
+ const timeoutMs = typeof args2.timeoutMs === "number" && Number.isFinite(args2.timeoutMs) && args2.timeoutMs > 0 ? args2.timeoutMs : void 0;
12769
+ const requestController = new AbortController();
12770
+ let timeoutHandle;
12771
+ let abortListener;
12772
+ const request3 = provider.complete(
12773
+ [{ role: "system", content: COMPACTION_SUMMARY_SYSTEM_PROMPT }, { role: "user", content: promptContent }],
12774
+ { signal: requestController.signal, ...timeoutMs ? { timeoutMs } : {} }
12775
+ ).then(
12776
+ (value) => ({ kind: "done", value }),
12777
+ (error) => ({ kind: "provider-error", error })
12778
+ );
12779
+ const guards = [];
12780
+ if (timeoutMs) guards.push(new Promise((resolve5) => {
12781
+ timeoutHandle = setTimeout(() => {
12782
+ requestController.abort();
12783
+ resolve5({ kind: "timeout" });
12784
+ }, timeoutMs);
12785
+ }));
12786
+ if (args2.abortSignal) guards.push(new Promise((resolve5) => {
12787
+ abortListener = () => {
12788
+ requestController.abort(args2.abortSignal?.reason);
12789
+ resolve5({ kind: "aborted" });
12790
+ };
12791
+ args2.abortSignal?.addEventListener("abort", abortListener, { once: true });
12792
+ }));
12793
+ const outcome = guards.length ? await Promise.race([request3, ...guards]) : await request3;
12794
+ if (timeoutHandle) clearTimeout(timeoutHandle);
12795
+ if (abortListener) args2.abortSignal?.removeEventListener("abort", abortListener);
12796
+ if (outcome.kind !== "done") {
12797
+ const reason = outcome.kind === "timeout" ? "timeout" : outcome.kind === "aborted" ? "aborted" : "provider-error";
12798
+ const detail = outcome.kind === "provider-error" ? outcome.error instanceof Error ? outcome.error.message : String(outcome.error) : reason === "timeout" ? `auto-compaction timed out after ${timeoutMs}ms` : "auto-compaction aborted";
12799
+ throw Object.assign(new Error(detail), { compactionReason: reason });
12800
+ }
12801
+ const result4 = outcome.value;
12742
12802
  const newSummary = typeof result4.content === "string" ? result4.content.trim() : "";
12743
12803
  if (!newSummary) {
12744
12804
  console.warn(
12745
12805
  "[localLoop] auto-compaction produced empty summary; keeping prior projection"
12746
12806
  );
12807
+ const failureReason = "provider-error";
12808
+ emitPhase({ kind: "compaction-failed", reason: failureReason });
12747
12809
  return {
12748
12810
  ...projectExisting(),
12749
12811
  failureMessage: "summary model returned empty content",
12812
+ failureReason,
12750
12813
  decision: {
12751
12814
  outcome: "failed",
12815
+ reason: failureReason,
12752
12816
  detail: "summary model returned empty content"
12753
12817
  }
12754
12818
  };
@@ -12775,6 +12839,7 @@ async function maybeAutoCompactLocalHistory(args2) {
12775
12839
  summaryUsage: result4.usage
12776
12840
  });
12777
12841
  console.log(formatCompactionMetricsLog(metrics));
12842
+ emitPhase({ kind: "compaction-end" });
12778
12843
  return {
12779
12844
  history: projectHistoryWithSummary({
12780
12845
  history,
@@ -12802,10 +12867,13 @@ async function maybeAutoCompactLocalHistory(args2) {
12802
12867
  } catch (error) {
12803
12868
  console.warn("[localLoop] auto-compaction failed:", error);
12804
12869
  const failureMessage = error instanceof Error && error.message ? error.message.slice(0, 200) : String(error).slice(0, 200);
12870
+ const failureReason = error instanceof Error && "compactionReason" in error ? error.compactionReason : "provider-error";
12871
+ emitPhase({ kind: "compaction-failed", reason: failureReason });
12805
12872
  return {
12806
12873
  ...projectExisting(),
12807
12874
  failureMessage,
12808
- decision: { outcome: "failed", detail: failureMessage }
12875
+ failureReason,
12876
+ decision: { outcome: "failed", reason: failureReason, detail: failureMessage }
12809
12877
  };
12810
12878
  }
12811
12879
  }
@@ -12862,6 +12930,8 @@ function emitLoopEvent(boundary, event, bridge) {
12862
12930
  boundary.emit(bridge ? { event, bridge } : { event });
12863
12931
  }
12864
12932
  var LLM_REQUEST_TIMEOUT = "LLM_REQUEST_TIMEOUT";
12933
+ var DEFAULT_LLM_REQUEST_TIMEOUT_MS = 10 * 60 * 1e3;
12934
+ var DEFAULT_COMPACTION_TIMEOUT_MS = 60 * 1e3;
12865
12935
  var LOCAL_TURN_ABORTED_CODE = "LOCAL_TURN_ABORTED";
12866
12936
  function buildAbortedError() {
12867
12937
  const error = new Error("local agent turn aborted by user");
@@ -12889,8 +12959,11 @@ async function runAbortableToolTask(input, task, pendingToolName) {
12889
12959
  }
12890
12960
  function resolveLlmRequestTimeoutMs(input) {
12891
12961
  const raw = input.llmRequestTimeoutMs ?? input.timeoutMs;
12892
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return void 0;
12893
- return raw;
12962
+ return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LLM_REQUEST_TIMEOUT_MS;
12963
+ }
12964
+ function resolveCompactionTimeoutMs(input) {
12965
+ const raw = input.compactionTimeoutMs;
12966
+ return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_COMPACTION_TIMEOUT_MS;
12894
12967
  }
12895
12968
  async function runCompleteWithTimeout(args2) {
12896
12969
  const { provider, messages, options, timeoutMs, round, input, boundary, context: context3, providerName, model } = args2;
@@ -12913,6 +12986,7 @@ async function runCompleteWithTimeout(args2) {
12913
12986
  const outcome = await runAbortableWithTimeout({
12914
12987
  task: complete,
12915
12988
  timeoutMs: typeof timeoutMs === "number" ? timeoutMs : void 0,
12989
+ activityVersion: args2.activityVersion,
12916
12990
  abortSignal: input.abortSignal,
12917
12991
  runtime: input.effectRuntime
12918
12992
  });
@@ -12923,7 +12997,7 @@ async function runCompleteWithTimeout(args2) {
12923
12997
  }
12924
12998
  if (outcome.kind === "timeout") {
12925
12999
  const timeoutError = new Error(
12926
- `LLM request timed out after ${timeoutMs}ms (round ${round})`
13000
+ `LLM request timed out after ${timeoutMs}ms without new output (idle window, round ${round})`
12927
13001
  );
12928
13002
  timeoutError.code = LLM_REQUEST_TIMEOUT;
12929
13003
  throw timeoutError;
@@ -13639,6 +13713,16 @@ async function runLocalAgentTurn(input) {
13639
13713
  }
13640
13714
  }
13641
13715
  let compactionUsage;
13716
+ const emitCompactionPhase = (phase, scope3) => {
13717
+ const atMs = Date.now();
13718
+ if (phase.kind === "validating-context") emitLoopEvent(observationBoundary, { kind: "turn-phase", phase: "validating-context", atMs, compactionScope: scope3 });
13719
+ else if (phase.kind === "compaction-start") {
13720
+ emitLoopEvent(observationBoundary, { kind: "turn-phase", phase: "compacting", atMs, compactionScope: scope3 });
13721
+ emitLoopEvent(observationBoundary, { kind: "compaction-start", atMs, scope: scope3 });
13722
+ } else if (phase.kind === "waiting-provider") emitLoopEvent(observationBoundary, { kind: "turn-phase", phase: "waiting-provider", atMs, compactionScope: scope3 });
13723
+ else if (phase.kind === "compaction-end") emitLoopEvent(observationBoundary, { kind: "compaction-end", atMs, scope: scope3 });
13724
+ else emitLoopEvent(observationBoundary, { kind: "compaction-failed", atMs, scope: scope3, reason: phase.reason });
13725
+ };
13642
13726
  const emitCompactionObservation = (compacted) => {
13643
13727
  const decision = compacted.decision;
13644
13728
  if (decision?.outcome === "skipped") {
@@ -13672,7 +13756,10 @@ async function runLocalAgentTurn(input) {
13672
13756
  model: agentConfig.model,
13673
13757
  resolveProvider: resolveProviderOnce,
13674
13758
  contextWindow,
13675
- realContextUsagePercent
13759
+ realContextUsagePercent,
13760
+ abortSignal: input.abortSignal,
13761
+ timeoutMs: resolveCompactionTimeoutMs(input),
13762
+ onPhase: (phase) => emitCompactionPhase(phase, "initial")
13676
13763
  });
13677
13764
  history = compacted.history;
13678
13765
  compactionUsage = compacted.usage;
@@ -13778,7 +13865,10 @@ async function runLocalAgentTurn(input) {
13778
13865
  model: agentConfig.model,
13779
13866
  resolveProvider: resolveProviderOnce,
13780
13867
  contextWindow,
13781
- ...ratio !== void 0 ? { realContextUsagePercent: ratio } : {}
13868
+ ...ratio !== void 0 ? { realContextUsagePercent: ratio } : {},
13869
+ abortSignal: input.abortSignal,
13870
+ timeoutMs: resolveCompactionTimeoutMs(input),
13871
+ onPhase: (phase) => emitCompactionPhase(phase, "in-loop")
13782
13872
  });
13783
13873
  if (compacted.usage) {
13784
13874
  compactionUsage = addOutOfBandUsage(compactionUsage, compacted.usage);
@@ -13848,11 +13938,12 @@ async function runLocalAgentTurn(input) {
13848
13938
  const shouldPassToolEvents = Boolean(
13849
13939
  input.onToolEvent || input.onObservationEvent || input.observationBoundary
13850
13940
  );
13941
+ let llmStreamActivity = 0;
13851
13942
  result4 = await runCompleteWithTimeout({
13852
13943
  provider,
13853
13944
  messages: requestMessages,
13854
13945
  options: {
13855
- ...typeof input.timeoutMs === "number" ? { timeoutMs: input.timeoutMs } : {},
13946
+ timeoutMs: resolveLlmRequestTimeoutMs(input),
13856
13947
  // 用户取消信号穿进 provider options:providerStreamRetry 据此在
13857
13948
  // 取消后跳过重试,provider 分支(如 antigravity/openai-compatible)
13858
13949
  // 也可把 fetch 绑定到同一信号,实现真正的传输层取消。
@@ -13861,6 +13952,7 @@ async function runLocalAgentTurn(input) {
13861
13952
  // 归到具体对话而不是 chat-proxy 兜底桶。新对话首轮 id 尚未分配。
13862
13953
  ...input.continueDialogId ? { dialogId: input.continueDialogId } : {},
13863
13954
  ...shouldStreamDeltas ? { onTextDelta: (chunk) => {
13955
+ llmStreamActivity += 1;
13864
13956
  partialContent += chunk;
13865
13957
  observationBoundary.emit({
13866
13958
  kind: "text-delta",
@@ -13870,6 +13962,7 @@ async function runLocalAgentTurn(input) {
13870
13962
  });
13871
13963
  } } : {},
13872
13964
  ...shouldStreamReasoning ? { onReasoningDelta: (chunk) => {
13965
+ llmStreamActivity += 1;
13873
13966
  observationBoundary.emit({
13874
13967
  kind: "reasoning-delta",
13875
13968
  chunk,
@@ -13878,6 +13971,7 @@ async function runLocalAgentTurn(input) {
13878
13971
  });
13879
13972
  } } : {},
13880
13973
  ...shouldPassToolEvents ? { onToolEvent: (event) => {
13974
+ llmStreamActivity += 1;
13881
13975
  observationBoundary.emit({
13882
13976
  kind: "tool-event",
13883
13977
  event,
@@ -13888,6 +13982,7 @@ async function runLocalAgentTurn(input) {
13888
13982
  ...shouldPassToolEvents ? { toolEventRound: round } : {}
13889
13983
  },
13890
13984
  timeoutMs: resolveLlmRequestTimeoutMs(input),
13985
+ activityVersion: () => llmStreamActivity,
13891
13986
  round,
13892
13987
  input,
13893
13988
  boundary: observationBoundary,
@@ -16436,29 +16531,6 @@ function getProcessRegistry() {
16436
16531
  return registry;
16437
16532
  }
16438
16533
 
16439
- // packages/agent-runtime/processOwnership.ts
16440
- var asNonEmptyString = (value) => {
16441
- if (typeof value !== "string") return null;
16442
- const trimmed = value.trim();
16443
- return trimmed ? trimmed : null;
16444
- };
16445
- function readProcessOwner(runtimeContext) {
16446
- if (!runtimeContext || typeof runtimeContext !== "object") return null;
16447
- const dialogId = asNonEmptyString(runtimeContext.dialogId) ?? asNonEmptyString(runtimeContext.parentThreadId);
16448
- if (!dialogId) return null;
16449
- const turnId = asNonEmptyString(runtimeContext.turnId);
16450
- const ownerId = asNonEmptyString(runtimeContext.ownerId);
16451
- return {
16452
- dialogId,
16453
- ...turnId ? { turnId } : {},
16454
- ...ownerId ? { ownerId } : {}
16455
- };
16456
- }
16457
-
16458
- // packages/agent-runtime/workspaceShell.ts
16459
- import { resolve as resolve3 } from "node:path";
16460
- import { spawn as spawnChildProcess2 } from "node:child_process";
16461
-
16462
16534
  // packages/agent-runtime/processTaskResult.ts
16463
16535
  var PROCESS_RESULT_CAPSULE_TAIL_CHARS = 4e3;
16464
16536
  var PROCESS_RESULT_CAPSULE_SPILL_THRESHOLD_CHARS = 2e4;
@@ -16478,11 +16550,17 @@ function buildProcessTaskResultCapsule(args2) {
16478
16550
  const exitCode = args2.exitCode;
16479
16551
  const captureError = args2.captureError?.trim() || void 0;
16480
16552
  const durationMs = typeof args2.startedAt === "number" && Number.isFinite(args2.startedAt) ? Math.max(0, (args2.endedAt ?? Date.now()) - args2.startedAt) : void 0;
16481
- const stdoutTail = tailText(stdout, tailChars);
16482
- const stderrTail = tailText(stderr, tailChars);
16553
+ const stdoutProvenance = args2.streamProvenance?.stdout;
16554
+ const stderrProvenance = args2.streamProvenance?.stderr;
16555
+ const ringEvicted = Boolean(stdoutProvenance?.evicted || stderrProvenance?.evicted);
16556
+ const stdoutTailSource = typeof args2.stdoutTail === "string" ? args2.stdoutTail : stdout;
16557
+ const stderrTailSource = typeof args2.stderrTail === "string" ? args2.stderrTail : stderr;
16558
+ const stdoutTail = tailText(stdoutTailSource, tailChars);
16559
+ const stderrTail = tailText(stderrTailSource, tailChars);
16483
16560
  const inlineSize = stdout.length + stderr.length;
16484
16561
  const inlineTruncated = stdoutTail.truncated || stderrTail.truncated;
16485
- const shouldSpill = inlineTruncated || inlineSize > spillThresholdChars;
16562
+ const truncated = inlineTruncated || ringEvicted || Boolean(captureError);
16563
+ const shouldSpill = truncated || inlineSize > spillThresholdChars;
16486
16564
  let spill;
16487
16565
  if (shouldSpill) {
16488
16566
  try {
@@ -16496,8 +16574,11 @@ function buildProcessTaskResultCapsule(args2) {
16496
16574
  });
16497
16575
  spill = {
16498
16576
  displayPath: result4.displayPath,
16499
- totalChars: result4.totalChars,
16500
- totalLines: result4.totalLines
16577
+ // Prefer the caller's live provenance counters: they count every
16578
+ // byte of the original stream, while the buffered content above may
16579
+ // be a capped prefix of it.
16580
+ totalChars: (stdoutProvenance?.totalChars ?? stdout.length) + (stderrProvenance?.totalChars ?? stderr.length),
16581
+ totalLines: (stdoutProvenance?.totalLines ?? countLines(stdout)) + (stderrProvenance?.totalLines ?? countLines(stderr))
16501
16582
  };
16502
16583
  } catch {
16503
16584
  }
@@ -16508,12 +16589,33 @@ function buildProcessTaskResultCapsule(args2) {
16508
16589
  exitCode,
16509
16590
  ...durationMs !== void 0 ? { durationMs } : {},
16510
16591
  ...captureError ? { captureError } : {},
16511
- truncated: inlineTruncated || Boolean(captureError),
16592
+ truncated,
16512
16593
  ...spill ? { spill } : {}
16513
16594
  };
16514
16595
  }
16515
16596
 
16597
+ // packages/agent-runtime/processOwnership.ts
16598
+ var asNonEmptyString = (value) => {
16599
+ if (typeof value !== "string") return null;
16600
+ const trimmed = value.trim();
16601
+ return trimmed ? trimmed : null;
16602
+ };
16603
+ function readProcessOwner(runtimeContext) {
16604
+ if (!runtimeContext || typeof runtimeContext !== "object") return null;
16605
+ const dialogId = asNonEmptyString(runtimeContext.dialogId) ?? asNonEmptyString(runtimeContext.parentThreadId);
16606
+ if (!dialogId) return null;
16607
+ const turnId = asNonEmptyString(runtimeContext.turnId);
16608
+ const ownerId = asNonEmptyString(runtimeContext.ownerId);
16609
+ return {
16610
+ dialogId,
16611
+ ...turnId ? { turnId } : {},
16612
+ ...ownerId ? { ownerId } : {}
16613
+ };
16614
+ }
16615
+
16516
16616
  // packages/agent-runtime/workspaceShell.ts
16617
+ import { resolve as resolve3 } from "node:path";
16618
+ import { spawn as spawnChildProcess2 } from "node:child_process";
16517
16619
  var EXEC_SHELL_TIMEOUT_ENV = "NOLO_EXEC_SHELL_TIMEOUT_MS";
16518
16620
  var EXEC_SHELL_DETACH_ENV = "NOLO_EXEC_SHELL_DETACH_MS";
16519
16621
  var DEFAULT_EXEC_SHELL_DETACH_MS = 12e4;
@@ -17382,13 +17484,169 @@ function runTasks(args2) {
17382
17484
  return { tasks, count: tasks.length };
17383
17485
  }
17384
17486
 
17487
+ // packages/agent-runtime/execShellCompletionWarnings.ts
17488
+ var BG_ESCAPE_WORD_PATTERN = /(?:^|[\s;|(&/\\])(nohup|disown|setsid|start-process)(?=$|[\s;|)&])/i;
17489
+ var SHELL_SPLIT_CHARS = /* @__PURE__ */ new Set([";", "|", "&", "(", ")", "<", ">", "\n", " ", " "]);
17490
+ function maskQuotedAndEscaped(command) {
17491
+ const out = Array.from(command);
17492
+ let quote = null;
17493
+ for (let i = 0; i < out.length; i += 1) {
17494
+ const ch = out[i];
17495
+ if (quote) {
17496
+ out[i] = " ";
17497
+ if (ch === "\\" && quote !== "'") {
17498
+ if (i + 1 < out.length) out[i + 1] = " ";
17499
+ i += 1;
17500
+ } else if (ch === quote) {
17501
+ quote = null;
17502
+ }
17503
+ continue;
17504
+ }
17505
+ if (ch === "\\") {
17506
+ out[i] = " ";
17507
+ if (i + 1 < out.length) out[i + 1] = " ";
17508
+ i += 1;
17509
+ continue;
17510
+ }
17511
+ if (ch === "'" || ch === '"' || ch === "`") {
17512
+ quote = ch;
17513
+ out[i] = " ";
17514
+ continue;
17515
+ }
17516
+ if (ch === "#" && (i === 0 || /[\s;]/.test(out[i - 1] ?? " "))) {
17517
+ while (i < out.length && out[i] !== "\n") {
17518
+ out[i] = " ";
17519
+ i += 1;
17520
+ }
17521
+ }
17522
+ }
17523
+ return out.join("");
17524
+ }
17525
+ function readRedirectTarget(original, start) {
17526
+ let i = start;
17527
+ while (i < original.length && /\s/.test(original[i] ?? "")) {
17528
+ i += 1;
17529
+ }
17530
+ if (i >= original.length) return void 0;
17531
+ let text = "";
17532
+ let j = i;
17533
+ while (j < original.length) {
17534
+ const o = original[j];
17535
+ if (o === "'" || o === '"' || o === "`") {
17536
+ const quote = o;
17537
+ j += 1;
17538
+ while (j < original.length && original[j] !== quote) {
17539
+ if (original[j] === "\\" && quote !== "'" && j + 1 < original.length) {
17540
+ text += original[j + 1];
17541
+ j += 2;
17542
+ continue;
17543
+ }
17544
+ text += original[j];
17545
+ j += 1;
17546
+ }
17547
+ j += 1;
17548
+ continue;
17549
+ }
17550
+ if (/\s/.test(o) || SHELL_SPLIT_CHARS.has(o)) break;
17551
+ text += o;
17552
+ j += 1;
17553
+ }
17554
+ if (!text || text === "$") return void 0;
17555
+ return { text, end: j };
17556
+ }
17557
+ function isIgnoredRedirectTarget(target) {
17558
+ return target === "/dev/null";
17559
+ }
17560
+ function detectExecShellSynchronousWarnings(command, shell) {
17561
+ const masked = maskQuotedAndEscaped(command);
17562
+ const isPowerShell = shell?.resolvedShell === "powershell";
17563
+ const redirectTargets = [];
17564
+ let backgrounded = false;
17565
+ let i = 0;
17566
+ while (i < masked.length) {
17567
+ const ch = masked[i];
17568
+ if (ch === "&") {
17569
+ const next = masked[i + 1];
17570
+ if (next === "&") {
17571
+ i += 2;
17572
+ continue;
17573
+ }
17574
+ if (next === ">") {
17575
+ let j = i + 2;
17576
+ if (masked[j] === ">") j += 1;
17577
+ const target = readRedirectTarget(command, j);
17578
+ if (target && !isIgnoredRedirectTarget(target.text)) redirectTargets.push(target.text);
17579
+ i = target ? target.end : j;
17580
+ continue;
17581
+ }
17582
+ const prev = masked[i - 1];
17583
+ if (prev === ">" || prev === "<") {
17584
+ i += 1;
17585
+ continue;
17586
+ }
17587
+ if (isPowerShell) {
17588
+ i += 1;
17589
+ continue;
17590
+ }
17591
+ backgrounded = true;
17592
+ i += 1;
17593
+ continue;
17594
+ }
17595
+ if (ch === ">") {
17596
+ const prev = masked[i - 1];
17597
+ if (prev === ">" || prev === "&" || prev === "<") {
17598
+ i += 1;
17599
+ continue;
17600
+ }
17601
+ let j = i + 1;
17602
+ if (masked[j] === ">") j += 1;
17603
+ if (masked[j] === "&") {
17604
+ const ampTarget = readRedirectTarget(command, j + 1);
17605
+ const isFdDuplication = ampTarget !== void 0 && (/^\d+$/.test(ampTarget.text) || ampTarget.text === "-");
17606
+ if (ampTarget && !isFdDuplication && !isIgnoredRedirectTarget(ampTarget.text)) {
17607
+ redirectTargets.push(ampTarget.text);
17608
+ }
17609
+ i = ampTarget ? ampTarget.end : j + 1;
17610
+ continue;
17611
+ }
17612
+ const target = readRedirectTarget(command, j);
17613
+ if (target && !isIgnoredRedirectTarget(target.text)) redirectTargets.push(target.text);
17614
+ i = target ? target.end : j;
17615
+ continue;
17616
+ }
17617
+ if (ch === "<") {
17618
+ i += masked[i + 1] === "<" || masked[i + 1] === "&" ? 2 : 1;
17619
+ continue;
17620
+ }
17621
+ i += 1;
17622
+ }
17623
+ const wordMatch = BG_ESCAPE_WORD_PATTERN.exec(masked);
17624
+ if (wordMatch) backgrounded = true;
17625
+ const warnings = [];
17626
+ if (redirectTargets.length > 0) {
17627
+ const target = redirectTargets[0];
17628
+ warnings.push({
17629
+ kind: "output-redirect",
17630
+ redirectTarget: target,
17631
+ message: `[wake-safety] This execShell call completed synchronously; its output was redirected to ${target}. That file is NOT delivered by any completion notification \u2014 no tracked background task was created. If you need the result, read that file in this turn.`
17632
+ });
17633
+ }
17634
+ if (backgrounded) {
17635
+ warnings.push({
17636
+ kind: "shell-backgrounding",
17637
+ message: `[wake-safety] Detected shell-internal backgrounding (lone \`&\`, nohup/disown/setsid/Start-Process or similar). Descendant processes are NOT tracked as platform tasks and will not produce completion notifications; text such as \`LAUNCHED\` printed by the command is plain stdout, not a platform receipt.`
17638
+ });
17639
+ }
17640
+ return warnings;
17641
+ }
17642
+
17385
17643
  // packages/agent-runtime/capabilities/execShellCapability.ts
17386
17644
  function buildExecShellToolDefinition(toolName = "execShell") {
17387
17645
  return {
17388
17646
  type: "function",
17389
17647
  function: {
17390
17648
  name: toolName,
17391
- description: `Execute a shell command from the workspace root. Prefer one compound command (e.g. 'git status && git diff --stat') to perform complete verification in one step instead of multiple small roundtrips. Do not cd into guessed paths; commands already run from the workspace root. Commands block until exit. Long-running commands (sleep over ${IMMEDIATE_DETACH_SLEEP_THRESHOLD_SECONDS}s, dev servers, watchers) automatically detach to background returning {detached: true, pid, label}; for persistent services, prefer launchProcess. On Windows the resolved shell may be Windows PowerShell 5.1 (metadata.shellKind="powershell5"): it does not support '&&'/'||' or $PSStyle \u2014 join commands with ';' or split into multiple calls. metadata.shellKind="pwsh" means PowerShell 7+ with full syntax. Read resolvedShell/shellKind from any execShell result before writing Windows-specific syntax.`,
17649
+ description: `Execute a shell command from the workspace root. Prefer one compound command (e.g. 'git status && git diff --stat') to perform complete verification in one step instead of multiple small roundtrips. Do not cd into guessed paths; commands already run from the workspace root. Commands block until exit. Long-running commands (sleep over ${IMMEDIATE_DETACH_SLEEP_THRESHOLD_SECONDS}s, dev servers, watchers) automatically detach to background returning {detached: true, pid, label}; for persistent services, prefer launchProcess. On Windows the resolved shell may be Windows PowerShell 5.1 (metadata.shellKind="powershell5"): it does not support '&&'/'||' or $PSStyle \u2014 join commands with ';' or split into multiple calls. metadata.shellKind="pwsh" means PowerShell 7+ with full syntax. Read resolvedShell/shellKind from any execShell result before writing Windows-specific syntax. LIFECYCLE CONTRACT (misreading this contract silenced a real dialog for 10 hours): (1) A normal command waits synchronously for at most 120 seconds; if it ends inside that window the call IS finished \u2014 a result containing exitCode means the command has already completed, never "still running". (2) Only a result with metadata.detached === true AND a taskId represents a tracked background task that may trigger completion wake-up (terminal completion turn carrying a bounded result capsule); that handle may be waited on with taskWait and will produce a completion notification. (3) Text the command itself prints (LAUNCHED, STARTED, BACKGROUND, ...) is plain stdout with zero lifecycle meaning \u2014 never read it as a platform receipt. (4) Shell-level backgrounding (\`&\`, nohup, disown, setsid, self-daemonizing scripts, PowerShell Start-Process) is NOT tracked: those descendants produce no completion notification, and the outer shell finishing tells you nothing about them. Use background: true to turn a finite job into a tracked background task immediately. When a synchronous result's core output was redirected to a file, read that file in the SAME turn \u2014 never end the turn promising a later automatic report without a taskId.`,
17392
17650
  parameters: {
17393
17651
  type: "object",
17394
17652
  properties: {
@@ -17399,6 +17657,10 @@ function buildExecShellToolDefinition(toolName = "execShell") {
17399
17657
  cmd: {
17400
17658
  type: "string",
17401
17659
  description: "Compatibility alias for command."
17660
+ },
17661
+ background: {
17662
+ type: "boolean",
17663
+ description: "Immediately detach this command into a tracked background task (no 120s synchronous wait): returns {detached: true, taskId}, and on completion the platform resumes this conversation with a bounded result capsule (exitCode, duration, stdout/stderr tail). Default false. Use for finite jobs (tests, builds, batch) whose result you need later; NOT for dev servers/watchers \u2014 use launchProcess for those."
17402
17664
  }
17403
17665
  }
17404
17666
  }
@@ -17439,11 +17701,13 @@ function normalizeExecShellInput(input) {
17439
17701
  throw new Error("execShell requires a non-empty command.");
17440
17702
  }
17441
17703
  const activity = extractActivity(record);
17704
+ const background = record.background === true || typeof record.background === "string" && record.background.trim().toLowerCase() === "true";
17442
17705
  return {
17443
17706
  command,
17444
17707
  ...record.shell !== void 0 ? { shell: record.shell } : {},
17445
17708
  ...activity ? { activity } : {},
17446
- ...record.input !== void 0 ? { input: record.input } : {}
17709
+ ...record.input !== void 0 ? { input: record.input } : {},
17710
+ ...background ? { background: true } : {}
17447
17711
  };
17448
17712
  }
17449
17713
  throw new Error("execShell requires a non-empty command.");
@@ -17481,6 +17745,7 @@ var execShellCapability = {
17481
17745
  command,
17482
17746
  shell: normalized.shell
17483
17747
  });
17748
+ const background = normalized.background === true;
17484
17749
  const result4 = await runWorkspaceCommand({
17485
17750
  workspaceRoot,
17486
17751
  command: shellPlan.argv,
@@ -17488,15 +17753,18 @@ var execShellCapability = {
17488
17753
  outputLimit: ctx.commandOutputLimit,
17489
17754
  commandPrefix: ctx.commandPrefix,
17490
17755
  abortSignal: ctx.abortSignal,
17491
- detachMs: isImmediateDetachShellCommand({ command }) ? 0 : ctx.detachMs,
17756
+ detachMs: background ? 0 : isImmediateDetachShellCommand({ command }) ? 0 : ctx.detachMs,
17492
17757
  stdin: typeof normalized.input === "string" ? normalized.input : void 0,
17493
17758
  // Ownership of the turn that invoked this command: an auto-detached
17494
17759
  // command keeps its parent dialog so its terminal can resume that
17495
17760
  // conversation (see processOwnership.ts). Absent ctx.owner = unowned.
17496
17761
  owner: ctx.owner ?? null
17497
17762
  });
17763
+ const warnings = result4.detached || result4.spawnFailed ? [] : detectExecShellSynchronousWarnings(command, {
17764
+ resolvedShell: shellPlan.resolvedShell
17765
+ });
17498
17766
  return {
17499
- content: result4.content,
17767
+ content: warnings.length > 0 ? [result4.content, ...warnings.map((warning) => warning.message)].join("\n\n") : result4.content,
17500
17768
  metadata: {
17501
17769
  command,
17502
17770
  exitCode: result4.exitCode,
@@ -17511,8 +17779,21 @@ var execShellCapability = {
17511
17779
  // Phase 0: stable taskId from the pre-registered Execution
17512
17780
  // Envelope (additive; existing detached fields untouched).
17513
17781
  taskId: result4.taskId,
17514
- status: "running"
17515
- } : {},
17782
+ status: "running",
17783
+ // P0 machine-readable execution state: an explicit positive
17784
+ // signal instead of requiring the model to infer "not detached"
17785
+ // from absence. Mirrors the real lifecycle contract.
17786
+ executionState: "detached",
17787
+ trackedBackgroundTask: true,
17788
+ completionNotification: "terminal-resume"
17789
+ } : {
17790
+ // P0 machine-readable execution state: the command already
17791
+ // finished inside the synchronous window; no background task
17792
+ // exists, so no completion notification will ever arrive.
17793
+ executionState: "synchronous-complete",
17794
+ trackedBackgroundTask: false,
17795
+ completionNotification: "none"
17796
+ },
17516
17797
  ...normalized.activity ? { activity: normalized.activity } : {}
17517
17798
  }
17518
17799
  };
@@ -17728,7 +18009,7 @@ function buildLaunchProcessTool() {
17728
18009
  type: "function",
17729
18010
  function: {
17730
18011
  name: "launchProcess",
17731
- description: "Start a long-running background process (dev server, watcher, REPL) and return immediately with {pid, label, status}. Use listProcesses to inspect or stop it.",
18012
+ description: "Start a long-running ambient background process (dev server, watcher, REPL) and return immediately with {pid, label, taskId, status}. Real boundaries: this registers a tracked background task, but completion does NOT automatically start a new turn \u2014 the process is intentionally notice-only (an exited dev server must not silently burn a model turn). When it reaches a terminal state the bounded result (exitCode, duration, stdout/stderr tail) is recorded and surfaced as a notice injected only on your next real turn in the same conversation. Use listProcesses to inspect, taskWait to block for its terminal state, or taskStop to stop it. For a finite job (tests, builds, batch) whose result must resume the conversation automatically, use execShell with background: true instead.",
17732
18013
  parameters: {
17733
18014
  type: "object",
17734
18015
  properties: {
@@ -18869,6 +19150,55 @@ async function execShellTool(args2) {
18869
19150
  }
18870
19151
  );
18871
19152
  }
19153
+ var LAUNCH_PROCESS_STREAM_RING_CHARS = 65536;
19154
+ var LAUNCH_PROCESS_STREAM_CAPTURE_MAX_CHARS = 4e6;
19155
+ function createLaunchProcessStreamCapture() {
19156
+ let ring = "";
19157
+ let full = null;
19158
+ let capturedChars = 0;
19159
+ let totalChars = 0;
19160
+ let totalNewlines = 0;
19161
+ return {
19162
+ push(chunk) {
19163
+ const str = typeof chunk === "string" ? chunk : chunk.toString("utf8");
19164
+ if (!str) return;
19165
+ totalChars += str.length;
19166
+ for (let i = 0; i < str.length; i += 1) {
19167
+ if (str.charCodeAt(i) === 10) totalNewlines += 1;
19168
+ }
19169
+ const combined = ring + str;
19170
+ if (combined.length <= LAUNCH_PROCESS_STREAM_RING_CHARS) {
19171
+ ring = combined;
19172
+ return;
19173
+ }
19174
+ if (full === null) {
19175
+ full = [combined];
19176
+ capturedChars = combined.length;
19177
+ } else if (capturedChars < LAUNCH_PROCESS_STREAM_CAPTURE_MAX_CHARS) {
19178
+ const room = LAUNCH_PROCESS_STREAM_CAPTURE_MAX_CHARS - capturedChars;
19179
+ const slice = str.length <= room ? str : str.slice(0, room);
19180
+ full.push(slice);
19181
+ capturedChars += slice.length;
19182
+ }
19183
+ ring = combined.slice(-LAUNCH_PROCESS_STREAM_RING_CHARS);
19184
+ },
19185
+ text() {
19186
+ return full === null ? ring : full.join("");
19187
+ },
19188
+ tailText() {
19189
+ return ring;
19190
+ },
19191
+ evicted() {
19192
+ return full !== null;
19193
+ },
19194
+ totalChars() {
19195
+ return totalChars;
19196
+ },
19197
+ totalLines() {
19198
+ return totalChars > 0 ? totalNewlines + 1 : 0;
19199
+ }
19200
+ };
19201
+ }
18872
19202
  async function launchProcessTool(args2) {
18873
19203
  const parsed = parseWorkspaceToolArguments(args2.call.arguments);
18874
19204
  const command = requireShellCommand(parsed, args2.call.name);
@@ -18896,6 +19226,14 @@ async function launchProcessTool(args2) {
18896
19226
  stdio: ["ignore", "pipe", "pipe"],
18897
19227
  detached
18898
19228
  });
19229
+ const stdoutCapture = createLaunchProcessStreamCapture();
19230
+ const stderrCapture = createLaunchProcessStreamCapture();
19231
+ proc.stdout?.on("data", (chunk) => stdoutCapture.push(chunk));
19232
+ proc.stderr?.on("data", (chunk) => stderrCapture.push(chunk));
19233
+ proc.stdout?.on("error", () => {
19234
+ });
19235
+ proc.stderr?.on("error", () => {
19236
+ });
18899
19237
  const pid = proc.pid;
18900
19238
  if (typeof pid !== "number") {
18901
19239
  return {
@@ -18936,7 +19274,38 @@ async function launchProcessTool(args2) {
18936
19274
  process.removeListener("SIGTERM", cleanupChildOnHostSignal);
18937
19275
  process.removeListener("SIGINT", cleanupChildOnHostSignal);
18938
19276
  }
18939
- registry2.markExited(pid, code ?? 1);
19277
+ const exitCode = code ?? 1;
19278
+ let capsule;
19279
+ try {
19280
+ capsule = buildProcessTaskResultCapsule({
19281
+ // The full captured output (bounded prefix when the process was
19282
+ // extremely chatty): the module tails it for the inline capsule and
19283
+ // spills it for recovery. Provenance carries the live original-stream
19284
+ // counters so truncated/spill metadata never derives from the ring.
19285
+ stdout: stdoutCapture.text(),
19286
+ stderr: stderrCapture.text(),
19287
+ stdoutTail: stdoutCapture.tailText(),
19288
+ stderrTail: stderrCapture.tailText(),
19289
+ exitCode,
19290
+ // Duration measured from the registry envelope's startedAt (spawn).
19291
+ startedAt: envelope.startedAt,
19292
+ workspaceRoot: args2.workspaceRoot,
19293
+ streamProvenance: {
19294
+ stdout: {
19295
+ evicted: stdoutCapture.evicted(),
19296
+ totalChars: stdoutCapture.totalChars(),
19297
+ totalLines: stdoutCapture.totalLines()
19298
+ },
19299
+ stderr: {
19300
+ evicted: stderrCapture.evicted(),
19301
+ totalChars: stderrCapture.totalChars(),
19302
+ totalLines: stderrCapture.totalLines()
19303
+ }
19304
+ }
19305
+ });
19306
+ } catch {
19307
+ }
19308
+ registry2.markExited(pid, exitCode, capsule);
18940
19309
  });
18941
19310
  const activity = extractActivity(parsed);
18942
19311
  const resultData = { pid, label, taskId: envelope.taskId, status: "running" };
@@ -18948,6 +19317,13 @@ async function launchProcessTool(args2) {
18948
19317
  label,
18949
19318
  status: "running",
18950
19319
  taskId: envelope.taskId,
19320
+ // P0 machine-readable execution state (consistent with the real wake
19321
+ // contract): a real background task from birth, but ambient — its
19322
+ // terminal result rides the next real turn's notice, not an automatic
19323
+ // completion turn.
19324
+ executionState: "background",
19325
+ trackedBackgroundTask: true,
19326
+ completionNotification: "next-turn-notice",
18951
19327
  processLaunch: resultData,
18952
19328
  ...activity ? { activity } : {}
18953
19329
  }