newmark-agent 0.6.4 → 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 (70) 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 +2880 -1069
  6. package/dist/conversation-utility-host.js +94 -7
  7. package/dist/core/agent.d.ts +126 -12
  8. package/dist/core/agent.js +783 -172
  9. package/dist/core/agentKernelRunner.d.ts +7 -1
  10. package/dist/core/agentKernelRunner.js +156 -20
  11. package/dist/core/autoRouter.d.ts +49 -44
  12. package/dist/core/autoRouter.js +117 -302
  13. package/dist/core/config.js +27 -11
  14. package/dist/core/continuation/contracts.d.ts +1 -1
  15. package/dist/core/continuation/store.d.ts +91 -1
  16. package/dist/core/continuation/store.js +291 -61
  17. package/dist/core/conversationKernel.d.ts +49 -7
  18. package/dist/core/conversationKernel.js +303 -51
  19. package/dist/core/conversationStateDocument.d.ts +18 -0
  20. package/dist/core/conversationStateDocument.js +88 -0
  21. package/dist/core/conversationVisualBudget.d.ts +74 -0
  22. package/dist/core/conversationVisualBudget.js +153 -0
  23. package/dist/core/electronUtilityAgentClient.d.ts +3 -2
  24. package/dist/core/electronUtilityAgentClient.js +32 -8
  25. package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
  26. package/dist/core/electronUtilityRuntimePool.js +93 -24
  27. package/dist/core/hostRuntimeHooks.d.ts +23 -0
  28. package/dist/core/hostRuntimeHooks.js +17 -0
  29. package/dist/core/jevDecision.d.ts +144 -0
  30. package/dist/core/jevDecision.js +226 -0
  31. package/dist/core/memoryProbe.d.ts +24 -0
  32. package/dist/core/memoryProbe.js +104 -0
  33. package/dist/core/mobilePairing.d.ts +7 -1
  34. package/dist/core/mobilePairing.js +9 -1
  35. package/dist/core/performanceDiagnostics.d.ts +1 -1
  36. package/dist/core/routeDecisionValidator.d.ts +86 -0
  37. package/dist/core/routeDecisionValidator.js +249 -0
  38. package/dist/core/routeEligibility.d.ts +52 -0
  39. package/dist/core/routeEligibility.js +108 -0
  40. package/dist/core/runtimeMemoryBudget.d.ts +6 -0
  41. package/dist/core/runtimeMemoryBudget.js +12 -0
  42. package/dist/core/utilityAgentProtocol.d.ts +8 -10
  43. package/dist/core/utilityHostToolRouter.js +2 -0
  44. package/dist/core/visualDownscale.d.ts +6 -0
  45. package/dist/core/visualDownscale.js +137 -0
  46. package/dist/core/wslAgentClient.d.ts +1 -2
  47. package/dist/core/wslAgentClient.js +0 -8
  48. package/dist/core/wslAgentProtocol.d.ts +1 -10
  49. package/dist/core/wslAgentRuntimePool.d.ts +1 -3
  50. package/dist/core/wslAgentRuntimePool.js +17 -22
  51. package/dist/llm/provider.d.ts +3 -1
  52. package/dist/llm/provider.js +53 -9
  53. package/dist/main.js +107 -30
  54. package/dist/preload.js +3 -1
  55. package/dist/server.js +30 -18
  56. package/dist/tools/computerUse.d.ts +4 -0
  57. package/dist/tools/computerUse.js +202 -4
  58. package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
  59. package/dist/tools/computerUsePowerShellHost.js +105 -31
  60. package/dist/tools/index.js +4 -1
  61. package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
  62. package/dist/tui/src/i18n.js +12 -1
  63. package/dist/tui/src/render.js +9 -2
  64. package/dist/tui/src/settings-schema.js +19 -0
  65. package/dist/tui/src/state.js +69 -1
  66. package/dist/ui/index.html +651 -233
  67. package/dist/ui/lucide-sprite.svg +0 -8
  68. package/dist/wsl-agent-host.bundle.cjs +2685 -1044
  69. package/dist/wsl-agent-host.js +0 -3
  70. package/package.json +36 -9
@@ -1,4 +1,4 @@
1
- import { Agent, AutoRouteRatingResult } from './agent';
1
+ import { Agent } from './agent';
2
2
  import { AgentMode, AgentWorkEvent, ConversationInputEnvelope, ConversationImageAttachment, GuideReceipt, OptionQuestion } from './types';
