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.
- package/config.example.json +13 -3
- package/dist/cli-commands.d.ts +1 -1
- package/dist/cli-commands.js +53 -2
- package/dist/cli-discovery.js +3 -0
- package/dist/conversation-utility-host.bundle.cjs +2880 -1069
- package/dist/conversation-utility-host.js +94 -7
- package/dist/core/agent.d.ts +126 -12
- package/dist/core/agent.js +783 -172
- package/dist/core/agentKernelRunner.d.ts +7 -1
- package/dist/core/agentKernelRunner.js +156 -20
- package/dist/core/autoRouter.d.ts +49 -44
- package/dist/core/autoRouter.js +117 -302
- package/dist/core/config.js +27 -11
- package/dist/core/continuation/contracts.d.ts +1 -1
- package/dist/core/continuation/store.d.ts +91 -1
- package/dist/core/continuation/store.js +291 -61
- package/dist/core/conversationKernel.d.ts +49 -7
- package/dist/core/conversationKernel.js +303 -51
- package/dist/core/conversationStateDocument.d.ts +18 -0
- package/dist/core/conversationStateDocument.js +88 -0
- package/dist/core/conversationVisualBudget.d.ts +74 -0
- package/dist/core/conversationVisualBudget.js +153 -0
- package/dist/core/electronUtilityAgentClient.d.ts +3 -2
- package/dist/core/electronUtilityAgentClient.js +32 -8
- package/dist/core/electronUtilityRuntimePool.d.ts +28 -3
- package/dist/core/electronUtilityRuntimePool.js +93 -24
- package/dist/core/hostRuntimeHooks.d.ts +23 -0
- package/dist/core/hostRuntimeHooks.js +17 -0
- package/dist/core/jevDecision.d.ts +144 -0
- package/dist/core/jevDecision.js +226 -0
- package/dist/core/memoryProbe.d.ts +24 -0
- package/dist/core/memoryProbe.js +104 -0
- package/dist/core/mobilePairing.d.ts +7 -1
- package/dist/core/mobilePairing.js +9 -1
- package/dist/core/performanceDiagnostics.d.ts +1 -1
- package/dist/core/routeDecisionValidator.d.ts +86 -0
- package/dist/core/routeDecisionValidator.js +249 -0
- package/dist/core/routeEligibility.d.ts +52 -0
- package/dist/core/routeEligibility.js +108 -0
- package/dist/core/runtimeMemoryBudget.d.ts +6 -0
- package/dist/core/runtimeMemoryBudget.js +12 -0
- package/dist/core/utilityAgentProtocol.d.ts +8 -10
- package/dist/core/utilityHostToolRouter.js +2 -0
- package/dist/core/visualDownscale.d.ts +6 -0
- package/dist/core/visualDownscale.js +137 -0
- package/dist/core/wslAgentClient.d.ts +1 -2
- package/dist/core/wslAgentClient.js +0 -8
- package/dist/core/wslAgentProtocol.d.ts +1 -10
- package/dist/core/wslAgentRuntimePool.d.ts +1 -3
- package/dist/core/wslAgentRuntimePool.js +17 -22
- package/dist/llm/provider.d.ts +3 -1
- package/dist/llm/provider.js +53 -9
- package/dist/main.js +107 -30
- package/dist/preload.js +3 -1
- package/dist/server.js +30 -18
- package/dist/tools/computerUse.d.ts +4 -0
- package/dist/tools/computerUse.js +202 -4
- package/dist/tools/computerUsePowerShellHost.d.ts +6 -1
- package/dist/tools/computerUsePowerShellHost.js +105 -31
- package/dist/tools/index.js +4 -1
- package/dist/tui/src/adapters/core-runtime-adapter.js +13 -1
- package/dist/tui/src/i18n.js +12 -1
- package/dist/tui/src/render.js +9 -2
- package/dist/tui/src/settings-schema.js +19 -0
- package/dist/tui/src/state.js +69 -1
- package/dist/ui/index.html +651 -233
- package/dist/ui/lucide-sprite.svg +0 -8
- package/dist/wsl-agent-host.bundle.cjs +2685 -1044
- package/dist/wsl-agent-host.js +0 -3
- package/package.json +36 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { 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?: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
|
466
|
+
await options.releaseOccupancy();
|
|
303
467
|
}
|
|
304
|
-
catch {
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
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
|