newmark-agent 0.6.3 → 0.6.7

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 (72) hide show
  1. package/config.example.json +13 -3
  2. package/dist/cli-commands.d.ts +1 -1
  3. package/dist/cli-commands.js +53 -2
  4. package/dist/cli-discovery.js +3 -0
  5. package/dist/conversation-utility-host.bundle.cjs +3046 -1038
  6. package/dist/conversation-utility-host.js +94 -7
  7. package/dist/core/agent.d.ts +149 -12
  8. package/dist/core/agent.js +843 -176
  9. package/dist/core/agentKernelRunner.d.ts +7 -1
  10. package/dist/core/agentKernelRunner.js +171 -22
  11. package/dist/core/autoRouter.d.ts +49 -44
  12. package/dist/core/autoRouter.js +117 -302
  13. package/dist/core/branchIdentity.d.ts +29 -0
  14. package/dist/core/branchIdentity.js +54 -0
  15. package/dist/core/config.js +27 -11
  16. package/dist/core/continuation/contracts.d.ts +1 -1
  17. package/dist/core/continuation/store.d.ts +119 -1
  18. package/dist/core/continuation/store.js +337 -19
  19. package/dist/core/conversationKernel.d.ts +62 -8
  20. package/dist/core/conversationKernel.js +361 -47
  21. package/dist/core/conversationStateDocument.d.ts +18 -0
  22. package/dist/core/conversationStateDocument.js +88 -0
  23. package/dist/core/conversationVisualBudget.d.ts +74 -0
  24. package/dist/core/conversationVisualBudget.js +153 -0
  25. package/dist/core/electronUtilityAgentClient.d.ts +3 -2
  26. package/dist/core/electronUtilityAgentClient.js +32 -8
  27. package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
  28. package/dist/core/electronUtilityRuntimePool.js +93 -24
  29. package/dist/core/hostRuntimeHooks.d.ts +23 -0
  30. package/dist/core/hostRuntimeHooks.js +17 -0
  31. package/dist/core/jevDecision.d.ts +144 -0
  32. package/dist/core/jevDecision.js +226 -0
  33. package/dist/core/memoryProbe.d.ts +24 -0
  34. package/dist/core/memoryProbe.js +104 -0
  35. package/dist/core/mobilePairing.d.ts +7 -1
  36. package/dist/core/mobilePairing.js +9 -1
  37. package/dist/core/performanceDiagnostics.d.ts +1 -1
  38. package/dist/core/routeDecisionValidator.d.ts +86 -0
  39. package/dist/core/routeDecisionValidator.js +249 -0
  40. package/dist/core/routeEligibility.d.ts +52 -0
  41. package/dist/core/routeEligibility.js +108 -0
  42. package/dist/core/runtimeMemoryBudget.d.ts +6 -0
  43. package/dist/core/runtimeMemoryBudget.js +12 -0
  44. package/dist/core/utilityAgentProtocol.d.ts +8 -10
  45. package/dist/core/utilityHostToolRouter.js +2 -0
  46. package/dist/core/visualDownscale.d.ts +6 -0
  47. package/dist/core/visualDownscale.js +137 -0
  48. package/dist/core/wslAgentClient.d.ts +1 -2
  49. package/dist/core/wslAgentClient.js +0 -8
  50. package/dist/core/wslAgentProtocol.d.ts +1 -10
  51. package/dist/core/wslAgentRuntimePool.d.ts +1 -3
  52. package/dist/core/wslAgentRuntimePool.js +17 -22
  53. package/dist/llm/provider.d.ts +3 -1
  54. package/dist/llm/provider.js +53 -9
  55. package/dist/main.js +108 -31
  56. package/dist/preload.js +3 -1
  57. package/dist/server.js +30 -18
  58. package/dist/tools/computerUse.d.ts +4 -0
  59. package/dist/tools/computerUse.js +202 -4
  60. package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
  61. package/dist/tools/computerUsePowerShellHost.js +105 -31
  62. package/dist/tools/index.js +4 -1
  63. package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
  64. package/dist/tui/src/i18n.js +12 -1
  65. package/dist/tui/src/render.js +9 -2
  66. package/dist/tui/src/settings-schema.js +19 -0
  67. package/dist/tui/src/state.js +69 -1
  68. package/dist/ui/index.html +994 -277
  69. package/dist/ui/lucide-sprite.svg +0 -8
  70. package/dist/wsl-agent-host.bundle.cjs +2851 -1013
  71. package/dist/wsl-agent-host.js +0 -3
  72. package/package.json +38 -9
