newmark-agent 0.6.3 → 0.6.4

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.
@@ -271,6 +271,87 @@ class GuardedContinuationStore {
271
271
  return { ...build, input: cloneInput(build.input) };
272
272
  });
273
273
  }
274
+ /**
275
+ * dev-0.6.4 hotfix: 当某个被受理的 Build 失败/取消后,它的后继会永远无法被
276
+ * claim(`claimBuild` 只认「父 = 已提交 frontier」),整条队列就此卡死,新提交
277
+ * 也只是接在失败链后面。
278
+ *
279
+ * 这里提供一条**显式**修复通道:把被阻断的排队 Build 重新挂到当前已提交的
280
+ * frontier 上,保持原有队列顺序,并为每一次改挂写入 `BuildQueueRepaired`
281
+ * 审计事件(记录旧 parent / 新 parent / 原因)。它不是静默 rebase:调用方是
282
+ * 用户的显式动作(恢复队列)或一次新的入队命令。
283
+ */
284
+ repairBlockedQueue(input) {
285
+ return this.mutate(ledger => {
286
+ const branch = this.requireBranch(ledger, input);
287
+ const frontier = branch.headBuildId || null;
288
+ const queued = Object.values(ledger.builds)
289
+ .filter(build => build.branchId === branch.branchId && build.status === 'QUEUED')
290
+ .sort((a, b) => a.queueSequence - b.queueSequence);
291
+ const repaired = [];
292
+ let chainParent = frontier;
293
+ let mutated = false;
294
+ for (const build of queued) {
295
+ if ((build.parentBuildId || null) !== (chainParent || null)) {
296
+ const previousParentBuildId = build.parentBuildId;
297
+ build.parentBuildId = chainParent;
298
+ build.waitingReason = undefined;
299
+ build.updatedAt = new Date().toISOString();
300
+ repaired.push(build.buildId);
301
+ mutated = true;
302
+ this.appendEvent(ledger, {
303
+ type: 'BuildQueueRepaired',
304
+ workspaceId: this.workspaceId,
305
+ rootId: branch.rootId,
306
+ branchId: branch.branchId,
307
+ buildId: build.buildId,
308
+ attemptId: null,
309
+ fence: null,
310
+ payload: {
311
+ previousParentBuildId,
312
+ newParentBuildId: chainParent,
313
+ reason: String(input.reason || 'committed-frontier-repair'),
314
+ },
315
+ });
316
+ }
317
+ chainParent = build.buildId;
318
+ }
319
+ if (mutated) {
320
+ branch.queueRevision += 1;
321
+ branch.lifecycleRevision += 1;
322
+ branch.updatedAt = new Date().toISOString();
323
+ }
324
+ return { repaired, frontier };
325
+ });
326
+ }
327
+ /**
328
+ * dev-0.6.4 hotfix: 读取视角的受阻推导。
329
+ *
330
+ * 一个 QUEUED Build 只有在它的父正好是「已提交 frontier」或队列里位于它之前
331
+ * 的可执行 Build 时才可能被 claim(见 `claimBuild`)。这里按队列顺序推导哪些行
332
+ * 目前不可执行:父不是链上前驱、或父已 FAILED/CANCELLED 的行即为受阻,且受阻点
333
+ * 之后的所有行同样受阻。
334
+ */
335
+ blockedQueuedBuildIds(ledger, branch) {
336
+ const blocked = new Set();
337
+ const queued = Object.values(ledger.builds)
338
+ .filter(build => build.branchId === branch.branchId && build.status === 'QUEUED')
339
+ .sort((a, b) => a.queueSequence - b.queueSequence);
340
+ let chainParent = branch.headBuildId || null;
341
+ for (const build of queued) {
342
+ const parent = build.parentBuildId ? ledger.builds[build.parentBuildId] : null;
343
+ // 链上的前驱(frontier 或队列里位于它之前的行)在执行时已经提交,因此只有
344
+ // 「父不是链上前驱」或「父已 FAILED/CANCELLED」的行才是真正受阻的。
345
+ const parentIsChainPredecessor = (build.parentBuildId || null) === (chainParent || null);
346
+ const parentFailed = !!parent && (parent.status === 'FAILED' || parent.status === 'CANCELLED');
347
+ if (!parentIsChainPredecessor || parentFailed) {
348
+ blocked.add(build.buildId);
349
+ continue;
350
+ }
351
+ chainParent = build.buildId;
352
+ }
353
+ return blocked;
354
+ }
274
355
  deleteQueuedBuild(input) {
275
356
  this.mutate(ledger => {
276
357
  const build = ledger.builds[input.buildId];
@@ -702,10 +783,17 @@ class GuardedContinuationStore {
702
783
  }
703
784
  snapshotUnlocked(ledger, branch) {
704
785
  const guard = ledger.guards[branch.branchId] || (0, contracts_1.defaultGuard)(branch.branchId);
786
+ // dev-0.6.4 hotfix: 受阻状态由父链推导,而不是只在失败瞬间给直系后继打标记。
787
+ // 这样“失败之后才入队”的行同样会显示为受阻,界面才能给出显式修复入口。
788
+ const blockedBuildIds = this.blockedQueuedBuildIds(ledger, branch);
705
789
  const builds = Object.values(ledger.builds)
706
790
  .filter(build => build.branchId === branch.branchId)
707
791
  .sort((a, b) => a.queueSequence - b.queueSequence)
708
- .map(build => ({ ...build, input: cloneInput(build.input) }));
792
+ .map(build => ({
793
+ ...build,
794
+ input: cloneInput(build.input),
795
+ waitingReason: build.waitingReason || (blockedBuildIds.has(build.buildId) ? 'DEPENDENCY_FAILED' : undefined),
796
+ }));
709
797
  return {
710
798
  workspaceId: this.workspaceId,
711
799
  rootId: branch.rootId,
@@ -19,8 +19,12 @@ export interface ConversationQueueItemSnapshot {
19
19
  /** Exact workspace ledger branch id used when this row was admitted. */
20
20
  ledgerBranchId?: string;
21
21
  modelSelection?: string;
22
+ /** Ledger reason shown when a queued row cannot run (for example DEPENDENCY_FAILED). */
23
+ waitingReason?: string;
24
+ /** True when the ledger currently blocks this row from being claimed. */
25
+ blocked?: boolean;
22
26
  }
23
- export type ConversationQueueAction = 'enqueue' | 'update' | 'delete' | 'reorder' | 'toggle_pause' | 'set_pause' | 'guide';
27
+ export type ConversationQueueAction = 'enqueue' | 'update' | 'delete' | 'reorder' | 'toggle_pause' | 'set_pause' | 'repair_blocked' | 'guide';
24
28
  export interface ConversationQueueActionInput {
25
29
  id?: string;
26
30
  text?: string;
@@ -303,6 +307,14 @@ export declare class ConversationKernel {
303
307
  deleteQueueItem(target: ConversationTargetInput, idInput: string): boolean;
304
308
  reorderQueueItems(target: ConversationTargetInput, orderedIdsInput: string[]): ConversationQueueItemSnapshot[];
305
309
  setQueuePaused(target: ConversationTargetInput, paused: boolean): boolean;
310
+ /**
311
+ * dev-0.6.4 hotfix: 显式修复被失败前置阻断的排队行。
312
+ *
313
+ * 0.6.3 的合约是「失败/取消阻断后继,且不静默跳过」,所以这里不挂在恢复动作上
314
+ * 自动执行;它必须由用户的显式动作触发(界面上的“修复受阻队列”按钮)。每一次
315
+ * 改挂都写入 `BuildQueueRepaired` 事件并发布可见状态,触发者与原因可审计。
316
+ */
317
+ private repairBlockedQueueNow;
306
318
  queueAction(target: ConversationTargetInput, action: ConversationQueueAction, input?: ConversationQueueActionInput): {
307
319
  ok: boolean;
308
320
  queueItems: ConversationQueueItemSnapshot[];
@@ -388,6 +388,23 @@ class ConversationKernel {
388
388
  const runtime = this.findRuntime(target);
389
389
  if (!runtime)
390
390
  return [];
391
+ // dev-0.6.4 hotfix: 把账本里的受阻原因带进队列投影,界面才能显示
392
+ // “被失败前置阻断”而不是无声卡住。
393
+ let waitingByBuildId = new Map();
394
+ const continuation = this.continuationStore(runtime);
395
+ if (continuation) {
396
+ try {
397
+ const snapshot = continuation.store.snapshot({
398
+ workspaceId: continuation.workspaceId,
399
+ rootId: continuation.rootId,
400
+ branchId: continuation.branchId,
401
+ });
402
+ waitingByBuildId = new Map(snapshot.builds
403
+ .filter(build => !!build.waitingReason)
404
+ .map(build => [build.buildId, String(build.waitingReason)]));
405
+ }
406
+ catch { /* the projection stays usable without ledger diagnostics */ }
407
+ }
391
408
  return runtime.pendingNextTurn.flatMap(item => {
392
409
  if (item.queueMode !== 'followUp')
393
410
  return [];
@@ -396,6 +413,7 @@ class ConversationKernel {
396
413
  const id = String(item.message.clientMessageId || '');
397
414
  if (!id)
398
415
  return [];
416
+ const buildId = String(item.message.buildId || '');
399
417
  return [{
400
418
  id,
401
419
  buildId: item.message.buildId,
@@ -409,6 +427,8 @@ class ConversationKernel {
409
427
  branchNodeId: item.message.branchNodeId,
410
428
  branchPath: item.message.branchPath?.slice(),
411
429
  modelSelection: item.message.modelSelection,
430
+ waitingReason: waitingByBuildId.get(buildId),
431
+ blocked: waitingByBuildId.has(buildId),
412
432
  }];
413
433
  });
414
434
  }
@@ -484,6 +504,8 @@ class ConversationKernel {
484
504
  const ledgerBranchId = continuation?.branchId;
485
505
  let buildId = '';
486
506
  if (continuation) {
507
+ // 入队严格保持 0.6.3 语义:新命令接在当前队尾之后。被失败前置阻断的行不会
508
+ // 被静默跳过;用户可通过界面上的“修复受阻队列”显式动作重建可执行链。
487
509
  const authoritative = continuation.store.snapshot({
488
510
  workspaceId: continuation.workspaceId,
489
511
  rootId: continuation.rootId,
@@ -708,6 +730,10 @@ class ConversationKernel {
708
730
  return this.queueItems(runtime.target);
709
731
  }
710
732
  setQueuePaused(target, paused) {
733
+ // 队列策略只作用于已经存在的 runtime:在这里按需重建 runtime 会让空闲会话
734
+ // 重新变成“持有排队工作”的所有者,破坏工作区清理契约
735
+ // (`Cannot release a conversation with running or queued work`)。界面路径统一
736
+ // 走 queueAction,它已经负责按需创建 runtime。
711
737
  const runtime = this.findRuntime(target);
712
738
  if (!runtime)
713
739
  throw new Error('Target conversation runtime is unavailable');
@@ -720,6 +746,35 @@ class ConversationKernel {
720
746
  }
721
747
  return runtime.queuePaused;
722
748
  }
749
+ /**
750
+ * dev-0.6.4 hotfix: 显式修复被失败前置阻断的排队行。
751
+ *
752
+ * 0.6.3 的合约是「失败/取消阻断后继,且不静默跳过」,所以这里不挂在恢复动作上
753
+ * 自动执行;它必须由用户的显式动作触发(界面上的“修复受阻队列”按钮)。每一次
754
+ * 改挂都写入 `BuildQueueRepaired` 事件并发布可见状态,触发者与原因可审计。
755
+ */
756
+ repairBlockedQueueNow(runtime) {
757
+ const continuation = this.continuationStore(runtime);
758
+ if (!continuation)
759
+ return 0;
760
+ try {
761
+ const result = continuation.store.repairBlockedQueue({
762
+ workspaceId: continuation.workspaceId,
763
+ rootId: continuation.rootId,
764
+ branchId: continuation.branchId,
765
+ reason: 'explicit-user-repair',
766
+ });
767
+ if (result.repaired.length) {
768
+ 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.`);
769
+ this.schedulePendingRuntimeContinuation(runtime, runtime.runId);
770
+ }
771
+ return result.repaired.length;
772
+ }
773
+ catch (error) {
774
+ runtime.runner.recordWorkStatus(`Queue repair failed: ${error instanceof Error ? error.message : String(error)}`);
775
+ return 0;
776
+ }
777
+ }
723
778
  queueAction(target, action, input = {}) {
724
779
  const normalized = this.normalizeTarget(target);
725
780
  const runtime = this.findRuntime(normalized) || this.runtime(normalized, {
@@ -758,6 +813,10 @@ class ConversationKernel {
758
813
  else if (action === 'set_pause') {
759
814
  this.setQueuePaused(runtime.target, input.paused === true);
760
815
  }
816
+ else if (action === 'repair_blocked') {
817
+ // 显式修复:把被失败前置阻断的排队行重新挂到最后一个已提交 Build。
818
+ this.repairBlockedQueueNow(runtime);
819
+ }
761
820
  else if (action === 'guide') {
762
821
  if (input.text !== undefined)
763
822
  this.updateQueueItem(runtime.target, String(input.id || ''), input.text, input);
@@ -1675,6 +1734,9 @@ class ConversationKernel {
1675
1734
  && runtime.queued.followUp.length === 0
1676
1735
  && runner.subagents.readRootInbox().length === 0;
1677
1736
  });
1737
+ // 执行投影严格跟随当前激活分支节点:新分支绝不接管另一条分页的排队输入
1738
+ // (dev-0.6.3 的既有契约)。界面上跨分页的排队行可见性由渲染层负责,
1739
+ // 执行始终属于受理时捕获的那条分支。
1678
1740
  for (const continuation of runner.conversationContinuations()) {
1679
1741
  runtime.pendingNextTurn.push({
1680
1742
  message: continuation.clientMessageId || continuation.images?.length || continuation.hiddenUserInput
package/dist/main.js CHANGED
@@ -3123,7 +3123,7 @@ else {
3123
3123
  };
3124
3124
  const mutateConversationQueue = async (target, rawAction, input = {}) => {
3125
3125
  const action = rawAction.replace(/^queue_/, '');
3126
- if (!['enqueue', 'update', 'delete', 'reorder', 'toggle_pause', 'set_pause', 'guide'].includes(action))
3126
+ if (!['enqueue', 'update', 'delete', 'reorder', 'toggle_pause', 'set_pause', 'repair_blocked', 'guide'].includes(action))
3127
3127
  throw new Error('Unknown queue action');
3128
3128
  const key = activeFlowStateKey(target);
3129
3129
  const flow = activeFlowStateFor(target);