@dsh-enhanced/assistant-delivery 0.1.24 → 0.1.31

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 (66) hide show
  1. package/README.md +81 -10
  2. package/lib/acceptance.d.ts +46 -0
  3. package/lib/acceptance.d.ts.map +1 -0
  4. package/lib/acceptance.js +2 -0
  5. package/lib/acceptance.js.map +1 -0
  6. package/lib/agent-runtime.d.ts +33 -3
  7. package/lib/agent-runtime.d.ts.map +1 -1
  8. package/lib/agent-runtime.js +835 -62
  9. package/lib/agent-runtime.js.map +1 -1
  10. package/lib/coordinator.d.ts +4 -1
  11. package/lib/coordinator.d.ts.map +1 -1
  12. package/lib/coordinator.js +9 -0
  13. package/lib/coordinator.js.map +1 -1
  14. package/lib/feedback-command.d.ts +21 -1
  15. package/lib/feedback-command.d.ts.map +1 -1
  16. package/lib/feedback-command.js +18 -0
  17. package/lib/feedback-command.js.map +1 -1
  18. package/lib/goal-wake-types.d.ts +93 -0
  19. package/lib/goal-wake-types.d.ts.map +1 -0
  20. package/lib/goal-wake-types.js +70 -0
  21. package/lib/goal-wake-types.js.map +1 -0
  22. package/lib/inbound-images.d.ts.map +1 -1
  23. package/lib/inbound-images.js +11 -9
  24. package/lib/inbound-images.js.map +1 -1
  25. package/lib/index.d.ts +4 -0
  26. package/lib/index.d.ts.map +1 -1
  27. package/lib/index.js +1 -0
  28. package/lib/index.js.map +1 -1
  29. package/lib/native-web-owner.d.ts +78 -0
  30. package/lib/native-web-owner.d.ts.map +1 -0
  31. package/lib/native-web-owner.js +291 -0
  32. package/lib/native-web-owner.js.map +1 -0
  33. package/lib/operator-snapshot.d.ts +80 -0
  34. package/lib/operator-snapshot.d.ts.map +1 -0
  35. package/lib/operator-snapshot.js +935 -0
  36. package/lib/operator-snapshot.js.map +1 -0
  37. package/lib/operator.d.ts +2 -0
  38. package/lib/operator.d.ts.map +1 -1
  39. package/lib/operator.js +10 -0
  40. package/lib/operator.js.map +1 -1
  41. package/lib/service.d.ts +63 -13
  42. package/lib/service.d.ts.map +1 -1
  43. package/lib/service.js +917 -349
  44. package/lib/service.js.map +1 -1
  45. package/lib/session-lease-runtime.d.ts +69 -0
  46. package/lib/session-lease-runtime.d.ts.map +1 -0
  47. package/lib/session-lease-runtime.js +395 -0
  48. package/lib/session-lease-runtime.js.map +1 -0
  49. package/lib/session-lease-types.d.ts +27 -0
  50. package/lib/session-lease-types.d.ts.map +1 -0
  51. package/lib/session-lease-types.js +2 -0
  52. package/lib/session-lease-types.js.map +1 -0
  53. package/lib/sqlite.d.ts +1 -1
  54. package/lib/sqlite.d.ts.map +1 -1
  55. package/lib/sqlite.js +201 -3
  56. package/lib/sqlite.js.map +1 -1
  57. package/lib/store.d.ts +133 -4
  58. package/lib/store.d.ts.map +1 -1
  59. package/lib/store.js +771 -43
  60. package/lib/store.js.map +1 -1
  61. package/lib/types.d.ts +9 -2
  62. package/lib/types.d.ts.map +1 -1
  63. package/lib/types.js.map +1 -1
  64. package/lib/version.d.ts +1 -1
  65. package/lib/version.js +1 -1
  66. package/package.json +61 -44
@@ -2,16 +2,19 @@ import { createHash } from 'node:crypto';
2
2
  import { mkdir, stat } from 'node:fs/promises';
3
3
  import { installModelSelection, } from '@deepseek-ai/dsh-agent';
