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.
- 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 +3046 -1038
- package/dist/conversation-utility-host.js +94 -7
- package/dist/core/agent.d.ts +149 -12
- package/dist/core/agent.js +843 -176
- package/dist/core/agentKernelRunner.d.ts +7 -1
- package/dist/core/agentKernelRunner.js +171 -22
- package/dist/core/autoRouter.d.ts +49 -44
- package/dist/core/autoRouter.js +117 -302
- package/dist/core/branchIdentity.d.ts +29 -0
- package/dist/core/branchIdentity.js +54 -0
- package/dist/core/config.js +27 -11
- package/dist/core/continuation/contracts.d.ts +1 -1
- package/dist/core/continuation/store.d.ts +119 -1
- package/dist/core/continuation/store.js +337 -19
- package/dist/core/conversationKernel.d.ts +62 -8
- package/dist/core/conversationKernel.js +361 -47
- 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 +108 -31
- 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 +994 -277
- package/dist/ui/lucide-sprite.svg +0 -8
- package/dist/wsl-agent-host.bundle.cjs +2851 -1013
- package/dist/wsl-agent-host.js +0 -3
- 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
|
-
|
|
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();
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
console.error('[Newmark] archive occupancy release failed:', error instanceof Error ? error.message : String(error));
|
|
303
470
|
}
|
|
304
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
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
|