3
3
  import { AutomationManager } from './automation';
4
4
  import { ConversationRuntimeTarget, NormalizedConversationTarget } from './conversationTarget';
@@ -91,8 +91,7 @@ export interface AgentPromptMessage {
91
91
  attachments?: ConversationImageAttachment[];
92
92
  }>;
93
93
  routePolicy?: {
94
- mode?: 'quality' | 'balanced' | 'cost' | 'speed';
95
- maxQualityLoss?: number;
94
+ mode?: 'conservative' | 'balanced' | 'aggressive';
96
95
  maxExpectedCostUsd?: number;
97
96
  allowPreview?: boolean;
98
97
  privacy?: 'default' | 'no_training' | 'zdr';
@@ -160,7 +159,6 @@ export interface ConversationKernelRunResult {
160
159
  workRuns: Agent['workRuns'];
161
160
  routeDecision?: Agent['lastRouteDecision'];
162
161
  resolvedDeployment?: ReturnType<Agent['activeDeployment']>;
163
- autoRouteRatingAvailable?: boolean;
164
162
  }
165
163
  export type ConversationTargetInput = string | ConversationRuntimeTarget;
166
164
  export interface ConversationRuntimeState {
@@ -220,6 +218,32 @@ export declare class ConversationKernel {
220
218
  private listeners;
221
219
  private readonly eventLimit;
222
220
  private generations;
221
+ /**
222
+ * Durable continuation ledgers are read on every queue projection and state
223
+ * snapshot. Building a fresh store each time re-parsed (and previously also
224
+ * re-wrote) the whole JSON ledger, so a long conversation became slower with
225
+ * every turn. The store revalidates its cache through the ledger file
226
+ * fingerprint on each read, so one instance per branch stays authoritative.
227
+ */
228
+ private readonly continuationStores;
229
+ /** Throttles orphan sweeps per branch so snapshots stay read-only and cheap. */
230
+ private readonly orphanSweepAt;
231
+ /**
232
+ * dev-0.6.6: is the process that owns an attempt still alive?
233
+ *
234
+ * Attempts record their owner as `kernel:<pid>`. A dead owner can never write
235
+ * a terminal state, so its build/guard must be reclaimed by a live reader
236
+ * instead of waiting for a claim that nobody will ever make.
237
+ */
238
+ private workerCapabilityAlive;
239
+ /**
240
+ * Reclaim an orphaned queued turn (dead owner / long-expired lease) and, when
241
+ * the conversation is idle and its queue is not paused, let the queue run
242
+ * again. Field evidence: a utility host died of V8 OOM mid-turn and left the
243
+ * build RUNNING with an expired guard, so every later queued row entered the
244
+ * panel but never executed.
245
+ */
246
+ private maybeRecoverOrphanedAttempt;
223
247
  constructor(root: string, host: Agent, automation: AutomationManager | null, lifecycle?: ConversationKernelLifecycle);
224
248
  subscribe(listener: WorkListener): () => void;
225
249
  isRunning(target: ConversationTargetInput): boolean;
@@ -256,7 +280,27 @@ export declare class ConversationKernel {
256
280
  }): string | null;
257
281
  settleExternalRun(target: ConversationTargetInput, completed: boolean): void;
258
282
  releaseExternalRun(target: ConversationTargetInput, paused: boolean): void;
259
- prepareForArchive(target: ConversationTargetInput): Promise<Agent | null>;
283
+ /**
284
+ * Start an archive for one conversation.
285
+ *
286
+ * An archive has to wait for the conversation's own run to settle before the
287
+ * writer takes its payload snapshot and deletes the state entry: a run that
288
+ * is still writing would otherwise append to a conversation that no longer
289
+ * exists (silently losing the tail), and its trailing state flush would
290
+ * resurrect the archived entry. That wait is not a budget to be truncated —
291
+ * it is real work, and the way to end it is to *release the occupancy* the
292
+ * run is holding, not to give up on it.
293
+ *
294
+ * So the caller passes the hard stop of the resident runtime as
295
+ * `releaseOccupancy`. It runs concurrently with the settle below: the hard
296
+ * stop invalidates the in-flight request (the client rejects every pending
297
+ * request of the evicted generation), which is exactly what makes a wedged
298
+ * run finish. Both teardown paths therefore overlap instead of serializing,
299
+ * and no timer exists to mask a runtime that refuses to end.
300
+ */
301
+ prepareForArchive(target: ConversationTargetInput, options?: {
302
+ releaseOccupancy?: () => Promise<void> | void;
303
+ }): Promise<Agent | null>;
260
304
  finishArchive(target: ConversationTargetInput, completed: boolean, preserveOwner?: boolean): void;
261
305
  isAnyRunning(): boolean;
262
306
  hasRetainedWork(): boolean;
@@ -357,7 +401,6 @@ export declare class ConversationKernel {
357
401
  conversationLocked: false;
358
402
  routeDecision: Agent['lastRouteDecision'];
359
403
  resolvedDeployment: ReturnType<Agent['activeDeployment']>;
360
- autoRouteRatingAvailable: boolean;
361
404
  };
362
405
  runtimeStates(): ConversationRuntimeState[];
363
406
  enqueueGuide(envelope: ConversationInputEnvelope): GuideReceipt;
@@ -369,7 +412,6 @@ export declare class ConversationKernel {
369
412
  at: string;
370
413
  };
371
414
  compressContext(target: ConversationTargetInput, options?: ConversationContextCompressOptions): Promise<Record<string, unknown>>;
372
- rateAutoRoute(target: ConversationTargetInput, score: number, expectedRouteId?: string): AutoRouteRatingResult;
373
415
  setWorkRunExpanded(target: ConversationTargetInput, runId: string, expanded: boolean): boolean;
374
416
  setInputMode(target: ConversationTargetInput, mode: string): 'guide' | 'next';
375
417
  setMode(target: ConversationTargetInput, mode: AgentMode): AgentMode;
@@ -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();
303
467
  }
304
- catch { /* archive owns the terminal outcome */ }
468
+ catch (error) {
469
+ console.error('[Newmark] archive occupancy release failed:', error instanceof Error ? error.message : String(error));
470
+ }
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;
@@ -394,14 +570,14 @@ class ConversationKernel {
394
570
  const continuation = this.continuationStore(runtime);
395
571
  if (continuation) {
396
572
  try {
397
- const snapshot = continuation.store.snapshot({
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({
398
576
  workspaceId: continuation.workspaceId,
399
577
  rootId: continuation.rootId,
400
578
  branchId: continuation.branchId,
401
579
  });
402
- waitingByBuildId = new Map(snapshot.builds
403
- .filter(build => !!build.waitingReason)
404
- .map(build => [build.buildId, String(build.waitingReason)]));
580
+ waitingByBuildId = new Map(Object.entries(reasons).map(([buildId, reason]) => [buildId, String(reason)]));
405
581
  }
406
582
  catch { /* the projection stays usable without ledger diagnostics */ }
407
583
  }
@@ -472,7 +648,12 @@ class ConversationKernel {
472
648
  const branchId = branchNodeIdOverride.includes('::')
473
649
  ? branchNodeIdOverride
474
650
  : `${rootId}::${branchNodeIdOverride || branchIdentity.branchNodeId || 'main'}`;
475
- 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
+ }
476
657
  store.ensureRoot(rootId, branchId);
477
658
  return { store, workspaceId, rootId, branchId };
478
659
  }
@@ -577,6 +758,10 @@ class ConversationKernel {
577
758
  }]);
578
759
  this.trackQueuedMessage(runtime, prompt, 'followUp');
579
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');
580
765
  if (!runtime.activePromise && !runtime.externalOwner && !runtime.queuePaused)
581
766
  this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
582
767
  return this.queueItems(runtime.target).find(item => item.id === id);
@@ -882,6 +1067,11 @@ class ConversationKernel {
882
1067
  runtime.queuePaused = true;
883
1068
  runtime.runId ||= (0, crypto_1.randomUUID)();
884
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');
885
1075
  const conversationSnapshot = runner.getConversationSnapshot(normalized.conversationId, options);
886
1076
  return {
887
1077
  ...conversationSnapshot,
@@ -911,9 +1101,6 @@ class ConversationKernel {
911
1101
  conversationLocked: false,
912
1102
  routeDecision: runner.lastRouteDecision,
913
1103
  resolvedDeployment: runner.activeDeployment(),
914
- autoRouteRatingAvailable: runner.model === 'auto'
915
- && runner.lastRouteDecision?.requestedSelection.kind === 'auto'
916
- && !!runner.lastRouteDecision.resolvedDeployment,
917
1104
  };
918
1105
  }
919
1106
  runtimeStates() {
@@ -1148,12 +1335,6 @@ class ConversationKernel {
1148
1335
  displayHistory: { untouched: true, messageCount: runner.chatMessages.length },
1149
1336
  };
1150
1337
  }
1151
- rateAutoRoute(target, score, expectedRouteId = '') {
1152
- const runtime = this.findRuntime(target);
1153
- if (!runtime)
1154
- return { ok: false, reason: 'no_active_auto_route' };
1155
- return runtime.runner.rateActiveAutoRoute(score, expectedRouteId);
1156
- }
1157
1338
  setWorkRunExpanded(target, runId, expanded) {
1158
1339
  const normalized = this.normalizeTarget(target);
1159
1340
  const runtime = this.findRuntime(normalized);
@@ -1472,7 +1653,26 @@ class ConversationKernel {
1472
1653
  this.mirrorHostIfTargetActive(runtime);
1473
1654
  return this.result(runtime, lastTokens);
1474
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 = '';
1475
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
+ }
1476
1676
  while (!runtime.queuePaused) {
1477
1677
  if (runtime.stopRequestedRunId === runtime.runId)
1478
1678
  return this.result(runtime, lastTokens);
@@ -1825,6 +2025,10 @@ class ConversationKernel {
1825
2025
  // An already-read inbox entry is not replayed by the mailbox listener.
1826
2026
  // Reconcile saved result receipts after hydrating its continuation copy.
1827
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');
1828
2032
  return runtime;
1829
2033
  }
1830
2034
  scheduleGoalContinuation(runtime, completedRunId) {
@@ -1888,6 +2092,57 @@ class ConversationKernel {
1888
2092
  : { ...delivery, runId: undefined };
1889
2093
  const continuation = this.continuationStore(runtime);
1890
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
+ };
1891
2146
  void this.runPendingQueueItem(runtime, next, async () => {
1892
2147
  this.bindQueuedTurnBranch(runtime, queued);
1893
2148
  if (queued?.modelSelection)
@@ -1929,41 +2184,41 @@ class ConversationKernel {
1929
2184
  this.emitQueueUpdate(runtime);
1930
2185
  return this.result(runtime, []);
1931
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();
1932
2204
  }
1933
2205
  const result = await this.prompt(message, runtime.target, runtime.options, 'followUp');
1934
- if (continuation && scope) {
1935
- continuation.store.commitFinal({
1936
- scope,
1937
- text: result.tokens.map(token => token.text || '').join(''),
1938
- });
1939
- }
2206
+ settleLedgerScope('commit', { text: result.tokens.map(token => token.text || '').join('') });
1940
2207
  return result;
1941
2208
  }, (accepted, error) => {
1942
- if (!continuation || !scope)
1943
- return;
1944
- const reason = error instanceof Error ? error.message : String(error);
1945
- if (accepted) {
1946
- try {
1947
- continuation.store.failBuild({ scope, reason });
1948
- }
1949
- catch { /* stop/archive may have already released the guard */ }
1950
- }
1951
- else {
1952
- // The input was never accepted. Stop/archive may already have
1953
- // released the guard, so use the idempotent requeue path: the same
1954
- // durable build id stays available for an explicit resume.
1955
- try {
1956
- continuation.store.requeueBuild({
1957
- workspaceId: continuation.workspaceId,
1958
- rootId: continuation.rootId,
1959
- branchId: continuation.branchId,
1960
- buildId: scope.buildId,
1961
- });
1962
- }
1963
- catch { /* the build may already be committed or explicitly cancelled */ }
1964
- }
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 });
1965
2212
  }).catch(() => {
1966
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
+ }
1967
2222
  });
1968
2223
  return;
1969
2224
  }
@@ -2167,9 +2422,6 @@ class ConversationKernel {
2167
2422
  workRuns: this.bindWorkRunsToRuntimeTarget(runtime.runner.getConversationSnapshot(runtime.id).workRuns, runtime.target),
2168
2423
  routeDecision: runtime.runner.lastRouteDecision,
2169
2424
  resolvedDeployment: runtime.runner.activeDeployment(),
2170
- autoRouteRatingAvailable: runtime.runner.model === 'auto'
2171
- && runtime.runner.lastRouteDecision?.requestedSelection.kind === 'auto'
2172
- && !!runtime.runner.lastRouteDecision.resolvedDeployment,
2173
2425
  };
2174
2426
  }
2175
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