4
4
  import { contentHasImage, createUserMessage, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
5
- import { effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
5
+ import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
6
6
  import { isAppendSurfaceEvent, SessionId } from '@deepseek-ai/dsh-session';
7
- import { effectiveApprovalPolicy, setApprovalPolicy, } from '@deepseek-ai/dsh-user-approval';
7
+ import { setApprovalPolicy, } from '@deepseek-ai/dsh-user-approval';
8
8
  import { approvalReviewerOf, setApprovalReviewer, waitForApprovalReviewerSessionEventReady, } from '@dsh-enhanced/assistant-policy';
9
9
  import { resolveLlmRouteCapability } from '@dsh-enhanced/llm-route-capabilities';
10
10
  import { externalPrincipalId } from './canonical.js';
11
11
  import { feedbackSignalInput, feedbackUsage, parseFeedbackCommand, } from './feedback-command.js';
12
+ import { objectiveStateMessage } from './feedback-command.js';
12
13
  import { learningCommandUsage, parseLearningCommand, serializeLearningExport, } from './learning-command.js';
13
14
  import { isExactDeliveryCommand, learningDispatchRecoveryCode, parseDeliveryCommand, permissionDispatchRecoveryCode, } from './session-commands.js';
14
15
  import { DeliveryStoreError } from './store.js';
16
+ import { SessionLeaseUnavailable } from './session-lease-runtime.js';
17
+ import { isDeliveryGoalWakeDeadline } from './goal-wake-types.js';
15
18
  import { parseWorkflowCommand, workflowCommandUsage, } from './workflow-command.js';
16
19
  const SAFE_NATIVE_COMMANDS = new Set(['compact']);
17
20
  class UserSessionCancellation extends Error {
@@ -477,16 +480,17 @@ async function appendApprovalReviewer(session, reviewer) {
477
480
  throw new ApprovalReviewerReaderUnavailableError(error);
478
481
  }
479
482
  }
480
- function currentPermissionLevel(service, targets, events) {
481
- const current = service.current(events);
483
+ function currentPermissionLevel(service, targets, session) {
484
+ const events = session.snapshotEvents();
485
+ const current = service.current(session);
482
486
  if (current === targets.full) {
483
- return effectiveSandboxMode(events) === 'danger-full-access'
484
- && effectiveApprovalPolicy(events) === 'never'
487
+ return explicitSandboxMode(events) === 'danger-full-access'
488
+ && explicitApprovalPolicy(events) === 'never'
485
489
  && approvalReviewerOf(events) === 'none'
486
490
  ? 'full'
487
491
  : 'custom';
488
492
  }
489
- if (effectiveSandboxMode(events) !== 'workspace-write' || effectiveApprovalPolicy(events) !== 'ask')
493
+ if (explicitSandboxMode(events) !== 'workspace-write' || explicitApprovalPolicy(events) !== 'ask')
490
494
  return 'custom';
491
495
  const reviewer = approvalReviewerOf(events);
492
496
  if (targets.ask === targets.auto && current === targets.ask)
@@ -507,7 +511,7 @@ function canonicalPresetReviewer(name) {
507
511
  return undefined;
508
512
  }
509
513
  async function ensurePermissionReviewer(session, preset, reviewer) {
510
- if (approvalReviewerOf(session.events) === reviewer)
514
+ if (approvalReviewerOf(session.snapshotEvents()) === reviewer)
511
515
  return;
512
516
  if (canonicalPresetReviewer(preset) === reviewer) {
513
517
  // PermissionPresetService deliberately makes a net-zero selection a no-op.
@@ -519,17 +523,17 @@ async function ensurePermissionReviewer(session, preset, reviewer) {
519
523
  await appendApprovalReviewer(session, reviewer);
520
524
  }
521
525
  function ensureSandboxMode(session, mode) {
522
- if (explicitSandboxMode(session.events) === mode)
526
+ if (explicitSandboxMode(session.snapshotEvents()) === mode)
523
527
  return;
524
528
  setSandboxMode(session, mode);
525
- if (explicitSandboxMode(session.events) !== mode)
529
+ if (explicitSandboxMode(session.snapshotEvents()) !== mode)
526
530
  session.append('sandbox/mode', { mode });
527
531
  }
528
532
  function ensureApprovalPolicy(session, policy) {
529
- if (explicitApprovalPolicy(session.events) === policy)
533
+ if (explicitApprovalPolicy(session.snapshotEvents()) === policy)
530
534
  return;
531
535
  setApprovalPolicy(session, policy);
532
- if (explicitApprovalPolicy(session.events) !== policy)
536
+ if (explicitApprovalPolicy(session.snapshotEvents()) !== policy)
533
537
  session.append('approval/policy', { policy });
534
538
  }
535
539
  function permissionLevelLabel(level) {
@@ -846,7 +850,7 @@ function assertAdoptableUnstartedSession(session, workspace, presetId) {
846
850
  if (session.header.cwd !== workspace || session.header.agentPreset !== presetId) {
847
851
  throw new DurableAgentIdentityError('assistant-delivery: durable orphan session identity does not match the requested generation');
848
852
  }
849
- if (session.events.some(event => event.type === 'turn/start' || event.type === 'user/message')
853
+ if (session.snapshotEvents().some(event => event.type === 'turn/start' || event.type === 'user/message')
850
854
  || session.deriveMessages().length > 0) {
851
855
  throw new DurableAgentIdentityError('assistant-delivery: refusing to adopt a deterministic session that already started a user turn');
852
856
  }
@@ -1018,16 +1022,27 @@ function finalAssistant(events, from, expectedTurn) {
1018
1022
  ...(failureCode === undefined ? {} : { failureCode }),
1019
1023
  };
1020
1024
  }
1025
+ function isScheduledGoalRuntimeContext(source) {
1026
+ return (source.kind === 'plugin' && source.plugin === '@deepseek-ai/dsh-system-prompt' && source.form === 'snapshot')
1027
+ || (source.kind === 'skill-catalog' && source.form === 'catalog');
1028
+ }
1021
1029
  export class DshDeliveryRuntime {
1022
1030
  ctx;
1023
1031
  policy;
1024
1032
  options;
1025
1033
  dispatchControl = 'explicit';
1026
1034
  activeSessionControls = new Map();
1035
+ sessionLeases;
1027
1036
  constructor(ctx, policy, options) {
1028
1037
  this.ctx = ctx;
1029
1038
  this.policy = policy;
1030
1039
  this.options = options;
1040
+ this.sessionLeases = options.sessionLeases;
1041
+ }
1042
+ async resumeAgent(options) {
1043
+ const id = String(options.resumeSessionId);
1044
+ this.sessionLeases.assert(id);
1045
+ return this.sessionLeases.resume(this.ctx, options);
1031
1046
  }
1032
1047
  async cancelActive(binding, command) {
1033
1048
  const sessionId = binding.sessionId;
@@ -1045,8 +1060,9 @@ export class DshDeliveryRuntime {
1045
1060
  if (!control.controller.signal.aborted) {
1046
1061
  control.controller.abort(new UserSessionCancellation(control.command));
1047
1062
  }
1063
+ this.sessionLeases.cancel(sessionId, new UserSessionCancellation(control.command));
1048
1064
  }
1049
- // rc.8 `whenIdle()` follows every later wakeup. Waiting on it here lets an
1065
+ // `whenIdle()` follows every later wakeup. Waiting on it here lets an
1050
1066
  // unrelated followup keep `/stop` or `/new` alive forever. Clear pending
1051
1067
  // input and wait only for Delivery's exact reply-scheduling boundary.
1052
1068
  if (activeAgent)
@@ -1085,27 +1101,377 @@ export class DshDeliveryRuntime {
1085
1101
  if (this.activeSessionControls.get(sessionId) === control)
1086
1102
  this.activeSessionControls.delete(sessionId);
1087
1103
  }
1088
- async disposeAfterReplyBoundary(sessionId, handle) {
1104
+ /**
1105
+ * Keep one Delivery-owned handle alive just long enough for the host's native
1106
+ * goal driver to retire the exact armed goal it observed. Delivery never
1107
+ * queues a round itself: the driver remains the only continuation producer.
1108
+ */
1109
+ waitForNativeGoalContinuation(binding, envelope, agent, signal) {
1110
+ if (this.options.goalContinuationTimeoutMs === 0)
1111
+ return undefined;
1112
+ const goals = this.ctx.get('goals');
1113
+ const agents = this.ctx.get('agents');
1114
+ if (goals === undefined || agents?.get(agent.id) !== agent)
1115
+ return undefined;
1116
+ let initial;
1117
+ try {
1118
+ initial = goals.get(agent);
1119
+ }
1120
+ catch {
1121
+ return undefined;
1122
+ }
1123
+ // Install this watcher before the foreground followup. A goal can be
1124
+ // resumed or created by that turn, and the native round driver observes
1125
+ // the same idle boundary immediately afterwards.
1126
+ if (initial?.phase !== 'active' || initial.activation !== 'armed')
1127
+ initial = undefined;
1128
+ let goalId = initial === undefined ? undefined : String(initial.id);
1129
+ const session = agent.session;
1130
+ let settled = false;
1131
+ let settledQuiescent = false;
1132
+ // `goal/changed` is synchronous. In particular, disarming our own goal can
1133
+ // emit it before cancelUnknown() has reached finish(false), so remember that
1134
+ // this is a forced, unknown outcome before mutating the native goal.
1135
+ let cancelling = false;
1136
+ let activeGoalSteps = 0;
1137
+ let settleRequested = false;
1138
+ let resolve;
1139
+ const completion = new Promise(done => { resolve = done; });
1140
+ let timeout;
1141
+ let deadlineAt;
1142
+ let removeGoalChanged;
1143
+ let removeStatus;
1144
+ let removeAbort;
1145
+ let removePreStep;
1146
+ let removeRequest;
1147
+ let removePreExecute;
1148
+ let removeToolGuard;
1149
+ const releaseFences = () => {
1150
+ removeGoalChanged?.();
1151
+ removeStatus?.();
1152
+ removeAbort?.();
1153
+ removePreStep?.();
1154
+ removeRequest?.();
1155
+ removePreExecute?.();
1156
+ removeToolGuard?.();
1157
+ removeGoalChanged = undefined;
1158
+ removeStatus = undefined;
1159
+ removeAbort = undefined;
1160
+ removePreStep = undefined;
1161
+ removeRequest = undefined;
1162
+ removePreExecute = undefined;
1163
+ removeToolGuard = undefined;
1164
+ };
1165
+ const finish = (quiescent) => {
1166
+ if (settled)
1167
+ return;
1168
+ settled = true;
1169
+ settledQuiescent = quiescent;
1170
+ resolve(quiescent);
1171
+ };
1172
+ const isAuthorized = () => {
1173
+ try {
1174
+ return this.options.isInboundAuthorized(binding, envelope);
1175
+ }
1176
+ catch {
1177
+ return false;
1178
+ }
1179
+ };
1180
+ const pinArmedGoal = () => {
1181
+ if (goalId !== undefined || settleRequested)
1182
+ return goalId !== undefined;
1183
+ try {
1184
+ const current = goals.get(agent);
1185
+ if (current?.phase !== 'active' || current.activation !== 'armed')
1186
+ return false;
1187
+ initial = current;
1188
+ goalId = String(current.id);
1189
+ return true;
1190
+ }
1191
+ catch {
1192
+ return false;
1193
+ }
1194
+ };
1195
+ const disposition = () => {
1196
+ // A replacement is deliberately not adopted. It needs its own explicit
1197
+ // foreground owner/acceptance boundary.
1198
+ if (goalId === undefined || initial === undefined)
1199
+ return 'still-running';
1200
+ if (agents?.get(agent.id) !== agent || agent.session !== session)
1201
+ return 'unavailable';
1202
+ try {
1203
+ const current = goals.get(agent);
1204
+ if (current === undefined)
1205
+ return 'settled';
1206
+ if (String(current.id) !== goalId)
1207
+ return 'replacement';
1208
+ return current.phase !== 'active' || current.activation !== 'armed'
1209
+ ? 'settled'
1210
+ : 'still-running';
1211
+ }
1212
+ catch {
1213
+ return 'unavailable';
1214
+ }
1215
+ };
1216
+ const disarmAndCancel = (reason) => {
1217
+ try {
1218
+ const current = goals.get(agent);
1219
+ if (goalId !== undefined && current !== undefined && String(current.id) === goalId
1220
+ && current.phase === 'active' && current.activation === 'armed') {
1221
+ goals.disarm(agent);
1222
+ }
1223
+ }
1224
+ catch { }
1225
+ try {
1226
+ agent.cancel({ kind: 'hook', reason });
1227
+ }
1228
+ catch { }
1229
+ };
1230
+ const finishNaturally = () => {
1231
+ // Goal state is only an observation. It cannot turn a cancelled or
1232
+ // revoked foreground authority into a successful, quiescent outcome.
1233
+ if (signal.aborted) {
1234
+ cancelUnknown('assistant-delivery-goal-continuation-cancelled');
1235
+ }
1236
+ else if (!isAuthorized()) {
1237
+ cancelUnknown('assistant-delivery-goal-continuation-authorization-revoked');
1238
+ }
1239
+ else {
1240
+ finish(true);
1241
+ }
1242
+ };
1243
+ const maybeFinish = () => {
1244
+ if (settled || cancelling || activeGoalSteps !== 0 || goalId === undefined)
1245
+ return;
1246
+ // A terminal goal record does not prove that its final model/tool turn
1247
+ // has retired. `whenIdle()` is deliberately avoided here because it
1248
+ // can follow a later wakeup forever; the current Agent state and inbox
1249
+ // are the bounded observation needed before declaring quiescence.
1250
+ if (agent.status !== 'idle' || agent.inbox.hasPending)
1251
+ return;
1252
+ const current = disposition();
1253
+ if (current === 'settled')
1254
+ finishNaturally();
1255
+ // Missing identity/readback and replacement are intentionally unknown:
1256
+ // neither proves that the exact pinned goal drained.
1257
+ else if (current !== 'still-running')
1258
+ finish(false);
1259
+ };
1260
+ const cancelUnknown = (reason) => {
1261
+ if (cancelling)
1262
+ return;
1263
+ cancelling = true;
1264
+ disarmAndCancel(reason);
1265
+ // A hard timeout or /stop must settle this foreground operation even if
1266
+ // a third-party tool ignores cancellation. Keep the fences until the
1267
+ // Delivery-owned handle is disposed, rather than waiting for idle here.
1268
+ if (!settled)
1269
+ finish(false);
1270
+ };
1271
+ const guardContinuationAuthorization = () => {
1272
+ if (cancelling)
1273
+ return false;
1274
+ if (isAuthorized())
1275
+ return true;
1276
+ cancelUnknown('assistant-delivery-goal-continuation-authorization-revoked');
1277
+ return false;
1278
+ };
1279
+ const teardownTimeoutMs = () => deadlineAt === undefined
1280
+ ? undefined
1281
+ : Math.max(0, deadlineAt - Date.now());
1282
+ const isQuiescent = () => {
1283
+ if (!settledQuiescent || cancelling)
1284
+ return false;
1285
+ // The goal can reach a terminal native state before the Agent scope has
1286
+ // finished disposing. Authority is still live at that point.
1287
+ if (signal.aborted || !isAuthorized()) {
1288
+ cancelUnknown(signal.aborted
1289
+ ? 'assistant-delivery-goal-continuation-cancelled'
1290
+ : 'assistant-delivery-goal-continuation-authorization-revoked');
1291
+ return false;
1292
+ }
1293
+ return true;
1294
+ };
1295
+ removeGoalChanged = this.ctx.on('goal/changed', ({ agent: changed }) => {
1296
+ if (changed !== agent)
1297
+ return;
1298
+ pinArmedGoal();
1299
+ maybeFinish();
1300
+ });
1301
+ removeStatus = this.ctx.on('agent/status', ({ agent: changed, status }) => {
1302
+ // The round driver can wake after an idle status, so idle alone is not a
1303
+ // completion signal. It is only useful after the goal itself disarmed.
1304
+ if (changed === agent && status === 'idle')
1305
+ maybeFinish();
1306
+ });
1307
+ const aborted = () => cancelUnknown('assistant-delivery-goal-continuation-cancelled');
1308
+ signal.addEventListener('abort', aborted, { once: true });
1309
+ removeAbort = () => signal.removeEventListener('abort', aborted);
1310
+ removePreStep = agent.ctx.on('agent/pre-step', async (_input, next) => {
1311
+ // The native loop supplies only the inbox claim for a later model step;
1312
+ // after a tool result it need not repeat the original `goal` source.
1313
+ // Once this watcher fixed a GoalId, every later model step on the owned
1314
+ // Agent is therefore fenced until its handle tears down.
1315
+ if (goalId === undefined)
1316
+ return await next();
1317
+ if (!guardContinuationAuthorization())
1318
+ return { kind: 'reject' };
1319
+ activeGoalSteps += 1;
1320
+ try {
1321
+ const result = await next();
1322
+ // A pre-step waterfall's `next()` can finish before the next model
1323
+ // boundary. Recheck immediately after it so a revocation in that
1324
+ // window still cancels the owned Agent before another request/tool.
1325
+ if (!guardContinuationAuthorization())
1326
+ return { kind: 'reject' };
1327
+ return result;
1328
+ }
1329
+ finally {
1330
+ activeGoalSteps -= 1;
1331
+ // Do not wait for `whenIdle()`: an uncooperative tool can keep it
1332
+ // pending forever. The timeout/cancellation path already settled
1333
+ // non-quiescent, and the fences remain until handle teardown.
1334
+ maybeFinish();
1335
+ }
1336
+ }, { prepend: true });
1337
+ removeRequest = agent.ctx.on('agent/request', async (_payload, next) => {
1338
+ if (goalId === undefined)
1339
+ return await next();
1340
+ if (!guardContinuationAuthorization()) {
1341
+ throw new Error('assistant-delivery: goal continuation authorization revoked before model request');
1342
+ }
1343
+ const request = await next();
1344
+ if (!guardContinuationAuthorization()) {
1345
+ throw new Error('assistant-delivery: goal continuation authorization revoked while preparing model request');
1346
+ }
1347
+ return request;
1348
+ }, { prepend: true });
1349
+ removePreExecute = agent.ctx.on('tools/pre-execute', async (_execution, next) => {
1350
+ if (guardContinuationAuthorization())
1351
+ return await next();
1352
+ return { kind: 'deny', reason: 'assistant-delivery: goal continuation authorization revoked' };
1353
+ }, { prepend: true });
1354
+ removeToolGuard = agent.ctx.tools.guard(() => guardContinuationAuthorization()
1355
+ ? undefined
1356
+ : 'assistant-delivery: goal continuation authorization revoked');
1357
+ const beginSettling = () => {
1358
+ if (!settleRequested) {
1359
+ settleRequested = true;
1360
+ // A goal may create and complete during the foreground pre-step,
1361
+ // before Delivery reaches `settle()`. That still leaves its Agent
1362
+ // scope to tear down, so it needs the same bounded deadline.
1363
+ deadlineAt = Date.now() + this.options.goalContinuationTimeoutMs;
1364
+ }
1365
+ if (settled)
1366
+ return completion;
1367
+ if (goalId === undefined) {
1368
+ finishNaturally();
1369
+ return completion;
1370
+ }
1371
+ timeout = setTimeout(() => cancelUnknown('assistant-delivery-goal-continuation-timeout'), this.options.goalContinuationTimeoutMs);
1372
+ timeout.unref?.();
1373
+ // A driver may have already claimed its first round before the original
1374
+ // Delivery turn reached its reply boundary. All step and tool guards
1375
+ // were installed above, before that round could begin.
1376
+ if (signal.aborted)
1377
+ cancelUnknown('assistant-delivery-goal-continuation-cancelled');
1378
+ else if (!isAuthorized())
1379
+ cancelUnknown('assistant-delivery-goal-continuation-authorization-revoked');
1380
+ else
1381
+ maybeFinish();
1382
+ return completion;
1383
+ };
1384
+ if (signal.aborted)
1385
+ aborted();
1386
+ return {
1387
+ settle: beginSettling,
1388
+ teardownTimeoutMs,
1389
+ isQuiescent,
1390
+ dispose: () => {
1391
+ if (timeout !== undefined)
1392
+ clearTimeout(timeout);
1393
+ if (!settled)
1394
+ cancelUnknown('assistant-delivery-goal-continuation-teardown');
1395
+ releaseFences();
1396
+ },
1397
+ };
1398
+ }
1399
+ async disposeAfterReplyBoundary(sessionId, handle, afterDisposed, abandonDisposal = false, timeoutMs) {
1089
1400
  const control = this.activeSessionControls.get(sessionId);
1090
1401
  control?.resolveReplySafe();
1091
- if (handle === undefined)
1092
- return;
1402
+ if (handle === undefined) {
1403
+ afterDisposed?.();
1404
+ return true;
1405
+ }
1093
1406
  const disposal = Promise.resolve().then(() => handle.dispose());
1094
- const disposition = control === undefined
1095
- ? await disposal.then(() => 'disposed')
1096
- : await Promise.race([
1097
- disposal.then(() => 'disposed'),
1098
- control.cancelRequested.then(() => 'cancelled'),
1099
- ]);
1100
- if (disposition === 'cancelled') {
1407
+ let clearDeadline;
1408
+ const deadline = timeoutMs === undefined
1409
+ ? undefined
1410
+ : timeoutMs <= 0
1411
+ ? Promise.resolve('timed-out')
1412
+ : new Promise(resolve => {
1413
+ const timeout = setTimeout(() => resolve('timed-out'), timeoutMs);
1414
+ timeout.unref?.();
1415
+ clearDeadline = () => clearTimeout(timeout);
1416
+ });
1417
+ let removeAbort;
1418
+ const aborted = control === undefined
1419
+ ? undefined
1420
+ : new Promise(resolve => {
1421
+ const abort = () => resolve('aborted');
1422
+ control.controller.signal.addEventListener('abort', abort, { once: true });
1423
+ removeAbort = () => control.controller.signal.removeEventListener('abort', abort);
1424
+ if (control.controller.signal.aborted)
1425
+ abort();
1426
+ });
1427
+ let released = false;
1428
+ const releaseAfterDisposal = () => {
1429
+ if (released)
1430
+ return;
1431
+ released = true;
1432
+ try {
1433
+ afterDisposed?.();
1434
+ }
1435
+ catch (error) {
1436
+ this.ctx.logger.warn(`assistant-delivery: continuation fence release failed: ${String(error)}`);
1437
+ }
1438
+ };
1439
+ let disposition;
1440
+ try {
1441
+ disposition = control === undefined
1442
+ ? (abandonDisposal ? 'abandoned' : await Promise.race([
1443
+ disposal.then(() => 'disposed', () => 'rejected'),
1444
+ ...(deadline === undefined ? [] : [deadline]),
1445
+ ]))
1446
+ : await Promise.race([
1447
+ disposal.then(() => 'disposed', () => 'rejected'),
1448
+ control.cancelRequested.then(() => 'cancelled'),
1449
+ ...(aborted === undefined ? [] : [aborted]),
1450
+ ...(deadline === undefined ? [] : [deadline]),
1451
+ ...(abandonDisposal ? [Promise.resolve('abandoned')] : []),
1452
+ ]);
1453
+ }
1454
+ finally {
1455
+ removeAbort?.();
1456
+ clearDeadline?.();
1457
+ }
1458
+ if (disposition === 'cancelled' || disposition === 'abandoned'
1459
+ || disposition === 'aborted' || disposition === 'timed-out') {
1101
1460
  // The Delivery turn is already cancelled and cannot enqueue another
1102
1461
  // reply. Teardown remains owned, but a third-party disposer must not
1103
1462
  // hold the conversation transition or its fresh generation hostage.
1104
- void disposal.catch(error => {
1105
- this.ctx.logger.warn(`assistant-delivery: cancelled session disposer failed for ${sessionFingerprint(sessionId)}: ${String(error)}`);
1463
+ void disposal.then(() => releaseAfterDisposal(), error => {
1464
+ this.ctx.logger.warn(`assistant-delivery: detached session disposer failed for ${sessionFingerprint(sessionId)}: ${String(error)}`);
1465
+ releaseAfterDisposal();
1106
1466
  });
1107
- return;
1467
+ return false;
1108
1468
  }
1469
+ releaseAfterDisposal();
1470
+ if (disposition === 'rejected') {
1471
+ this.ctx.logger.warn(`assistant-delivery: session disposer failed for ${sessionFingerprint(sessionId)}`);
1472
+ return false;
1473
+ }
1474
+ return true;
1109
1475
  }
1110
1476
  /** Wait for only the turn that claims Delivery's exact message identity. */
1111
1477
  followupTurn(agent, message, onClaimed, onSettled) {
@@ -1209,7 +1575,7 @@ export class DshDeliveryRuntime {
1209
1575
  }
1210
1576
  }
1211
1577
  /**
1212
- * Queue a synthetic completion turn only after the previous exact turn retired. rc.8 may still
1578
+ * Queue a synthetic completion turn only after the previous exact turn retired. The runtime may still
1213
1579
  * serialize scoped tool schemas, but every call is denied before approval/execution while this
1214
1580
  * turn owns the Agent, so recovering prose cannot repeat an external side effect.
1215
1581
  */
@@ -1264,7 +1630,7 @@ export class DshDeliveryRuntime {
1264
1630
  if (agent.status !== 'idle' || agent.inbox.hasPending) {
1265
1631
  throw new AutomaticCompletionUnavailableError('assistant-delivery: automatic completion lost the idle scheduling boundary');
1266
1632
  }
1267
- const latestBoundary = agent.session.events.findLast(event => event.type === 'turn/start' || event.type === 'turn/end');
1633
+ const latestBoundary = agent.session.snapshotEvents().findLast(event => event.type === 'turn/start' || event.type === 'turn/end');
1268
1634
  if (latestBoundary?.type !== 'turn/end' || latestBoundary.data.turn !== expectedPreviousTurn) {
1269
1635
  throw new AutomaticCompletionUnavailableError('assistant-delivery: another turn crossed the automatic completion boundary');
1270
1636
  }
@@ -1292,15 +1658,15 @@ export class DshDeliveryRuntime {
1292
1658
  releaseToolBarriers();
1293
1659
  }
1294
1660
  }
1295
- async setupAgent(agentCtx, workspace, presetId, principal, selected, agentPresets) {
1661
+ async setupAgent(agentCtx, workspace, presetId, principal, selected, agentPresets, initiator = 'external') {
1296
1662
  const agent = agentCtx.agent;
1297
1663
  if (agent === undefined)
1298
1664
  throw new Error('assistant-delivery: unpublished Agent identity is missing');
1299
1665
  if (agent.session.header.cwd !== workspace || agent.session.header.agentPreset !== presetId) {
1300
1666
  throw new DurableAgentIdentityError('assistant-delivery: durable Agent identity does not match its conversation binding');
1301
1667
  }
1302
- const unbind = this.policy.bindInitiator(agent, 'external', principal);
1303
- agentCtx.effect(() => unbind, 'assistant-delivery.external-initiator');
1668
+ const unbind = this.policy.bindInitiator(agent, initiator, principal);
1669
+ agentCtx.effect(() => unbind, `assistant-delivery.${initiator}-initiator`);
1304
1670
  installModelSelection(agentCtx, { current: selected, assembled: undefined });
1305
1671
  await agentPresets?.mount(agentCtx, presetId);
1306
1672
  }
@@ -1343,7 +1709,9 @@ export class DshDeliveryRuntime {
1343
1709
  }
1344
1710
  }
1345
1711
  async reconcileNativeFullPermissionReviewer(session) {
1712
+ this.sessionLeases.assert(String(session.id));
1346
1713
  const result = await this.policy.reconcileNativeFullReviewer(session);
1714
+ this.sessionLeases.assert(String(session.id));
1347
1715
  if (result === 'unavailable') {
1348
1716
  throw new ApprovalReviewerReaderUnavailableError(new Error('assistant-policy: native full reviewer reconciliation is unavailable'));
1349
1717
  }
@@ -1482,7 +1850,7 @@ export class DshDeliveryRuntime {
1482
1850
  signal.throwIfAborted();
1483
1851
  await requireWorkspace(binding.workspace);
1484
1852
  signal.throwIfAborted();
1485
- handle = await agents.resume({
1853
+ handle = await this.resumeAgent({
1486
1854
  resumeSessionId: SessionId(binding.sessionId),
1487
1855
  signal,
1488
1856
  agentOptions: {
@@ -1532,7 +1900,7 @@ export class DshDeliveryRuntime {
1532
1900
  const flushed = await sessions.flush(session);
1533
1901
  return reviewerReady
1534
1902
  && flushed
1535
- && currentPermissionLevel(permissionPresets, targets, session.events) === 'ask';
1903
+ && currentPermissionLevel(permissionPresets, targets, session) === 'ask';
1536
1904
  }
1537
1905
  catch {
1538
1906
  return false;
@@ -1596,8 +1964,8 @@ export class DshDeliveryRuntime {
1596
1964
  if (action.kind === 'invalid' || action.kind === 'warn-full') {
1597
1965
  return retryPermissionRecovery(recovery ?? 'failure-notice');
1598
1966
  }
1599
- const currentStateHash = permissionStateHash(session.events, targets.tableFingerprint);
1600
- const beforeLevel = currentPermissionLevel(permissionPresets, targets, session.events);
1967
+ const currentStateHash = permissionStateHash(session.snapshotEvents(), targets.tableFingerprint);
1968
+ const beforeLevel = currentPermissionLevel(permissionPresets, targets, session);
1601
1969
  const recoveredCommittedTarget = recovery === 'commit'
1602
1970
  && action.kind === 'switch'
1603
1971
  && beforeLevel === action.level;
@@ -1641,7 +2009,7 @@ export class DshDeliveryRuntime {
1641
2009
  return reply({ text: '权限卡片已过期或状态已变化;当前权限未改变。请重新发送 /permissions。', format: 'plain' });
1642
2010
  }
1643
2011
  if (action.kind === 'show') {
1644
- const level = currentPermissionLevel(permissionPresets, targets, session.events);
2012
+ const level = currentPermissionLevel(permissionPresets, targets, session);
1645
2013
  const emergencyStop = this.policy.getEmergencyStop();
1646
2014
  if (emergencyStop.enabled)
1647
2015
  return authorizationFailure('revoked');
@@ -1653,7 +2021,7 @@ export class DshDeliveryRuntime {
1653
2021
  issuedAt,
1654
2022
  expiresAt: issuedAt + this.options.permissionPickerTtlMs,
1655
2023
  current: level,
1656
- expectedStateHash: permissionStateHash(session.events, targets.tableFingerprint),
2024
+ expectedStateHash: permissionStateHash(session.snapshotEvents(), targets.tableFingerprint),
1657
2025
  emergencyStopVersion: emergencyStop.version,
1658
2026
  bindingVersion: binding.version,
1659
2027
  sessionId: binding.sessionId,
@@ -1727,7 +2095,7 @@ export class DshDeliveryRuntime {
1727
2095
  signal.throwIfAborted();
1728
2096
  return retryPermissionRecovery('failure-notice');
1729
2097
  }
1730
- const afterLevel = currentPermissionLevel(permissionPresets, targets, session.events);
2098
+ const afterLevel = currentPermissionLevel(permissionPresets, targets, session);
1731
2099
  if (afterLevel !== action.level) {
1732
2100
  const safeAskPersisted = await compensateToAsk();
1733
2101
  signal.throwIfAborted();
@@ -1812,9 +2180,14 @@ export class DshDeliveryRuntime {
1812
2180
  const setup = async (agentCtx) => {
1813
2181
  await this.setupAgent(agentCtx, workspace, presetId, externalPrincipalId(input.envelope.principal), selected, agentPresets);
1814
2182
  };
2183
+ const lease = this.sessionLeases.open({ kind: 'construction', sessionId: String(id),
2184
+ conversation: input.envelope.conversation, principal: input.envelope.principal,
2185
+ workspace, agentPreset: presetId, generation: input.generation,
2186
+ ...(input.previous === undefined ? {} : { previous: input.previous }) }, input.signal);
1815
2187
  let handle;
1816
2188
  let adopted = false;
1817
2189
  try {
2190
+ lease.dispatch();
1818
2191
  const agentOptions = {
1819
2192
  provider: selected.provider,
1820
2193
  model: selected.model,
@@ -1823,22 +2196,22 @@ export class DshDeliveryRuntime {
1823
2196
  const persistence = this.ctx.get('sessionPersistence');
1824
2197
  const persisted = persistence === undefined
1825
2198
  ? false
1826
- : (await persistence.list(input.signal)).some(header => String(header.id) === String(id));
2199
+ : (await persistence.list(lease.signal)).some(header => String(header.id) === String(id));
1827
2200
  if (persisted) {
1828
- handle = await agents.resume({
2201
+ handle = await this.resumeAgent({
1829
2202
  resumeSessionId: id,
1830
2203
  agentOptions,
1831
- signal: input.signal,
2204
+ signal: lease.signal,
1832
2205
  setup,
1833
2206
  });
1834
2207
  adopted = true;
1835
2208
  }
1836
2209
  if (handle === undefined) {
1837
- handle = await agents.create({
2210
+ handle = await this.sessionLeases.create(this.ctx, {
1838
2211
  sessionId: id,
1839
2212
  meta: { cwd: workspace, agentPreset: presetId },
1840
2213
  agentOptions,
1841
- signal: input.signal,
2214
+ signal: lease.signal,
1842
2215
  setup,
1843
2216
  });
1844
2217
  }
@@ -1853,8 +2226,340 @@ export class DshDeliveryRuntime {
1853
2226
  policyRef: input.previous?.policyRef ?? this.options.policyRef };
1854
2227
  }
1855
2228
  finally {
1856
- await handle?.dispose();
2229
+ try {
2230
+ if (handle !== undefined)
2231
+ await handle.dispose();
2232
+ }
2233
+ finally {
2234
+ lease.close();
2235
+ }
2236
+ }
2237
+ }
2238
+ /**
2239
+ * Resume one previously owner-scheduled native Goal. This is deliberately
2240
+ * not an inbound-message path: it creates no delivery source and never
2241
+ * calls followup(). The native round driver remains the only producer of
2242
+ * `source.kind === 'goal'` work.
2243
+ */
2244
+ async resumeScheduledGoal(binding, input) {
2245
+ const denied = () => ({ outcome: 'denied', dispatched: false, quiescent: false });
2246
+ const now = Date.now();
2247
+ if (!this.validGoalWakeInput(binding, input, now))
2248
+ return denied();
2249
+ const duration = input.deadlineAt - now;
2250
+ const deadline = new AbortController();
2251
+ const expire = setTimeout(() => deadline.abort(new Error('assistant-delivery: scheduled goal wake expired')), duration);
2252
+ expire.unref?.();
2253
+ const abort = () => deadline.abort(input.signal.reason);
2254
+ input.signal.addEventListener('abort', abort, { once: true });
2255
+ if (input.signal.aborted)
2256
+ abort();
2257
+ const signal = deadline.signal;
2258
+ let lease;
2259
+ let control;
2260
+ let handle;
2261
+ let releaseFences;
2262
+ let dispatched = false;
2263
+ let terminal = false;
2264
+ let disposed = false;
2265
+ let goalOutput;
2266
+ try {
2267
+ signal.throwIfAborted();
2268
+ const persisted = this.options.getModelSelection(binding.conversation);
2269
+ const requested = toModelRoute(persisted ?? { provider: this.options.provider, model: this.options.model });
2270
+ const resolved = await this.resolveExecutionRoute(binding.conversation, persisted, requested, signal);
2271
+ if ('retry' in resolved) {
2272
+ return denied();
2273
+ }
2274
+ const selected = agentSelection(resolved.route);
2275
+ const presets = this.options.getAgentPresets();
2276
+ const presetId = presets === undefined ? binding.agentPreset : (await presets.resolve(binding.agentPreset)).id;
2277
+ signal.throwIfAborted();
2278
+ await requireWorkspace(binding.workspace);
2279
+ signal.throwIfAborted();
2280
+ try {
2281
+ lease = this.sessionLeases.open({ kind: 'bound', binding }, signal);
2282
+ }
2283
+ catch (error) {
2284
+ if (error instanceof SessionLeaseUnavailable) {
2285
+ return { outcome: error.kind, dispatched: false, quiescent: false };
2286
+ }
2287
+ throw error;
2288
+ }
2289
+ control = this.beginSessionControl(binding.sessionId, lease.signal);
2290
+ lease.dispatch();
2291
+ handle = await this.resumeAgent({
2292
+ resumeSessionId: SessionId(binding.sessionId),
2293
+ signal: control.controller.signal,
2294
+ agentOptions: { provider: selected.provider, model: selected.model, maxTokens: this.options.maxOutputTokens },
2295
+ setup: async (agentCtx) => {
2296
+ // Restored inbox entries may wake immediately on Session publication.
2297
+ // Install the background-only gate before that publication boundary.
2298
+ if (agentCtx.agent === undefined)
2299
+ throw new Error('assistant-delivery: wake Agent missing');
2300
+ releaseFences = this.installScheduledGoalFences(agentCtx.agent, input);
2301
+ await this.setupAgent(agentCtx, binding.workspace, presetId, input.attestation.principalId, selected, presets, 'background');
2302
+ const llm = this.ctx.get('llm');
2303
+ if (llm === undefined)
2304
+ throw new Error('assistant-delivery: llm service is required');
2305
+ requireAdapterToolCallProtocol(llm, selected.provider, selected.model, presetId, agentCtx.tools.schemas(agentCtx.agent).length);
2306
+ },
2307
+ });
2308
+ const agent = handle.agent;
2309
+ this.assertScheduledGoal(input, agent, 'before-resume');
2310
+ await this.reconcileNativeFullPermissionReviewer(agent.session);
2311
+ this.assertScheduledGoal(input, agent, 'before-resume');
2312
+ const goals = this.ctx.get('goals');
2313
+ const current = goals?.get(agent);
2314
+ if (goals === undefined || current === undefined || String(current.id) !== input.native.goalId
2315
+ || current.revision !== input.native.revision || current.phase !== 'paused'
2316
+ || current.activation !== 'disarmed' || current.roundsStarted >= current.maxGoalRounds) {
2317
+ return denied();
2318
+ }
2319
+ if (agent.status !== 'idle' || agent.inbox.hasPending) {
2320
+ return denied();
2321
+ }
2322
+ this.assertScheduledGoal(input, agent, 'before-resume');
2323
+ input.beforeResume(agent);
2324
+ // `beforeResume` is the durable goal-wake CAS. Once it returned, native
2325
+ // resume might have committed before throwing, so this invocation is no
2326
+ // longer retry-safe regardless of the result of `goals.resume()`.
2327
+ dispatched = true;
2328
+ this.assertScheduledGoal(input, agent, 'before-resume');
2329
+ const beforeSequence = agent.session.snapshotEvents().at(-1)?.seq ?? 0;
2330
+ goals.resume(agent, { id: current.id, revision: current.revision });
2331
+ terminal = await this.waitForScheduledGoalTerminal(agent, input, control.controller.signal);
2332
+ if (terminal && input.includeOutput === true) {
2333
+ this.assertScheduledGoal(input, agent, 'terminal');
2334
+ const events = agent.session.snapshotEvents().filter(event => event.seq > beforeSequence);
2335
+ const message = events.findLast(event => event.type === 'user/message'
2336
+ && event.data.source.kind === 'goal' && String(event.data.source.goalId) === input.native.goalId
2337
+ && event.data.source.revision === input.native.revision + 1);
2338
+ const start = message === undefined ? undefined : events.findLast(event => event.type === 'turn/start' && event.seq < message.seq);
2339
+ const turn = start?.type === 'turn/start' ? start.data.turn : undefined;
2340
+ const end = events.find(event => event.type === 'turn/end' && event.data.turn === turn);
2341
+ const inputs = start === undefined || end === undefined ? [] : events.filter(event => event.seq > start.seq && event.seq < end.seq
2342
+ && event.type === 'user/message' && !isScheduledGoalRuntimeContext(event.data.source));
2343
+ const output = finalAssistant(events, 0, inputs.length === 1 && inputs[0]?.seq === message?.seq ? turn : undefined);
2344
+ if (output.completed && !output.truncated && !output.stopped && !output.hasUnpairedToolCall
2345
+ && output.failureCode === undefined && output.text.trim() !== ''
2346
+ && Buffer.byteLength(output.text, 'utf8') <= this.options.maxTextBytes)
2347
+ goalOutput = output.text;
2348
+ }
2349
+ }
2350
+ catch (error) {
2351
+ if (!(error instanceof SessionLeaseUnavailable) && !signal.aborted) {
2352
+ this.ctx.logger.warn(`assistant-delivery: scheduled goal wake failed: ${String(error)}`);
2353
+ }
2354
+ terminal = false;
2355
+ try {
2356
+ const agent = handle?.agent;
2357
+ const goals = this.ctx.get('goals');
2358
+ if (agent !== undefined) {
2359
+ try {
2360
+ goals?.disarm(agent);
2361
+ }
2362
+ catch { }
2363
+ agent.cancel({ kind: 'hook', reason: 'assistant-delivery-scheduled-goal-wake-cancelled' });
2364
+ }
2365
+ }
2366
+ catch { }
1857
2367
  }
2368
+ finally {
2369
+ input.signal.removeEventListener('abort', abort);
2370
+ clearTimeout(expire);
2371
+ try {
2372
+ const remaining = Math.max(0, input.deadlineAt - Date.now());
2373
+ disposed = await this.disposeAfterReplyBoundary(binding.sessionId, handle, releaseFences, !terminal, remaining);
2374
+ }
2375
+ finally {
2376
+ if (handle === undefined)
2377
+ releaseFences?.();
2378
+ if (control !== undefined)
2379
+ this.endSessionControl(binding.sessionId, control);
2380
+ lease?.close();
2381
+ }
2382
+ }
2383
+ const quiescent = terminal && disposed && !signal.aborted;
2384
+ return { outcome: quiescent ? 'succeeded' : 'unknown', dispatched, quiescent,
2385
+ ...(quiescent && goalOutput !== undefined ? { output: goalOutput } : {}) };
2386
+ }
2387
+ validGoalWakeInput(binding, input, now) {
2388
+ if (input === null || typeof input !== 'object' || input.signal === undefined
2389
+ || typeof input.signal.aborted !== 'boolean' || typeof input.signal.addEventListener !== 'function'
2390
+ || typeof input.signal.removeEventListener !== 'function' || input.signal.aborted
2391
+ || !isDeliveryGoalWakeDeadline(input.deadlineAt, now) || typeof input.assertCurrent !== 'function'
2392
+ || typeof input.beforeResume !== 'function' || typeof input.settle !== 'function'
2393
+ || typeof input.resolveOutcomeFeedbackTarget !== 'function'
2394
+ || (input.includeOutput !== undefined && typeof input.includeOutput !== 'boolean') || typeof input.native?.goalId !== 'string'
2395
+ || input.native.goalId.length === 0 || input.native.goalId.length > 512
2396
+ || !Number.isSafeInteger(input.native.revision) || input.native.revision < 1)
2397
+ return false;
2398
+ const attestation = input.attestation;
2399
+ return attestation !== null && typeof attestation === 'object'
2400
+ && attestation.bindingId === binding.id && attestation.bindingVersion === binding.version
2401
+ && attestation.bindingGeneration === binding.generation && attestation.sessionId === binding.sessionId
2402
+ && attestation.scope?.workspace === binding.workspace && attestation.scope.preset === binding.agentPreset
2403
+ && attestation.principalId === externalPrincipalId(binding.principal)
2404
+ && typeof attestation.principalLineage?.principalRecordId === 'string'
2405
+ && attestation.principalLineage.principalRecordId.length > 0
2406
+ && Number.isSafeInteger(attestation.principalLineage.principalVersion)
2407
+ && attestation.principalLineage.principalVersion >= 1;
2408
+ }
2409
+ assertScheduledGoal(input, agent, phase) {
2410
+ this.sessionLeases.assert(String(agent.session.id));
2411
+ input.signal.throwIfAborted();
2412
+ if (Date.now() >= input.deadlineAt)
2413
+ throw new Error('assistant-delivery: scheduled goal wake expired');
2414
+ input.assertCurrent(agent, phase);
2415
+ }
2416
+ installScheduledGoalFences(agent, input) {
2417
+ const goalTurn = () => {
2418
+ const events = agent.session.snapshotEvents();
2419
+ const start = events.findLast(event => event.type === 'turn/start');
2420
+ // DSH persists runtime context snapshots as user/message too; they are
2421
+ // context for this turn, never a second input or a grant of authority.
2422
+ const messages = start === undefined ? [] : events.filter(event => event.seq > start.seq && event.type === 'user/message'
2423
+ && !isScheduledGoalRuntimeContext(event.data.source));
2424
+ if (start === undefined || events.some(event => event.seq > start.seq && event.type === 'turn/end')
2425
+ || messages.length !== 1 || messages[0]?.type !== 'user/message')
2426
+ throw new Error('assistant-delivery: exact native goal turn required');
2427
+ const source = messages[0].data.source;
2428
+ const native = this.ctx.get('goals')?.get(agent);
2429
+ if (source.kind !== 'goal' || String(source.goalId) !== input.native.goalId
2430
+ || source.revision !== input.native.revision + 1 || source.round < 1
2431
+ || native === undefined || native.phase !== 'active' || String(native.id) !== input.native.goalId
2432
+ || native.revision !== source.revision || native.roundsStarted !== source.round)
2433
+ throw new Error('assistant-delivery: foreign wake input denied');
2434
+ };
2435
+ const reject = () => {
2436
+ try {
2437
+ agent.cancel({ kind: 'hook', reason: 'assistant-delivery-scheduled-goal-wake-revoked' });
2438
+ }
2439
+ catch { }
2440
+ throw new Error('assistant-delivery: scheduled goal wake authorization changed');
2441
+ };
2442
+ const request = agent.ctx.on('agent/request', async (_payload, next) => {
2443
+ try {
2444
+ goalTurn();
2445
+ this.assertScheduledGoal(input, agent, 'running');
2446
+ }
2447
+ catch {
2448
+ return reject();
2449
+ }
2450
+ const result = await next();
2451
+ try {
2452
+ goalTurn();
2453
+ this.assertScheduledGoal(input, agent, 'running');
2454
+ }
2455
+ catch {
2456
+ return reject();
2457
+ }
2458
+ return result;
2459
+ }, { prepend: true });
2460
+ const preExecute = agent.ctx.on('tools/pre-execute', async (_payload, next) => {
2461
+ try {
2462
+ goalTurn();
2463
+ this.assertScheduledGoal(input, agent, 'running');
2464
+ }
2465
+ catch {
2466
+ return { kind: 'deny', reason: 'assistant-delivery: scheduled goal wake authorization changed' };
2467
+ }
2468
+ const result = await next();
2469
+ try {
2470
+ goalTurn();
2471
+ this.assertScheduledGoal(input, agent, 'running');
2472
+ }
2473
+ catch {
2474
+ return { kind: 'deny', reason: 'assistant-delivery: scheduled goal wake authorization changed' };
2475
+ }
2476
+ return result;
2477
+ }, { prepend: true });
2478
+ const guard = agent.ctx.tools.guard(() => {
2479
+ try {
2480
+ goalTurn();
2481
+ this.assertScheduledGoal(input, agent, 'running');
2482
+ return undefined;
2483
+ }
2484
+ catch {
2485
+ return 'assistant-delivery: scheduled goal wake authorization changed';
2486
+ }
2487
+ });
2488
+ return () => { request(); preExecute(); guard(); };
2489
+ }
2490
+ waitForScheduledGoalTerminal(agent, input, signal) {
2491
+ return new Promise(resolve => {
2492
+ let settled = false;
2493
+ let verifying = false;
2494
+ const finish = (value) => {
2495
+ if (settled)
2496
+ return;
2497
+ settled = true;
2498
+ removeGoal();
2499
+ removeStatus();
2500
+ signal.removeEventListener('abort', aborted);
2501
+ resolve(value);
2502
+ };
2503
+ const check = () => {
2504
+ if (settled || verifying)
2505
+ return;
2506
+ try {
2507
+ this.sessionLeases.assert(String(agent.session.id));
2508
+ input.signal.throwIfAborted();
2509
+ if (Date.now() >= input.deadlineAt)
2510
+ throw new Error('assistant-delivery: scheduled goal wake expired');
2511
+ const goals = this.ctx.get('goals');
2512
+ const current = goals?.get(agent);
2513
+ if (current === undefined || String(current.id) !== input.native.goalId)
2514
+ return finish(false);
2515
+ if (current.phase === 'active') {
2516
+ input.assertCurrent(agent, 'running');
2517
+ return;
2518
+ }
2519
+ if (agent.status !== 'idle' || agent.inbox.hasPending)
2520
+ return;
2521
+ input.assertCurrent(agent, 'terminal');
2522
+ // Native idle/round-limit can precede the asynchronous step and
2523
+ // whole-goal verifier. Keep the Agent and its lease alive until that
2524
+ // exact terminal round settles; cancellation still ends this wait.
2525
+ verifying = true;
2526
+ void Promise.resolve().then(() => input.settle(agent, signal)).then(() => {
2527
+ if (settled)
2528
+ return;
2529
+ signal.throwIfAborted();
2530
+ this.assertScheduledGoal(input, agent, 'terminal');
2531
+ if (agent.status !== 'idle' || agent.inbox.hasPending)
2532
+ throw new Error('assistant-delivery: goal changed during terminal settlement');
2533
+ finish(true);
2534
+ }).catch(() => {
2535
+ if (settled)
2536
+ return;
2537
+ try {
2538
+ agent.cancel({ kind: 'hook', reason: 'assistant-delivery-scheduled-goal-settlement-failed' });
2539
+ }
2540
+ catch { }
2541
+ finish(false);
2542
+ });
2543
+ }
2544
+ catch {
2545
+ try {
2546
+ agent.cancel({ kind: 'hook', reason: 'assistant-delivery-scheduled-goal-wake-revoked' });
2547
+ }
2548
+ catch { }
2549
+ finish(false);
2550
+ }
2551
+ };
2552
+ const removeGoal = this.ctx.on('goal/changed', ({ agent: changed }) => { if (changed === agent)
2553
+ check(); });
2554
+ const removeStatus = this.ctx.on('agent/status', ({ agent: changed }) => { if (changed === agent)
2555
+ check(); });
2556
+ const aborted = () => finish(false);
2557
+ signal.addEventListener('abort', aborted, { once: true });
2558
+ if (signal.aborted)
2559
+ aborted();
2560
+ else
2561
+ check();
2562
+ });
1858
2563
  }
1859
2564
  async prepare(binding, envelope, signal, context) {
1860
2565
  const imageCount = (envelope.attachments ?? [])
@@ -1954,9 +2659,13 @@ export class DshDeliveryRuntime {
1954
2659
  signal.throwIfAborted();
1955
2660
  markDispatching();
1956
2661
  signal.throwIfAborted();
1957
- if (parsed.kind === 'objective') {
1958
- const objective = await this.options.dispatchObjectiveFeedback(binding, envelope, parsed.objectiveStatus);
2662
+ if (parsed.kind === 'objective' || parsed.kind === 'objective-revision' || parsed.kind === 'objective-status') {
2663
+ const objective = await this.options.dispatchObjectiveFeedback(binding, envelope, parsed.kind === 'objective' ? parsed.objectiveStatus : parsed);
1959
2664
  signal.throwIfAborted();
2665
+ if (typeof objective === 'object') {
2666
+ this.options.replyCommand(binding, envelope.eventId, { text: (parsed.kind === 'objective-status' ? '' : `已把该次任务结果记录为 ${objective.objectiveStatus};同一次任务只计一票。\n`) + objectiveStateMessage(objective), format: 'plain' });
2667
+ return { outcome: 'processed' };
2668
+ }
1960
2669
  if (objective === 'unknown') {
1961
2670
  return {
1962
2671
  outcome: 'not-processed',
@@ -1980,13 +2689,13 @@ export class DshDeliveryRuntime {
1980
2689
  }
1981
2690
  if (objective === 'conflict') {
1982
2691
  this.options.replyCommand(binding, envelope.eventId, {
1983
- text: '该次任务已经记录了不同的任务结果;为避免重复计票,本次未覆盖原记录。',
2692
+ text: '该次任务已经记录了不同的任务结果或反馈版本已变化,本次未覆盖。请回复原任务结果发送 /feedback status,使用当前版本更正或撤回。',
1984
2693
  format: 'plain',
1985
2694
  });
1986
2695
  return { outcome: 'processed' };
1987
2696
  }
1988
2697
  this.options.replyCommand(binding, envelope.eventId, {
1989
- text: `已把该次任务结果记录为 ${parsed.objectiveStatus};同一次任务只计一票。`,
2698
+ text: parsed.kind === 'objective-status' ? '尚未记录任务反馈,请回复原任务结果发送 /feedback achieved、partial 或 not-achieved。' : `已把该次任务结果记录为 ${parsed.objectiveStatus};同一次任务只计一票。回复原结果发送 /feedback status 可更正或撤回。`,
1990
2699
  format: 'plain',
1991
2700
  });
1992
2701
  return { outcome: 'processed' };
@@ -2271,7 +2980,7 @@ export class DshDeliveryRuntime {
2271
2980
  ? binding.agentPreset
2272
2981
  : (await agentPresets.resolve(binding.agentPreset)).id;
2273
2982
  await requireWorkspace(binding.workspace);
2274
- handle = await agents.resume({
2983
+ handle = await this.resumeAgent({
2275
2984
  resumeSessionId: SessionId(binding.sessionId),
2276
2985
  signal,
2277
2986
  agentOptions: {
@@ -2299,7 +3008,7 @@ export class DshDeliveryRuntime {
2299
3008
  }
2300
3009
  if (command.name === 'status' || command.name === 'session') {
2301
3010
  const messages = agent.session.deriveMessages().length;
2302
- const turns = agent.session.events.filter(event => event.type === 'turn/end').length;
3011
+ const turns = agent.session.snapshotEvents().filter(event => event.type === 'turn/end').length;
2303
3012
  return this.replySessionCommand(binding, envelope, {
2304
3013
  text: [
2305
3014
  '当前会话',
@@ -2433,12 +3142,41 @@ export class DshDeliveryRuntime {
2433
3142
  format: 'plain',
2434
3143
  }, outerSignal, markDispatching);
2435
3144
  }
2436
- const control = this.beginSessionControl(binding.sessionId, outerSignal);
3145
+ let lease;
3146
+ try {
3147
+ lease = this.sessionLeases.open({ kind: 'bound', binding }, outerSignal);
3148
+ }
3149
+ catch (error) {
3150
+ if (error instanceof SessionLeaseUnavailable) {
3151
+ if (error.kind === 'unknown' && !outerSignal.aborted) {
3152
+ try {
3153
+ if (this.options.isInboundAuthorized(binding, envelope))
3154
+ this.options.replyCommand(binding, envelope.eventId, {
3155
+ text: '上一次执行是否已经停止还无法确认,因此暂时无法继续这个会话。请先核对外部操作;需要独立开始新任务可发送 /new,旧会话会保留。',
3156
+ format: 'plain',
3157
+ });
3158
+ }
3159
+ catch { /* Keep the exact unknown execution diagnostic even if its notice is unavailable. */ }
3160
+ }
3161
+ return {
3162
+ outcome: 'not-processed', failureCode: `session-lease-${error.kind}`,
3163
+ retryable: error.kind === 'busy', retryAfterMs: this.sessionLeases.leaseMs,
3164
+ };
3165
+ }
3166
+ throw error;
3167
+ }
3168
+ let control;
2437
3169
  try {
2438
- return await this.processControlled(binding, envelope, control.controller.signal, sessionCommand, prepared, markDispatching);
3170
+ control = this.beginSessionControl(binding.sessionId, lease.signal);
3171
+ lease.dispatch();
3172
+ return await this.processControlled(binding, envelope, control.controller.signal, sessionCommand, prepared, markDispatching === undefined ? undefined : () => {
3173
+ lease.assert();
3174
+ markDispatching();
3175
+ lease.assert();
3176
+ });
2439
3177
  }
2440
3178
  catch (error) {
2441
- if (control.command !== undefined && causedByUserCancellation(error)) {
3179
+ if (control?.command !== undefined && causedByUserCancellation(error)) {
2442
3180
  return permissionCommand(sessionCommand) === undefined
2443
3181
  ? { outcome: 'processed' }
2444
3182
  : {
@@ -2450,7 +3188,9 @@ export class DshDeliveryRuntime {
2450
3188
  throw error;
2451
3189
  }
2452
3190
  finally {
2453
- this.endSessionControl(binding.sessionId, control);
3191
+ if (control !== undefined)
3192
+ this.endSessionControl(binding.sessionId, control);
3193
+ lease.close();
2454
3194
  }
2455
3195
  }
2456
3196
  async processControlled(binding, envelope, signal, sessionCommand, prepared, markDispatching) {
@@ -2560,6 +3300,10 @@ export class DshDeliveryRuntime {
2560
3300
  const selected = agentSelection(selectedRoute);
2561
3301
  let handle;
2562
3302
  let dispatched = false;
3303
+ let acceptance;
3304
+ let acceptanceSucceeded = false;
3305
+ let goalContinuation;
3306
+ let goalContinuationQuiescent = true;
2563
3307
  let removeAbort;
2564
3308
  let removeProgress;
2565
3309
  let progressOpen = true;
@@ -2582,7 +3326,7 @@ export class DshDeliveryRuntime {
2582
3326
  ? binding.agentPreset
2583
3327
  : (await agentPresets.resolve(binding.agentPreset)).id;
2584
3328
  await requireWorkspace(binding.workspace);
2585
- handle = await agents.resume({ resumeSessionId: SessionId(binding.sessionId), signal,
3329
+ handle = await this.resumeAgent({ resumeSessionId: SessionId(binding.sessionId), signal,
2586
3330
  agentOptions: { provider: selected.provider, model: selected.model,
2587
3331
  maxTokens: this.options.maxOutputTokens },
2588
3332
  setup: async (agentCtx) => {
@@ -2607,7 +3351,7 @@ export class DshDeliveryRuntime {
2607
3351
  if (authorizationFailure !== undefined)
2608
3352
  return authorizationFailure;
2609
3353
  }
2610
- let turnFrom = agent.session.events.length;
3354
+ let turnFrom = agent.session.snapshotEvents().length;
2611
3355
  const automaticCompletionTurns = new Map();
2612
3356
  const automaticCompletionToolViolations = new Set();
2613
3357
  removeProgress = this.ctx.on('session/event', (session, event) => {
@@ -2649,6 +3393,11 @@ export class DshDeliveryRuntime {
2649
3393
  const authorizationFailure = preDispatchAuthorizationFailure();
2650
3394
  if (authorizationFailure !== undefined)
2651
3395
  return authorizationFailure;
3396
+ // Mount the native-goal fences before the foreground message is queued:
3397
+ // an owner tool can create/resume a goal in that very first turn and the
3398
+ // host driver is otherwise free to claim its first round at idle.
3399
+ goalContinuation = this.waitForNativeGoalContinuation(binding, envelope, agent, signal);
3400
+ acceptance = this.options.prepareForegroundTaskAcceptance(binding, envelope);
2652
3401
  markDispatching();
2653
3402
  // Once the durable marker exists, even a synchronous followup failure is ambiguous:
2654
3403
  // implementations may enqueue before throwing, so no retry is safe.
@@ -2663,7 +3412,7 @@ export class DshDeliveryRuntime {
2663
3412
  let assembledText = '';
2664
3413
  let bestEffortText = '';
2665
3414
  while (true) {
2666
- const output = finalAssistant(agent.session.events, turnFrom, deliveryTurn);
3415
+ const output = finalAssistant(agent.session.snapshotEvents(), turnFrom, deliveryTurn);
2667
3416
  let addedText = false;
2668
3417
  if (output.text.trim() !== '') {
2669
3418
  if (completionKind === 'compact' || completionKind === 'regenerate') {
@@ -2714,7 +3463,7 @@ export class DshDeliveryRuntime {
2714
3463
  ? 'compact'
2715
3464
  : 'continue';
2716
3465
  automaticTurns += 1;
2717
- turnFrom = agent.session.events.length;
3466
+ turnFrom = agent.session.snapshotEvents().length;
2718
3467
  const completionMessage = createUserMessage({
2719
3468
  content: [{
2720
3469
  type: 'text',
@@ -2828,13 +3577,20 @@ export class DshDeliveryRuntime {
2828
3577
  break;
2829
3578
  }
2830
3579
  signal.throwIfAborted();
2831
- // Agent answers are authored as Markdown (tables, bold, inline code), so request Markdown
2832
- // rendering; sending them as plain text shows the raw `|---|` and `**` syntax to the user.
3580
+ // The foreground reply has already crossed its durable reply path.
3581
+ // Retain the same Agent afterwards only for the native goal driver; its
3582
+ // unknown result must never turn the original reply into a claim that
3583
+ // the business goal was achieved.
2833
3584
  const learning = await this.options.replyCompletedPreferenceTurn(agent, binding, envelope, { text: assembledText, format: 'markdown' });
2834
3585
  if (learning === 'unknown') {
2835
3586
  this.ctx.logger.warn('assistant-delivery: completed-turn preference projection is ambiguous');
2836
3587
  }
3588
+ if (goalContinuation !== undefined) {
3589
+ goalContinuationQuiescent = await goalContinuation.settle();
3590
+ }
3591
+ signal.throwIfAborted();
2837
3592
  publishProgress({ kind: 'completed' });
3593
+ acceptanceSucceeded = goalContinuationQuiescent;
2838
3594
  return { outcome: 'processed' };
2839
3595
  }
2840
3596
  catch (error) {
@@ -2878,7 +3634,24 @@ export class DshDeliveryRuntime {
2878
3634
  removeAbort?.();
2879
3635
  removeProgress?.();
2880
3636
  progressOpen = false;
2881
- await this.disposeAfterReplyBoundary(binding.sessionId, handle);
3637
+ let disposed = false;
3638
+ try {
3639
+ disposed = await this.disposeAfterReplyBoundary(binding.sessionId, handle, () => goalContinuation?.dispose(), !goalContinuationQuiescent, goalContinuation?.teardownTimeoutMs());
3640
+ }
3641
+ finally {
3642
+ acceptanceSucceeded = acceptanceSucceeded && disposed && !signal.aborted
3643
+ && (goalContinuation?.isQuiescent() ?? true);
3644
+ if (acceptance !== undefined) {
3645
+ try {
3646
+ await this.options.completeForegroundTaskAcceptance(acceptance, {
3647
+ status: acceptanceSucceeded ? 'succeeded' : 'unknown', quiescent: acceptanceSucceeded,
3648
+ });
3649
+ }
3650
+ catch (error) {
3651
+ this.ctx.logger.warn(`assistant-delivery: acceptance completion failed: ${String(error)}`);
3652
+ }
3653
+ }
3654
+ }
2882
3655
  }
2883
3656
  }
2884
3657
  }