@dsh-enhanced/assistant-delivery 0.1.62 → 0.1.64

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 (49) hide show
  1. package/README.md +32 -10
  2. package/bin/dsh-restart-supervisor.js +76 -0
  3. package/lib/agent-runtime.d.ts +21 -1
  4. package/lib/agent-runtime.d.ts.map +1 -1
  5. package/lib/agent-runtime.js +192 -23
  6. package/lib/agent-runtime.js.map +1 -1
  7. package/lib/coordinator.d.ts.map +1 -1
  8. package/lib/coordinator.js +10 -0
  9. package/lib/coordinator.js.map +1 -1
  10. package/lib/operator-snapshot.d.ts +1 -1
  11. package/lib/operator-snapshot.d.ts.map +1 -1
  12. package/lib/operator-snapshot.js +5 -5
  13. package/lib/operator-snapshot.js.map +1 -1
  14. package/lib/restart-native.d.ts +47 -0
  15. package/lib/restart-native.d.ts.map +1 -0
  16. package/lib/restart-native.js +170 -0
  17. package/lib/restart-native.js.map +1 -0
  18. package/lib/restart-runtime.d.ts +38 -0
  19. package/lib/restart-runtime.d.ts.map +1 -0
  20. package/lib/restart-runtime.js +335 -0
  21. package/lib/restart-runtime.js.map +1 -0
  22. package/lib/restart-store.d.ts +132 -0
  23. package/lib/restart-store.d.ts.map +1 -0
  24. package/lib/restart-store.js +757 -0
  25. package/lib/restart-store.js.map +1 -0
  26. package/lib/restart-supervisor.d.ts +129 -0
  27. package/lib/restart-supervisor.d.ts.map +1 -0
  28. package/lib/restart-supervisor.js +615 -0
  29. package/lib/restart-supervisor.js.map +1 -0
  30. package/lib/service.d.ts +45 -1
  31. package/lib/service.d.ts.map +1 -1
  32. package/lib/service.js +488 -6
  33. package/lib/service.js.map +1 -1
  34. package/lib/sqlite.d.ts +1 -1
  35. package/lib/sqlite.d.ts.map +1 -1
  36. package/lib/sqlite.js +76 -2
  37. package/lib/sqlite.js.map +1 -1
  38. package/lib/store.d.ts +41 -1
  39. package/lib/store.d.ts.map +1 -1
  40. package/lib/store.js +433 -85
  41. package/lib/store.js.map +1 -1
  42. package/lib/tools.d.ts.map +1 -1
  43. package/lib/tools.js +21 -2
  44. package/lib/tools.js.map +1 -1
  45. package/lib/types.d.ts +33 -0
  46. package/lib/types.d.ts.map +1 -1
  47. package/lib/version.d.ts +1 -1
  48. package/lib/version.js +1 -1
  49. package/package.json +11 -10
@@ -13,6 +13,8 @@ import { externalPrincipalId } from './canonical.js';
13
13
  import { feedbackSignalInput, feedbackUsage, parseFeedbackCommand, } from './feedback-command.js';
14
14
  import { objectiveStateMessage } from './feedback-command.js';
15
15
  import { learningCommandUsage, parseLearningCommand, serializeLearningExport, } from './learning-command.js';
16
+ import { attachRestartRun, restartDigest, restartContinuationText } from './restart-runtime.js';
17
+ import { admitRestartContinuation, createRestartContinuation } from './restart-native.js';
16
18
  import { renderOwnerApprovalStatus } from './approval-status-context.js';
17
19
  import { isExactDeliveryCommand, learningDispatchRecoveryCode, parseDeliveryCommand, permissionDispatchRecoveryCode, } from './session-commands.js';
18
20
  import { DeliveryStoreError } from './store.js';
@@ -20,6 +22,12 @@ import { SessionLeaseUnavailable } from './session-lease-runtime.js';
20
22
  import { isDeliveryGoalWakeDeadline } from './goal-wake-types.js';
21
23
  import { parseWorkflowCommand, workflowCommandUsage, } from './workflow-command.js';
22
24
  const SAFE_NATIVE_COMMANDS = new Set(['compact']);