@@ -1,10 +1,54 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.ConversationKernel = void 0;
4
37
  const agent_1 = require("./agent");
5
38
  const store_1 = require("./continuation/store");
6
39
  const crypto_1 = require("crypto");
40
+ const path = __importStar(require("path"));
7
41
  const conversationTarget_1 = require("./conversationTarget");
42
+ /**
43
+ * dev-0.6.6 recovery policy for orphaned queue attempts.
44
+ *
45
+ * A live owner renews its lease every `leaseRenewalMs` (20s), so a lease that
46
+ * has been expired for this grace window means the owner stopped renewing (it
47
+ * was killed, crashed or lost its event loop). Dead-owner PIDs are reclaimed
48
+ * immediately; the grace window only guards against PID reuse and clock skew.
49
+ */
50
+ const ORPHAN_RECOVERY_GRACE_MS = 120_000;
51
+ const ORPHAN_SWEEP_INTERVAL_MS = 5_000;
8
52
  function changedLineCount(value) {
9
53
  if (!value)
10
54
  return 0;
@@ -28,6 +72,103 @@ class ConversationKernel {
28
72
  listeners = new Set();
29
73
  eventLimit = 500;
30
74
  generations = new Map();
75
+ /**
76
+ * Durable continuation ledgers are read on every queue projection and state
77
+ * snapshot. Building a fresh store each time re-parsed (and previously also
78
+ * re-wrote) the whole JSON ledger, so a long conversation became slower with
79
+ * every turn. The store revalidates its cache through the ledger file
80
+ * fingerprint on each read, so one instance per branch stays authoritative.
81
+ */
82
+ continuationStores = new Map();
83
+ /** Throttles orphan sweeps per branch so snapshots stay read-only and cheap. */
84
+ orphanSweepAt = new Map();
85
+ /**
86
+ * dev-0.6.6: is the process that owns an attempt still alive?
87
+ *
88
+ * Attempts record their owner as `kernel:<pid>`. A dead owner can never write
89
+ * a terminal state, so its build/guard must be reclaimed by a live reader
90
+ * instead of waiting for a claim that nobody will ever make.
91
+ */
92
+ workerCapabilityAlive(capability) {
93
+ const match = /:(\d+)$/.exec(String(capability || ''));
94
+ if (!match)
95
+ return true; // Unknown owners (for example flow-external) are never stolen by liveness.
96
+ const pid = Number(match[1]);
97
+ if (!Number.isFinite(pid) || pid <= 0)
98
+ return true;
99
+ if (pid === process.pid)
100
+ return true;
101
+ try {
102
+ process.kill(pid, 0);
103
+ return true;
104
+ }
105
+ catch (error) {
106
+ const code = error && typeof error === 'object' ? String(error.code || '') : '';
107
+ return code !== 'ESRCH';
108
+ }
109
+ }
110
+ /**
111
+ * Reclaim an orphaned queued turn (dead owner / long-expired lease) and, when
112
+ * the conversation is idle and its queue is not paused, let the queue run
113
+ * again. Field evidence: a utility host died of V8 OOM mid-turn and left the
114
+ * build RUNNING with an expired guard, so every later queued row entered the
115
+ * panel but never executed.
116
+ */
117
+ maybeRecoverOrphanedAttempt(runtime, reason) {
118
+ const continuation = this.continuationStore(runtime);
119
+ if (!continuation)
120
+ return false;
121
+ let active = null;
122
+ try {
123
+ active = continuation.store.activeAttempt({
124
+ workspaceId: continuation.workspaceId,
125
+ rootId: continuation.rootId,
126
+ branchId: continuation.branchId,
127
+ });
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ if (!active || !active.buildId)
133
+ return false;
134
+ const now = Date.now();
135
+ const ownerAlive = this.workerCapabilityAlive(active.workerCapability);
136
+ const leaseExpiredLongAgo = active.leaseUntil > 0 && active.leaseUntil + ORPHAN_RECOVERY_GRACE_MS <= now;
137
+ const danglingAttempt = active.status !== 'RUNNING';
138
+ if (!danglingAttempt && ownerAlive && !leaseExpiredLongAgo)
139
+ return false;
140
+ const sweepKey = `${continuation.workspaceId}::${continuation.rootId}::${continuation.branchId}`;
141
+ if (!danglingAttempt && (this.orphanSweepAt.get(sweepKey) || 0) + ORPHAN_SWEEP_INTERVAL_MS > now)
142
+ return false;
143
+ this.orphanSweepAt.set(sweepKey, now);
144
+ let outcome = { recovered: false };
145
+ try {
146
+ outcome = continuation.store.recoverOrphanedAttempt({
147
+ workspaceId: continuation.workspaceId,
148
+ rootId: continuation.rootId,
149
+ branchId: continuation.branchId,
150
+ graceMs: ORPHAN_RECOVERY_GRACE_MS,
151
+ reason,
152
+ ownerAlive: capability => this.workerCapabilityAlive(capability),
153
+ });
154
+ }
155
+ catch {
156
+ return false;
157
+ }
158
+ if (!outcome.recovered)
159
+ return false;
160
+ runtime.runner.recordWorkStatus(`Recovered an orphaned queued turn (${outcome.reason || reason}).`);
161
+ this.emitQueueUpdate(runtime);
162
+ // The reclaim only helps if the queue actually moves again.
163
+ if (runtime.pendingNextTurn.length
164
+ && !runtime.queuePaused
165
+ && !runtime.preparingArchive
166
+ && !runtime.externalOwner?.active) {
167
+ runtime.runId ||= (0, crypto_1.randomUUID)();
168
+ this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
169
+ }
170
+ return true;
171
+ }
31
172
  constructor(root, host, automation, lifecycle = {}) {
32
173
  this.root = root;
33
174
  this.host = host;
@@ -284,7 +425,25 @@ class ConversationKernel {
284
425
  runtime.externalOwner = undefined;
285
426
  this.setQueuePaused(target, paused);
286
427
  }
287
- async prepareForArchive(target) {
428
+ /**
429
+ * Start an archive for one conversation.
430
+ *
431
+ * An archive has to wait for the conversation's own run to settle before the
432
+ * writer takes its payload snapshot and deletes the state entry: a run that
433
+ * is still writing would otherwise append to a conversation that no longer
434
+ * exists (silently losing the tail), and its trailing state flush would
435
+ * resurrect the archived entry. That wait is not a budget to be truncated —
436
+ * it is real work, and the way to end it is to *release the occupancy* the
437
+ * run is holding, not to give up on it.
438
+ *
439
+ * So the caller passes the hard stop of the resident runtime as
440
+ * `releaseOccupancy`. It runs concurrently with the settle below: the hard
441
+ * stop invalidates the in-flight request (the client rejects every pending
442
+ * request of the evicted generation), which is exactly what makes a wedged
443
+ * run finish. Both teardown paths therefore overlap instead of serializing,
444
+ * and no timer exists to mask a runtime that refuses to end.
445
+ */
446
+ async prepareForArchive(target, options = {}) {
288
447
  const runtime = this.findRuntime(target);
289
448
  if (!runtime)
290
449
  return null;
@@ -296,12 +455,29 @@ class ConversationKernel {
296
455
  clearTimeout(runtime.goalContinuationTimer);
297
456
  runtime.goalContinuationTimer = undefined;
298
457
  runtime.runner.abortActiveKernelRun('user_stop');
299
- if (runtime.activePromise) {
300
- runtime.stopRequestedRunId = runtime.runId;
458
+ // The release is deliberately started before the settle is awaited so the
459
+ // two never serialize. A failing hard stop must not turn the archive into a
460
+ // failure on its own; the archived state is still derived from the locked
461
+ // writer, and the settle below stays authoritative.
462
+ const occupancyRelease = (async () => {
463
+ if (!options.releaseOccupancy)
464
+ return;
301
465
  try {
302
- await runtime.activePromise;
466
+ await options.releaseOccupancy();
467
+ }
468
+ catch (error) {
469
+ console.error('[Newmark] archive occupancy release failed:', error instanceof Error ? error.message : String(error));
303
470
  }
304
- catch { /* archive owns the terminal outcome */ }
471
+ })();
472
+ if (runtime.activePromise) {
473
+ runtime.stopRequestedRunId = runtime.runId;
474
+ // `catch` the settle so a throwing run still lets the parallel release
475
+ // finish; the archive owns the terminal outcome either way.
476
+ const settled = runtime.activePromise.catch(() => undefined);
477
+ await Promise.all([settled, occupancyRelease]);
478
+ }
479
+ else {
480
+ await occupancyRelease;
305
481
  }
306
482
  runtime.runner.flushWorkspaceConversationState();
307
483
  return runtime.runner;
@@ -388,6 +564,23 @@ class ConversationKernel {
388
564
  const runtime = this.findRuntime(target);
389
565
  if (!runtime)
390
566
  return [];
567
+ // dev-0.6.4 hotfix: 把账本里的受阻原因带进队列投影,界面才能显示
568
+ // “被失败前置阻断”而不是无声卡住。
569
+ let waitingByBuildId = new Map();
570
+ const continuation = this.continuationStore(runtime);
571
+ if (continuation) {
572
+ try {
573
+ // Only the queued rows matter here; the full snapshot would clone every
574
+ // build of the conversation on every state read.
575
+ const reasons = continuation.store.queuedBlockedReasons({
576
+ workspaceId: continuation.workspaceId,
577
+ rootId: continuation.rootId,
578
+ branchId: continuation.branchId,
579
+ });
580
+ waitingByBuildId = new Map(Object.entries(reasons).map(([buildId, reason]) => [buildId, String(reason)]));
581
+ }
582
+ catch { /* the projection stays usable without ledger diagnostics */ }
583
+ }
391
584
  return runtime.pendingNextTurn.flatMap(item => {
392
585
  if (item.queueMode !== 'followUp')
393
586
  return [];
@@ -396,6 +589,7 @@ class ConversationKernel {
396
589
  const id = String(item.message.clientMessageId || '');
397
590
  if (!id)
398
591
  return [];
592
+ const buildId = String(item.message.buildId || '');
399
593
  return [{
400
594
  id,
401
595
  buildId: item.message.buildId,
@@ -409,6 +603,8 @@ class ConversationKernel {
409
603
  branchNodeId: item.message.branchNodeId,
410
604
  branchPath: item.message.branchPath?.slice(),
411
605
  modelSelection: item.message.modelSelection,
606
+ waitingReason: waitingByBuildId.get(buildId),
607
+ blocked: waitingByBuildId.has(buildId),
412
608
  }];
413
609
  });
414
610
  }
@@ -452,7 +648,12 @@ class ConversationKernel {
452
648
  const branchId = branchNodeIdOverride.includes('::')
453
649
  ? branchNodeIdOverride
454
650
  : `${rootId}::${branchNodeIdOverride || branchIdentity.branchNodeId || 'main'}`;
455
- const store = new store_1.GuardedContinuationStore(workspace.path, workspaceId);
651
+ const storeKey = `${path.resolve(workspace.path)}::${workspaceId}::${rootId}::${branchId}`;
652
+ let store = this.continuationStores.get(storeKey);
653
+ if (!store) {
654
+ store = new store_1.GuardedContinuationStore(workspace.path, workspaceId);
655
+ this.continuationStores.set(storeKey, store);
656
+ }
456
657
  store.ensureRoot(rootId, branchId);
457
658
  return { store, workspaceId, rootId, branchId };
458
659
  }
@@ -484,6 +685,8 @@ class ConversationKernel {
484
685
  const ledgerBranchId = continuation?.branchId;
485
686
  let buildId = '';
486
687
  if (continuation) {
688
+ // 入队严格保持 0.6.3 语义:新命令接在当前队尾之后。被失败前置阻断的行不会
689
+ // 被静默跳过;用户可通过界面上的“修复受阻队列”显式动作重建可执行链。
487
690
  const authoritative = continuation.store.snapshot({
488
691
  workspaceId: continuation.workspaceId,
489
692
  rootId: continuation.rootId,
@@ -555,6 +758,10 @@ class ConversationKernel {
555
758
  }]);
556
759
  this.trackQueuedMessage(runtime, prompt, 'followUp');
557
760
  this.emitQueueUpdate(runtime);
761
+ // A new queue command is also an explicit "make the queue work" signal: if
762
+ // the branch guard is still held by a crashed owner, reclaim it now instead
763
+ // of silently piling the new row behind a dead attempt.
764
+ this.maybeRecoverOrphanedAttempt(runtime, 'queue-enqueue');
558
765
  if (!runtime.activePromise && !runtime.externalOwner && !runtime.queuePaused)
559
766
  this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
560
767
  return this.queueItems(runtime.target).find(item => item.id === id);
@@ -708,6 +915,10 @@ class ConversationKernel {
708
915
  return this.queueItems(runtime.target);
709
916
  }
710
917
  setQueuePaused(target, paused) {
918
+ // 队列策略只作用于已经存在的 runtime:在这里按需重建 runtime 会让空闲会话
919
+ // 重新变成“持有排队工作”的所有者,破坏工作区清理契约
920
+ // (`Cannot release a conversation with running or queued work`)。界面路径统一
921
+ // 走 queueAction,它已经负责按需创建 runtime。
711
922
  const runtime = this.findRuntime(target);
712
923
  if (!runtime)
713
924
  throw new Error('Target conversation runtime is unavailable');
@@ -720,6 +931,35 @@ class ConversationKernel {
720
931
  }
721
932
  return runtime.queuePaused;
722
933
  }
934
+ /**
935
+ * dev-0.6.4 hotfix: 显式修复被失败前置阻断的排队行。
936
+ *
937
+ * 0.6.3 的合约是「失败/取消阻断后继,且不静默跳过」,所以这里不挂在恢复动作上
938
+ * 自动执行;它必须由用户的显式动作触发(界面上的“修复受阻队列”按钮)。每一次
939
+ * 改挂都写入 `BuildQueueRepaired` 事件并发布可见状态,触发者与原因可审计。
940
+ */
941
+ repairBlockedQueueNow(runtime) {
942
+ const continuation = this.continuationStore(runtime);
943
+ if (!continuation)
944
+ return 0;
945
+ try {
946
+ const result = continuation.store.repairBlockedQueue({
947
+ workspaceId: continuation.workspaceId,
948
+ rootId: continuation.rootId,
949
+ branchId: continuation.branchId,
950
+ reason: 'explicit-user-repair',
951
+ });
952
+ if (result.repaired.length) {
953
+ runtime.runner.recordWorkStatus(`${result.repaired.length} queued row(s) blocked by a failed predecessor were re-anchored to the last committed build; the queue resumes in order.`);
954
+ this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
955
+ }
956
+ return result.repaired.length;
957
+ }
958
+ catch (error) {
959
+ runtime.runner.recordWorkStatus(`Queue repair failed: ${error instanceof Error ? error.message : String(error)}`);
960
+ return 0;
961
+ }
962
+ }
723
963
  queueAction(target, action, input = {}) {
724
964
  const normalized = this.normalizeTarget(target);
725
965
  const runtime = this.findRuntime(normalized) || this.runtime(normalized, {
@@ -758,6 +998,10 @@ class ConversationKernel {
758
998
  else if (action === 'set_pause') {
759
999
  this.setQueuePaused(runtime.target, input.paused === true);
760
1000
  }
1001
+ else if (action === 'repair_blocked') {
1002
+ // 显式修复:把被失败前置阻断的排队行重新挂到最后一个已提交 Build。
1003
+ this.repairBlockedQueueNow(runtime);
1004
+ }
761
1005
  else if (action === 'guide') {
762
1006
  if (input.text !== undefined)
763
1007
  this.updateQueueItem(runtime.target, String(input.id || ''), input.text, input);
@@ -823,6 +1067,11 @@ class ConversationKernel {
823
1067
  runtime.queuePaused = true;
824
1068
  runtime.runId ||= (0, crypto_1.randomUUID)();
825
1069
  }
1070
+ // Every state projection is a chance to notice that the durable queue is
1071
+ // held by a dead owner (host crash) and reclaim it. The sweep is throttled
1072
+ // and read-only unless an orphan actually exists.
1073
+ if (runtime)
1074
+ this.maybeRecoverOrphanedAttempt(runtime, 'state-projection');
826
1075
  const conversationSnapshot = runner.getConversationSnapshot(normalized.conversationId, options);
827
1076
  return {
828
1077
  ...conversationSnapshot,
@@ -852,9 +1101,6 @@ class ConversationKernel {
852
1101
  conversationLocked: false,
853
1102
  routeDecision: runner.lastRouteDecision,
854
1103
  resolvedDeployment: runner.activeDeployment(),
855
- autoRouteRatingAvailable: runner.model === 'auto'
856
- && runner.lastRouteDecision?.requestedSelection.kind === 'auto'
857
- && !!runner.lastRouteDecision.resolvedDeployment,
858
1104
  };
859
1105
  }
860
1106
  runtimeStates() {
@@ -1089,12 +1335,6 @@ class ConversationKernel {
1089
1335
  displayHistory: { untouched: true, messageCount: runner.chatMessages.length },
1090
1336
  };
1091
1337
  }
1092
- rateAutoRoute(target, score, expectedRouteId = '') {
1093
- const runtime = this.findRuntime(target);
1094
- if (!runtime)
1095
- return { ok: false, reason: 'no_active_auto_route' };
1096
- return runtime.runner.rateActiveAutoRoute(score, expectedRouteId);
1097
- }
1098
1338
  setWorkRunExpanded(target, runId, expanded) {
1099
1339
  const normalized = this.normalizeTarget(target);
1100
1340
  const runtime = this.findRuntime(normalized);
@@ -1413,7 +1653,26 @@ class ConversationKernel {
1413
1653
  this.mirrorHostIfTargetActive(runtime);
1414
1654
  return this.result(runtime, lastTokens);
1415
1655
  }
1656
+ // Wedge guard: this loop owns the root-subagent-inbox follow-up handoff. A
1657
+ // root message whose prompt is already pending produces no state change on
1658
+ // the next iteration; without a guard that becomes an unbounded busy loop
1659
+ // (observed: a single gate burning a full core with the turn never settling).
1660
+ let stalledIterations = 0;
1661
+ let lastLoopSignature = '';
1416
1662
  for (;;) {
1663
+ const loopSignature = `${runtime.pendingNextTurn.length}:${runtime.runId}:${runtime.queuePaused ? 1 : 0}`;
1664
+ if (loopSignature === lastLoopSignature)
1665
+ stalledIterations += 1;
1666
+ else {
1667
+ lastLoopSignature = loopSignature;
1668
+ stalledIterations = 0;
1669
+ }
1670
+ if (stalledIterations > 250) {
1671
+ if (process.env.NEWMARK_KERNEL_LOOP_DIAGNOSTICS === '1') {
1672
+ console.error(`[NewmarkKernel] run loop yielded after ${stalledIterations} stalled iterations (runId=${runtime.runId}, pending=${runtime.pendingNextTurn.length}, queuePaused=${!!runtime.queuePaused})`);
1673
+ }
1674
+ break;
1675
+ }
1417
1676
  while (!runtime.queuePaused) {
1418
1677
  if (runtime.stopRequestedRunId === runtime.runId)
1419
1678
  return this.result(runtime, lastTokens);
@@ -1675,6 +1934,9 @@ class ConversationKernel {
1675
1934
  && runtime.queued.followUp.length === 0
1676
1935
  && runner.subagents.readRootInbox().length === 0;
1677
1936
  });
1937
+ // 执行投影严格跟随当前激活分支节点:新分支绝不接管另一条分页的排队输入
1938
+ // (dev-0.6.3 的既有契约)。界面上跨分页的排队行可见性由渲染层负责,
1939
+ // 执行始终属于受理时捕获的那条分支。
1678
1940
  for (const continuation of runner.conversationContinuations()) {
1679
1941
  runtime.pendingNextTurn.push({
1680
1942
  message: continuation.clientMessageId || continuation.images?.length || continuation.hiddenUserInput
@@ -1763,6 +2025,10 @@ class ConversationKernel {
1763
2025
  // An already-read inbox entry is not replayed by the mailbox listener.
1764
2026
  // Reconcile saved result receipts after hydrating its continuation copy.
1765
2027
  runner.acknowledgeSubagentSettlementReceipts();
2028
+ // A runtime is rebuilt after every host restart (including a crash). If the
2029
+ // previous host died mid-turn, its durable attempt is still RUNNING: heal it
2030
+ // here so the conversation can continue instead of sitting on a dead guard.
2031
+ this.maybeRecoverOrphanedAttempt(runtime, 'runtime-created');
1766
2032
  return runtime;
1767
2033
  }
1768
2034
  scheduleGoalContinuation(runtime, completedRunId) {
@@ -1826,6 +2092,57 @@ class ConversationKernel {
1826
2092
  : { ...delivery, runId: undefined };
1827
2093
  const continuation = this.continuationStore(runtime);
1828
2094
  let scope = null;
2095
+ let leaseHeartbeat;
2096
+ let scopeSettled = false;
2097
+ const stopLeaseHeartbeat = () => {
2098
+ if (leaseHeartbeat)
2099
+ clearInterval(leaseHeartbeat);
2100
+ leaseHeartbeat = undefined;
2101
+ };
2102
+ /**
2103
+ * dev-0.6.6: an admitted queued turn must settle its durable scope on
2104
+ * every exit path. Field evidence: when the utility host process died
2105
+ * (V8 OOM) mid-turn, the build stayed RUNNING and the guard kept
2106
+ * pointing at the dead attempt, so the queue never ran again and could
2107
+ * not be repaired from the UI. Commit / fail / requeue now converge on
2108
+ * one idempotent settlement that also stops the lease heartbeat.
2109
+ */
2110
+ const settleLedgerScope = (outcome, detail) => {
2111
+ if (!continuation || !scope || scopeSettled)
2112
+ return;
2113
+ scopeSettled = true;
2114
+ stopLeaseHeartbeat();
2115
+ const owned = scope;
2116
+ const requeue = () => continuation.store.requeueBuild({
2117
+ workspaceId: continuation.workspaceId,
2118
+ rootId: continuation.rootId,
2119
+ branchId: continuation.branchId,
2120
+ buildId: owned.buildId,
2121
+ });
2122
+ try {
2123
+ if (outcome === 'commit') {
2124
+ continuation.store.commitFinal({ scope: owned, text: String(detail.text || '') });
2125
+ return;
2126
+ }
2127
+ if (outcome === 'fail') {
2128
+ continuation.store.failBuild({
2129
+ scope: owned,
2130
+ reason: detail.error instanceof Error ? detail.error.message : String(detail.error || 'queued turn failed'),
2131
+ });
2132
+ return;
2133
+ }
2134
+ requeue();
2135
+ }
2136
+ catch {
2137
+ // Ownership may have been recovered elsewhere (dead-owner recovery
2138
+ // or an explicit repair). Never leave the durable scope RUNNING;
2139
+ // requeue is idempotent and a no-op for terminal builds.
2140
+ try {
2141
+ requeue();
2142
+ }
2143
+ catch { /* already terminal or owned elsewhere */ }
2144
+ }
2145
+ };
1829
2146
  void this.runPendingQueueItem(runtime, next, async () => {
1830
2147
  this.bindQueuedTurnBranch(runtime, queued);
1831
2148
  if (queued?.modelSelection)
@@ -1867,41 +2184,41 @@ class ConversationKernel {
1867
2184
  this.emitQueueUpdate(runtime);
1868
2185
  return this.result(runtime, []);
1869
2186
  }
2187
+ // The lease is a crash detector, not a wall-clock budget: renew it
2188
+ // while the attempt is genuinely alive. Without this every turn
2189
+ // longer than the 60s lease could be taken over mid-run, and the
2190
+ // real owner's terminal write was rejected as STALE_EXECUTION.
2191
+ leaseHeartbeat = setInterval(() => {
2192
+ if (!continuation || !scope)
2193
+ return;
2194
+ try {
2195
+ if (!continuation.store.heartbeat(scope))
2196
+ stopLeaseHeartbeat();
2197
+ }
2198
+ catch {
2199
+ stopLeaseHeartbeat();
2200
+ }
2201
+ }, continuation.store.leaseRenewalMs);
2202
+ if (typeof leaseHeartbeat.unref === 'function')
2203
+ leaseHeartbeat.unref();
1870
2204
  }
1871
2205
  const result = await this.prompt(message, runtime.target, runtime.options, 'followUp');
1872
- if (continuation && scope) {
1873
- continuation.store.commitFinal({
1874
- scope,
1875
- text: result.tokens.map(token => token.text || '').join(''),
1876
- });
1877
- }
2206
+ settleLedgerScope('commit', { text: result.tokens.map(token => token.text || '').join('') });
1878
2207
  return result;
1879
2208
  }, (accepted, error) => {
1880
- if (!continuation || !scope)
1881
- return;
1882
- const reason = error instanceof Error ? error.message : String(error);
1883
- if (accepted) {
1884
- try {
1885
- continuation.store.failBuild({ scope, reason });
1886
- }
1887
- catch { /* stop/archive may have already released the guard */ }
1888
- }
1889
- else {
1890
- // The input was never accepted. Stop/archive may already have
1891
- // released the guard, so use the idempotent requeue path: the same
1892
- // durable build id stays available for an explicit resume.
1893
- try {
1894
- continuation.store.requeueBuild({
1895
- workspaceId: continuation.workspaceId,
1896
- rootId: continuation.rootId,
1897
- branchId: continuation.branchId,
1898
- buildId: scope.buildId,
1899
- });
1900
- }
1901
- catch { /* the build may already be committed or explicitly cancelled */ }
1902
- }
2209
+ // A provider failure after acceptance must never replay the already
2210
+ // submitted user input; an unaccepted row stays available for resume.
2211
+ settleLedgerScope(accepted ? 'fail' : 'requeue', { error });
1903
2212
  }).catch(() => {
1904
2213
  // Agent.process and the work-run finalizer already publish the error.
2214
+ }).finally(() => {
2215
+ stopLeaseHeartbeat();
2216
+ // Safety net: an execution that resolved without any terminal ledger
2217
+ // write (for example a cooperative stop resolving outside the error
2218
+ // path) must still leave the scope recoverable instead of RUNNING.
2219
+ if (continuation && scope && !scopeSettled) {
2220
+ settleLedgerScope('requeue', { error: 'queued turn settled without a terminal ledger write' });
2221
+ }
1905
2222
  });
1906
2223
  return;
1907
2224
  }
@@ -2105,9 +2422,6 @@ class ConversationKernel {
2105
2422
  workRuns: this.bindWorkRunsToRuntimeTarget(runtime.runner.getConversationSnapshot(runtime.id).workRuns, runtime.target),
2106
2423
  routeDecision: runtime.runner.lastRouteDecision,
2107
2424
  resolvedDeployment: runtime.runner.activeDeployment(),
2108
- autoRouteRatingAvailable: runtime.runner.model === 'auto'
2109
- && runtime.runner.lastRouteDecision?.requestedSelection.kind === 'auto'
2110
- && !!runtime.runner.lastRouteDecision.resolvedDeployment,
2111
2425
  };
2112
2426
  }
2113
2427
  /**
@@ -0,0 +1,18 @@
1
+ export type ConversationStateRecord = Record<string, unknown>;
2
+ export interface ConversationStateDocument {
3
+ /** Absolute path of the workspace index document. */
4
+ path: string;
5
+ /** Index document exactly as stored (no shard payloads). */
6
+ index: ConversationStateRecord;
7
+ /** Conversation entries merged with their shard payloads. */
8
+ conversations: Record<string, ConversationStateRecord>;
9
+ /** Index document as text (no shard payloads). */
10
+ indexText: string;
11
+ /** Index plus hydrated shard payloads, serialized as one JSON document. */
12
+ hydratedText: string;
13
+ }
14
+ /** Read a workspace conversation index and hydrate every conversation shard. */
15
+ export declare function readConversationStateDocument(workspacePath: string): ConversationStateDocument;
16
+ /** Index-only text, for assertions about what the index itself must not contain. */
17
+ export declare function readConversationIndexText(workspacePath: string): string;
18
+ //# sourceMappingURL=conversationStateDocument.d.ts.map
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readConversationStateDocument = readConversationStateDocument;
37
+ exports.readConversationIndexText = readConversationIndexText;
38
+ /**
39
+ * Workspace conversation state on disk is an **index** document
40
+ * (`conversations/state.json`) plus one heavy payload shard per conversation
41
+ * (`conversations/shards/<key>.<hash>.json`).
42
+ *
43
+ * Product code reads through `Agent.readStoredConversationState`, which
44
+ * hydrates shards lazily. Fixtures, diagnostics and archive tooling that read
45
+ * the raw file need the same view: reading only the index makes every
46
+ * conversation look empty. These helpers are the single shard-aware reader for
47
+ * such callers.
48
+ */
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ function parseJson(text) {
52
+ try {
53
+ const parsed = JSON.parse(text.replace(/^\uFEFF/, ''));
54
+ return parsed && typeof parsed === 'object' ? parsed : {};
55
+ }
56
+ catch {
57
+ return {};
58
+ }
59
+ }
60
+ /** Read a workspace conversation index and hydrate every conversation shard. */
61
+ function readConversationStateDocument(workspacePath) {
62
+ const indexPath = path.join(workspacePath, 'conversations', 'state.json');
63
+ const indexText = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf-8') : '{}';
64
+ const index = parseJson(indexText);
65
+ const shardDir = path.join(path.dirname(indexPath), 'shards');
66
+ const conversations = {};
67
+ for (const [key, rawEntry] of Object.entries((index.conversations || {}))) {
68
+ const entry = rawEntry || {};
69
+ const shardFile = typeof entry.shard === 'string' && entry.shard ? path.join(shardDir, entry.shard) : '';
70
+ if (shardFile && fs.existsSync(shardFile)) {
71
+ conversations[key] = { ...entry, ...parseJson(fs.readFileSync(shardFile, 'utf-8')) };
72
+ continue;
73
+ }
74
+ conversations[key] = { ...entry };
75
+ }
76
+ return {
77
+ path: indexPath,
78
+ index,
79
+ conversations,
80
+ indexText,
81
+ hydratedText: JSON.stringify({ ...index, conversations }, null, 2),
82
+ };
83
+ }
84
+ /** Index-only text, for assertions about what the index itself must not contain. */
85
+ function readConversationIndexText(workspacePath) {
86
+ return readConversationStateDocument(workspacePath).indexText;
87
+ }
88
+ //# sourceMappingURL=conversationStateDocument.js.map