25
+ class AgentTaskTimeoutError extends Error {
26
+ constructor() {
27
+ super('assistant-delivery: foreground task exceeded its execution deadline');
28
+ this.name = 'AgentTaskTimeoutError';
29
+ }
30
+ }
23
31
  class UserSessionCancellation extends Error {
24
32
  command;
25
33
  constructor(command) {
@@ -1116,6 +1124,24 @@ export class DshDeliveryRuntime {
1116
1124
  ctx;
1117
1125
  policy;
1118
1126
  options;
1127
+ restartRuns = new WeakMap();
1128
+ canRequestRestart(agent) { return this.restartRuns.get(agent)?.canRequest() === true; }
1129
+ requestRestart(agent, reason) {
1130
+ const run = this.restartRuns.get(agent);
1131
+ if (run === undefined)
1132
+ throw new Error('assistant-delivery: this foreground task has no restart authority');
1133
+ const record = run.request(reason);
1134
+ return { id: record.id, status: record.status };
1135
+ }
1136
+ async resumeRestart(record, binding, envelope, signal) {
1137
+ const restart = this.options.restart;
1138
+ if (restart === undefined || !restart.authorize(record))
1139
+ throw new Error('assistant-delivery: restart continuation is not authorized');
1140
+ return await this.process(binding, envelope, signal, undefined, () => {
1141
+ if (!restart.authorize(record))
1142
+ throw new Error('assistant-delivery: restart continuation authority changed');
1143
+ }, record);
1144
+ }
1119
1145
  dispatchControl = 'explicit';
1120
1146
  activeSessionControls = new Map();
1121
1147
  sessionLeases;
@@ -1561,11 +1587,15 @@ export class DshDeliveryRuntime {
1561
1587
  return true;
1562
1588
  }
1563
1589
  /** Wait for only the turn that claims Delivery's exact message identity. */
1564
- followupTurn(agent, message, onClaimed, onSettled) {
1590
+ followupTurn(agent, message, onClaimed, onSettled, admit, signal) {
1565
1591
  return new Promise((resolve, reject) => {
1566
1592
  let claimedTurn;
1567
1593
  let settled = false;
1594
+ let abortDeadline;
1568
1595
  const cleanup = () => {
1596
+ if (abortDeadline !== undefined)
1597
+ clearTimeout(abortDeadline);
1598
+ signal?.removeEventListener('abort', aborted);
1569
1599
  removeClaimed();
1570
1600
  removeDiscarded();
1571
1601
  removeSessionEvent();
@@ -1584,6 +1614,13 @@ export class DshDeliveryRuntime {
1584
1614
  cleanup();
1585
1615
  reject(error);
1586
1616
  };
1617
+ const aborted = () => {
1618
+ // Let cooperative native cancellation persist its tool result and
1619
+ // turn/end before disposing. Uncooperative tools get only this grace.
1620
+ abortDeadline ??= setTimeout(() => fail(signal?.reason
1621
+ ?? new Error('assistant-delivery: execution aborted')), 1_000);
1622
+ abortDeadline.unref?.();
1623
+ };
1587
1624
  const removeClaimed = this.ctx.on('agent/inbox/claimed', event => {
1588
1625
  if (event.agent === agent && event.message.id === message.id) {
1589
1626
  claimedTurn = event.turn;
@@ -1614,8 +1651,21 @@ export class DshDeliveryRuntime {
1614
1651
  settle(claimedTurn);
1615
1652
  }
1616
1653
  });
1654
+ signal?.addEventListener('abort', aborted, { once: true });
1655
+ if (signal?.aborted) {
1656
+ fail(signal.reason);
1657
+ return;
1658
+ }
1617
1659
  try {
1618
- agent.followup(message);
1660
+ if (admit === undefined)
1661
+ agent.followup(message);
1662
+ else
1663
+ void admit().then(state => {
1664
+ if (state.kind === 'settled')
1665
+ settle(state.turn);
1666
+ else if (state.kind !== 'pending')
1667
+ fail(new Error('assistant-delivery: continuation was already dispatched without a settled result'));
1668
+ }, fail);
1619
1669
  }
1620
1670
  catch (error) {
1621
1671
  fail(error);
@@ -1728,7 +1778,7 @@ export class DshDeliveryRuntime {
1728
1778
  return await this.followupTurn(agent, message, onClaimed, () => {
1729
1779
  exactTurnSettled = true;
1730
1780
  releaseToolBarriers();
1731
- });
1781
+ }, undefined, signal);
1732
1782
  }
1733
1783
  catch (error) {
1734
1784
  if (signal.aborted)
@@ -3341,7 +3391,7 @@ export class DshDeliveryRuntime {
3341
3391
  await this.disposeAfterReplyBoundary(binding.sessionId, handle);
3342
3392
  }
3343
3393
  }
3344
- async process(binding, envelope, outerSignal, prepared, markDispatching) {
3394
+ async process(binding, envelope, outerSignal, prepared, markDispatching, restartContinuation) {
3345
3395
  const sessionCommand = parseDeliveryCommand(envelope);
3346
3396
  if (envelope.kind === 'command' && sessionCommand === undefined) {
3347
3397
  return this.replySessionCommand(binding, envelope, {
@@ -3386,14 +3436,21 @@ export class DshDeliveryRuntime {
3386
3436
  throw error;
3387
3437
  }
3388
3438
  let control;
3439
+ let taskDeadline;
3389
3440
  try {
3390
3441
  control = this.beginSessionControl(binding.sessionId, lease.signal);
3442
+ taskDeadline = setTimeout(() => {
3443
+ // Cancel the durable lease as well as the local wait: late tools and
3444
+ // model requests must remain fenced until real teardown is observed.
3445
+ lease.cancel(new AgentTaskTimeoutError());
3446
+ }, this.options.taskTimeoutMs);
3447
+ taskDeadline.unref?.();
3391
3448
  lease.dispatch();
3392
3449
  return await this.processControlled(binding, envelope, control.controller.signal, sessionCommand, prepared, markDispatching === undefined ? undefined : () => {
3393
3450
  lease.assert();
3394
3451
  markDispatching();
3395
3452
  lease.assert();
3396
- });
3453
+ }, restartContinuation);
3397
3454
  }
3398
3455
  catch (error) {
3399
3456
  if (control?.command !== undefined && causedByUserCancellation(error)) {
@@ -3408,12 +3465,14 @@ export class DshDeliveryRuntime {
3408
3465
  throw error;
3409
3466
  }
3410
3467
  finally {
3468
+ if (taskDeadline !== undefined)
3469
+ clearTimeout(taskDeadline);
3411
3470
  if (control !== undefined)
3412
3471
  this.endSessionControl(binding.sessionId, control);
3413
3472
  lease.close();
3414
3473
  }
3415
3474
  }
3416
- async processControlled(binding, envelope, signal, sessionCommand, prepared, markDispatching) {
3475
+ async processControlled(binding, envelope, signal, sessionCommand, prepared, markDispatching, restartContinuation) {
3417
3476
  if (sessionCommand?.name === 'feedback') {
3418
3477
  return await this.runFeedbackCommand(sessionCommand, binding, envelope, signal, markDispatching);
3419
3478
  }
@@ -3516,9 +3575,13 @@ export class DshDeliveryRuntime {
3516
3575
  if (agents === undefined || sessions === undefined) {
3517
3576
  return { outcome: 'not-processed', failureCode: 'agent-runtime-unavailable', retryable: true };
3518
3577
  }
3519
- const persistedSelection = this.options.getModelSelection(envelope.conversation);
3578
+ const restart = this.options.restart;
3579
+ const continuationTask = restartContinuation === undefined ? undefined : restart?.store.getTask(restartContinuation.inboxId);
3580
+ if (restartContinuation !== undefined && continuationTask?.model === undefined)
3581
+ throw new Error('assistant-delivery: restart model is not frozen');
3582
+ const persistedSelection = restartContinuation === undefined ? this.options.getModelSelection(envelope.conversation) : undefined;
3520
3583
  const requestedRoute = toModelRoute(persistedSelection
3521
- ?? prepared?.modelRoute
3584
+ ?? continuationTask?.model ?? prepared?.modelRoute
3522
3585
  ?? { provider: this.options.provider, model: this.options.model });
3523
3586
  const resolvedRoute = await this.resolveExecutionRoute(envelope.conversation, persistedSelection, requestedRoute, signal);
3524
3587
  if ('retry' in resolvedRoute) {
@@ -3532,21 +3595,45 @@ export class DshDeliveryRuntime {
3532
3595
  let foregroundExecution;
3533
3596
  let removeAcceptanceRequest;
3534
3597
  let acceptanceSucceeded = false;
3598
+ let completionPending = false;
3535
3599
  let goalContinuation;
3536
3600
  let goalContinuationQuiescent = true;
3537
3601
  let removeAbort;
3538
3602
  let removeProgress;
3539
3603
  let progressOpen = true;
3604
+ let restartRun;
3605
+ let restartCheckpoint;
3540
3606
  let progressQueue = Promise.resolve();
3607
+ let progressTerminal = false;
3608
+ const drainProgress = async () => {
3609
+ let timeout;
3610
+ try {
3611
+ await Promise.race([progressQueue, new Promise(resolve => {
3612
+ timeout = setTimeout(resolve, 5_000);
3613
+ timeout.unref?.();
3614
+ })]);
3615
+ }
3616
+ finally {
3617
+ if (timeout !== undefined)
3618
+ clearTimeout(timeout);
3619
+ }
3620
+ };
3541
3621
  const publishProgress = (update) => {
3542
- if (!progressOpen)
3622
+ const terminal = update.kind === 'completed' || update.kind === 'failed' || update.kind === 'paused';
3623
+ if ((!progressOpen && !terminal) || progressTerminal)
3543
3624
  return;
3544
- progressQueue = progressQueue
3545
- .then(async () => {
3546
- if (checkAuthorization() !== 'authorized')
3625
+ if (terminal)
3626
+ progressTerminal = true;
3627
+ const publish = async () => {
3628
+ if ((!terminal && progressTerminal) || checkAuthorization() !== 'authorized')
3547
3629
  return;
3548
3630
  await this.options.progress(binding, envelope.eventId, update);
3549
- })
3631
+ };
3632
+ // Terminal journaling must not wait behind a slow provider's step updates.
3633
+ // The adapter preserves provider ordering after staging the durable result.
3634
+ progressQueue = (terminal
3635
+ ? Promise.all([progressQueue, publish()]).then(() => { })
3636
+ : progressQueue.then(publish))
3550
3637
  // Progress is presentation-only: its provider failure must never retry an Agent turn.
3551
3638
  .catch(() => { });
3552
3639
  };
@@ -3627,8 +3714,27 @@ export class DshDeliveryRuntime {
3627
3714
  // an owner tool can create/resume a goal in that very first turn and the
3628
3715
  // host driver is otherwise free to claim its first round at idle.
3629
3716
  goalContinuation = this.waitForNativeGoalContinuation(binding, envelope, agent, signal);
3630
- acceptance = this.options.prepareForegroundTaskAcceptance(binding, envelope);
3631
- foregroundExecution = this.options.beginForegroundTaskExecution(binding, envelope);
3717
+ acceptance = restartContinuation === undefined ? this.options.prepareForegroundTaskAcceptance(binding, envelope) : undefined;
3718
+ foregroundExecution = restartContinuation?.inboxId ?? this.options.beginForegroundTaskExecution(binding, envelope);
3719
+ if (restart !== undefined && foregroundExecution !== undefined && restart.store.getTask(foregroundExecution) !== undefined) {
3720
+ if (restartContinuation !== undefined) {
3721
+ const checkpoint = restartContinuation.checkpoint;
3722
+ if (checkpoint === undefined || restartDigest(agent.session.snapshotEvents().slice(0, checkpoint.sessionEventCount)) !== checkpoint.sessionDigest
3723
+ || checkpoint.continuationTextDigest !== restartDigest(restartContinuationText)
3724
+ || createRestartContinuation({ continuationId: restartContinuation.id, text: restartContinuationText }).id !== checkpoint.continuationMessageId) {
3725
+ throw new Error('assistant-delivery: restart checkpoint does not match the persisted Session');
3726
+ }
3727
+ }
3728
+ restartRun = attachRestartRun({ agent, inboxId: foregroundExecution, binding, envelope,
3729
+ store: restart.store, config: restart.config,
3730
+ isRestartSafeTool: restart.isRestartSafeTool,
3731
+ ...(restartContinuation === undefined ? {} : { continuation: restartContinuation }),
3732
+ authorize: () => checkAuthorization() === 'authorized' && restart.authorizeTask(foregroundExecution),
3733
+ authorizeRestart: () => acceptance === undefined && !goalContinuation?.pinActiveArmedGoal()
3734
+ && restart.authorizeTask(foregroundExecution),
3735
+ });
3736
+ this.restartRuns.set(agent, restartRun);
3737
+ }
3632
3738
  if (acceptance !== undefined || foregroundExecution !== undefined)
3633
3739
  removeAcceptanceRequest = this.ctx.on('session/event', (session, event) => {
3634
3740
  if (session !== agent.session || event.type !== 'request/header')
@@ -3646,11 +3752,32 @@ export class DshDeliveryRuntime {
3646
3752
  // Once the durable marker exists, even a synchronous followup failure is ambiguous:
3647
3753
  // implementations may enqueue before throwing, so no retry is safe.
3648
3754
  dispatched = true;
3649
- const message = createUserMessage({ content, source: {
3755
+ const message = restartContinuation === undefined ? createUserMessage({ content, source: {
3650
3756
  kind: 'delivery', channel: envelope.channel, account: envelope.account, eventId: envelope.eventId,
3651
3757
  trust: 'untrusted',
3652
- } });
3653
- let deliveryTurn = await this.followupTurn(agent, message);
3758
+ } }) : createRestartContinuation({ continuationId: restartContinuation.id, text: restartContinuationText });
3759
+ let deliveryTurn = await this.followupTurn(agent, message, undefined, undefined, restartContinuation === undefined ? undefined
3760
+ : () => admitRestartContinuation(agent, message, { signal,
3761
+ authorize: () => {
3762
+ this.sessionLeases.assert(String(agent.session.id));
3763
+ if (!restart.authorize(restartContinuation))
3764
+ throw new Error('restart authority changed');
3765
+ },
3766
+ flush: () => sessions.flush(agent.session),
3767
+ onAdmitted: () => {
3768
+ this.sessionLeases.assert(String(agent.session.id));
3769
+ restart.onContinuationAdmitted(restartContinuation, String(agent.session.id));
3770
+ },
3771
+ }), signal);
3772
+ if (restartRun?.requested() !== undefined) {
3773
+ await this.waitForAgentIdle(agent, signal);
3774
+ restartCheckpoint = await restartRun.checkpoint();
3775
+ this.options.replyCommand(binding, envelope.eventId, {
3776
+ text: '已保存当前任务,准备进行维护重启;服务就绪后会自动继续,无需重新发送请求。', format: 'plain',
3777
+ }, `inbound:${foregroundExecution}:maintenance`);
3778
+ // Publish the maintenance handoff only after teardown and ready commit.
3779
+ return { outcome: 'processed' };
3780
+ }
3654
3781
  let completionKind;
3655
3782
  let automaticTurns = 0;
3656
3783
  let assembledText = '';
@@ -3844,7 +3971,7 @@ export class DshDeliveryRuntime {
3844
3971
  goalContinuationQuiescent = await goalContinuation.settle();
3845
3972
  }
3846
3973
  signal.throwIfAborted();
3847
- publishProgress({ kind: 'completed' });
3974
+ completionPending = true;
3848
3975
  acceptanceSucceeded = goalContinuationQuiescent;
3849
3976
  return { outcome: 'processed' };
3850
3977
  }
@@ -3858,7 +3985,13 @@ export class DshDeliveryRuntime {
3858
3985
  return { outcome: 'processed' };
3859
3986
  }
3860
3987
  if (dispatched) {
3861
- publishProgress({ kind: 'failed' });
3988
+ const timedOut = signal.reason instanceof AgentTaskTimeoutError;
3989
+ publishProgress({ kind: 'failed', ...(timedOut
3990
+ ? { code: 'agent-task-timeout' } : {}) });
3991
+ // Preserve an exact durable diagnosis so the existing interruption
3992
+ // Outbox can notify the owner even when progress presentation failed.
3993
+ if (timedOut)
3994
+ return { outcome: 'not-processed', failureCode: 'agent-task-timeout', retryable: false };
3862
3995
  throw new Error(`assistant-delivery: Agent turn became ambiguous: ${String(error)}`, { cause: error });
3863
3996
  }
3864
3997
  if (causedByDurableIdentity(error)) {
@@ -3906,23 +4039,49 @@ export class DshDeliveryRuntime {
3906
4039
  removeProgress?.();
3907
4040
  progressOpen = false;
3908
4041
  let disposed = false;
4042
+ let maintenanceReady = false;
3909
4043
  try {
3910
- disposed = await this.disposeAfterReplyBoundary(binding.sessionId, handle, () => goalContinuation?.dispose(), !goalContinuationQuiescent, goalContinuation?.teardownTimeoutMs());
4044
+ disposed = await this.disposeAfterReplyBoundary(binding.sessionId, handle, () => goalContinuation?.dispose(), !goalContinuationQuiescent, restartCheckpoint === undefined && restartContinuation === undefined
4045
+ ? goalContinuation?.teardownTimeoutMs() ?? 10_000 : 10_000);
3911
4046
  }
3912
4047
  finally {
3913
4048
  acceptanceSucceeded = acceptanceSucceeded && disposed && !signal.aborted
3914
4049
  && (goalContinuation?.isQuiescent() ?? true);
3915
- if (foregroundExecution !== undefined) {
4050
+ if (foregroundExecution !== undefined && restartCheckpoint === undefined) {
3916
4051
  try {
3917
4052
  this.options.completeForegroundTaskExecution(foregroundExecution, {
3918
4053
  status: acceptanceSucceeded ? 'succeeded' : 'unknown', quiescent: acceptanceSucceeded,
3919
4054
  });
3920
4055
  }
3921
4056
  catch (error) {
4057
+ acceptanceSucceeded = false;
3922
4058
  this.ctx.logger.warn(`assistant-delivery: foreground execution completion failed: ${String(error)}`);
3923
4059
  }
3924
4060
  }
3925
- if (acceptance !== undefined) {
4061
+ if (restart !== undefined && restartContinuation === undefined && restartCheckpoint === undefined) {
4062
+ const unfinished = restartRun?.requested();
4063
+ if (unfinished !== undefined)
4064
+ restart.store.fail(unfinished.id, 'restart-checkpoint-failed');
4065
+ }
4066
+ if (restart !== undefined && restartCheckpoint !== undefined) {
4067
+ if (disposed && !signal.aborted && restart.authorize(restartCheckpoint)) {
4068
+ restart.store.ready(restartCheckpoint.id);
4069
+ maintenanceReady = true;
4070
+ }
4071
+ else
4072
+ restart.store.fail(restartCheckpoint.id, 'restart-teardown-not-quiescent');
4073
+ }
4074
+ if (restart !== undefined && restartContinuation !== undefined) {
4075
+ const current = restart.store.getRequest(restartContinuation.id);
4076
+ if (current?.status === 'resuming') {
4077
+ if (acceptanceSucceeded)
4078
+ restart.store.complete(restartContinuation.id);
4079
+ else
4080
+ restart.store.fail(restartContinuation.id, 'restart-continuation-outcome-unknown');
4081
+ }
4082
+ }
4083
+ restartRun?.dispose();
4084
+ if (acceptance !== undefined && restartCheckpoint === undefined) {
3926
4085
  try {
3927
4086
  await this.options.completeForegroundTaskAcceptance(acceptance, {
3928
4087
  status: acceptanceSucceeded ? 'succeeded' : 'unknown', quiescent: acceptanceSucceeded,
@@ -3932,6 +4091,16 @@ export class DshDeliveryRuntime {
3932
4091
  this.ctx.logger.warn(`assistant-delivery: acceptance completion failed: ${String(error)}`);
3933
4092
  }
3934
4093
  }
4094
+ // A provider-confirmed success cannot precede the durable execution
4095
+ // result: teardown can still fail after the final reply was queued.
4096
+ if (restartCheckpoint !== undefined)
4097
+ publishProgress(maintenanceReady
4098
+ ? { kind: 'paused' } : { kind: 'failed', code: 'INTERRUPTED_RESULT_UNKNOWN' });
4099
+ else if (completionPending)
4100
+ publishProgress(acceptanceSucceeded
4101
+ ? { kind: 'completed' } : { kind: 'failed', code: 'INTERRUPTED_RESULT_UNKNOWN' });
4102
+ // Terminal journal admission precedes the bounded provider drain.
4103
+ await drainProgress();
3935
4104
  }
3936
4105
  }
3937
4106
  }