@dsh-enhanced/assistant-skills 0.1.32 → 0.1.35

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 (58) hide show
  1. package/README.md +28 -4
  2. package/lib/definition.d.ts +53 -2
  3. package/lib/definition.d.ts.map +1 -1
  4. package/lib/definition.js +96 -0
  5. package/lib/definition.js.map +1 -1
  6. package/lib/delegated-arm.d.ts +47 -0
  7. package/lib/delegated-arm.d.ts.map +1 -0
  8. package/lib/delegated-arm.js +26 -0
  9. package/lib/delegated-arm.js.map +1 -0
  10. package/lib/external-holdout.js +1 -1
  11. package/lib/external-holdout.js.map +1 -1
  12. package/lib/holdout-authority.d.ts +1 -122
  13. package/lib/holdout-authority.d.ts.map +1 -1
  14. package/lib/holdout-authority.js +5 -262
  15. package/lib/holdout-authority.js.map +1 -1
  16. package/lib/holdout-cli.d.ts.map +1 -1
  17. package/lib/holdout-cli.js +2 -0
  18. package/lib/holdout-cli.js.map +1 -1
  19. package/lib/holdout-qualification.d.ts +11 -3
  20. package/lib/holdout-qualification.d.ts.map +1 -1
  21. package/lib/holdout-qualification.js +19 -9
  22. package/lib/holdout-qualification.js.map +1 -1
  23. package/lib/index.d.ts +10 -1
  24. package/lib/index.d.ts.map +1 -1
  25. package/lib/index.js +4 -0
  26. package/lib/index.js.map +1 -1
  27. package/lib/prospective-holdout.d.ts +1 -34
  28. package/lib/prospective-holdout.d.ts.map +1 -1
  29. package/lib/prospective-holdout.js +4 -253
  30. package/lib/prospective-holdout.js.map +1 -1
  31. package/lib/repair-agent.d.ts +1 -0
  32. package/lib/repair-agent.d.ts.map +1 -1
  33. package/lib/repair-agent.js +15 -4
  34. package/lib/repair-agent.js.map +1 -1
  35. package/lib/repair-profile.d.ts +6 -2
  36. package/lib/repair-profile.d.ts.map +1 -1
  37. package/lib/repair-profile.js +10 -4
  38. package/lib/repair-profile.js.map +1 -1
  39. package/lib/repair-workspace-path.d.ts +17 -0
  40. package/lib/repair-workspace-path.d.ts.map +1 -0
  41. package/lib/repair-workspace-path.js +84 -0
  42. package/lib/repair-workspace-path.js.map +1 -0
  43. package/lib/replay.d.ts.map +1 -1
  44. package/lib/replay.js +17 -2
  45. package/lib/replay.js.map +1 -1
  46. package/lib/service.d.ts +269 -1
  47. package/lib/service.d.ts.map +1 -1
  48. package/lib/service.js +438 -19
  49. package/lib/service.js.map +1 -1
  50. package/lib/store.d.ts +26 -0
  51. package/lib/store.d.ts.map +1 -1
  52. package/lib/store.js +80 -6
  53. package/lib/store.js.map +1 -1
  54. package/lib/version.d.ts +1 -1
  55. package/lib/version.js +1 -1
  56. package/lib/watch-proof.js +2 -2
  57. package/lib/watch-proof.js.map +1 -1
  58. package/package.json +8 -8
package/lib/service.js CHANGED
@@ -5,7 +5,7 @@ import { acceptanceDigest } from '@dsh-enhanced/task-acceptance-contract';
5
5
  import Schema from '@deepseek-ai/schemastery';
6
6
  import { homedir } from 'node:os';
7
7
  import { join } from 'node:path';
8
- import { createDefinition, fileObservationSteps, instantiate } from './definition.js';
8
+ import { compensationDirective, createDefinition, evaluatePreconditions, fileObservationSteps, instantiate } from './definition.js';
9
9
  import { captureFailureCandidateProvenance, captureRunExpansions } from './capture-expansion.js';
10
10
  import { validateComparisonProfiles, SkillComparator } from './comparison.js';
11
11
  import { watchBindingCurrent, watchObservation, watchObservationResult, watchObservationRevision } from './watch-proof.js';
@@ -18,6 +18,7 @@ import { RepairContinuationRuntime } from './repair-runtime.js';
18
18
  import { OwnerRepairAgentRuntime } from './repair-agent.js';
19
19
  import { enqueueRepairFeedback } from './repair-feedback.js';
20
20
  import { SkillStore } from './store.js';
21
+ import { awaitBenchmarkSignal, benchmarkArmDefinition, sameBenchmarkArm } from './delegated-arm.js';
21
22
  export const Config = Schema.object({
22
23
  databasePath: Schema.string().default(join(homedir(), '.dsh', 'assistant-skills.sqlite')),
23
24
  allowedTools: Schema.array(Schema.string()).default(['read', 'write', 'edit']),
@@ -53,6 +54,14 @@ function exactArguments(value, allowed) {
53
54
  return undefined;
54
55
  return args;
55
56
  }
57
+ function repairModelRoute(value) {
58
+ if (!value || typeof value !== 'object' || Array.isArray(value))
59
+ return false;
60
+ const route = value;
61
+ return typeof route.provider === 'string' && route.provider.length > 0 && route.provider.length <= 256 && !route.provider.includes('\0')
62
+ && typeof route.model === 'string' && route.model.length > 0 && route.model.length <= 256 && !route.model.includes('\0')
63
+ && (route.reasoningEffort === undefined || typeof route.reasoningEffort === 'string' && route.reasoningEffort.length > 0 && route.reasoningEffort.length <= 256 && !route.reasoningEffort.includes('\0'));
64
+ }
56
65
  function failureLocator(value) {
57
66
  const locator = plainArguments(value);
58
67
  return locator !== undefined && Object.keys(locator).length === 2 && Object.hasOwn(locator, 'sessionId') && Object.hasOwn(locator, 'goalId')
@@ -91,6 +100,37 @@ function failureWindow(input) {
91
100
  .sort((left, right) => compare(left.sessionId, right.sessionId) || compare(left.goalId, right.goalId));
92
101
  return { failures: Object.freeze(canonical.map(locator => Object.freeze(locator))), minimumOccurrences: minimumOccurrences };
93
102
  }
103
+ /**
104
+ * Bounded window of model-suggested owner-root success locators for one
105
+ * autonomous growth deposit. The locator shape ({sessionId,goalId}) is
106
+ * identical to a failure locator; only its meaning differs. Discovery only —
107
+ * every locator still has to pass an independent owner verified source read.
108
+ */
109
+ function successWindow(input) {
110
+ const raw = input.successLocators;
111
+ const minimumOccurrences = input.minimumOccurrences ?? 1;
112
+ if (!Array.isArray(raw) || Object.getPrototypeOf(raw) !== Array.prototype || raw.length < 1 || raw.length > 32
113
+ || Object.getOwnPropertySymbols(raw).length !== 0 || Reflect.ownKeys(raw).length !== raw.length + 1
114
+ || !Number.isSafeInteger(minimumOccurrences) || minimumOccurrences < 1 || minimumOccurrences > 32
115
+ || minimumOccurrences > raw.length) {
116
+ throw new Error('assistant-skills: invalid bounded success locator window');
117
+ }
118
+ const descriptors = Object.getOwnPropertyDescriptors(raw), successes = [];
119
+ for (let index = 0; index < raw.length; index++) {
120
+ const descriptor = descriptors[String(index)];
121
+ if (descriptor === undefined || !descriptor.enumerable || !('value' in descriptor) || !failureLocator(descriptor.value)) {
122
+ throw new Error('assistant-skills: invalid bounded success locator window');
123
+ }
124
+ successes.push(descriptor.value);
125
+ }
126
+ const identities = successes.map(locator => acceptanceDigest(locator));
127
+ if (new Set(identities).size !== identities.length)
128
+ throw new Error('assistant-skills: duplicate success locator');
129
+ const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
130
+ const canonical = successes.map(locator => ({ sessionId: locator.sessionId, goalId: locator.goalId }))
131
+ .sort((left, right) => compare(left.sessionId, right.sessionId) || compare(left.goalId, right.goalId));
132
+ return { successes: Object.freeze(canonical.map(locator => Object.freeze(locator))), minimumOccurrences: minimumOccurrences };
133
+ }
94
134
  function publicDefinition(definition) {
95
135
  const source = { digest: acceptanceDigest(definition.source), goalDefinitionDigest: definition.source.goal.definition.digest, stepCount: definition.source.steps.length,
96
136
  failedObservationCount: definition.source.failedObservations?.length ?? 0, segmentCount: definition.source.segments?.length ?? 0 };
@@ -188,6 +228,17 @@ function trialProofSteps(steps, candidateId, trialRunId, goalId, invocationId, i
188
228
  /** Fixed tool compositions run in the original native Goal; no new AgentLoop or scheduler. */
189
229
  export class AssistantSkillsService extends Service {
190
230
  static Config = Config;
231
+ static { Object.defineProperty(this, 'name', { value: 'dsh-enhanced-assistant-skills' }); }
232
+ static #benchmarkCapabilities = new WeakMap();
233
+ static #benchmarkRecipients = new WeakMap();
234
+ #benchmarkIdentity = Object.freeze({});
235
+ /** A Host identity only; it carries no permission and survives fresh Cordis trace proxies. */
236
+ benchmarkRecipientIdentity = () => this.#benchmarkIdentity;
237
+ #issuedBenchmarkArms = new Set();
238
+ #issuedBenchmarkCells = new Set();
239
+ #mountedBenchmarkArms = new Map();
240
+ #benchmarkRuns = new Set();
241
+ #benchmarkGeneration = 0;
191
242
  #store;
192
243
  #allowed;
193
244
  #duration;
@@ -226,18 +277,37 @@ export class AssistantSkillsService extends Service {
226
277
  this.#externalHoldouts = validateExternalHoldoutProfiles(config.externalHoldouts ?? []);
227
278
  this.#repairProfiles = validateRepairProfiles(config.repairProfiles ?? [], this.#externalHoldouts);
228
279
  this.#store = new SkillStore(config.databasePath ?? join(homedir(), '.dsh', 'assistant-skills.sqlite'));
229
- ctx.effect(() => async () => { this.#active = false; this.#lifecycle.abort(); await Promise.all([...this.#repairStops].map(stop => stop())); await Promise.allSettled(this.#comparing); await Promise.allSettled(this.#captureTasks); await Promise.allSettled([...this.#comparators.values()].map(async (value) => (await value).close())); this.#store.close(); }, 'assistant-skills.store');
280
+ AssistantSkillsService.#benchmarkRecipients.set(this.#benchmarkIdentity, this);
281
+ ctx.effect(() => async () => { this.#active = false; this.#lifecycle.abort(); this.#mountedBenchmarkArms.clear(); await Promise.allSettled(this.#benchmarkRuns); await Promise.all([...this.#repairStops].map(stop => stop())); await Promise.allSettled(this.#comparing); await Promise.allSettled(this.#captureTasks); await Promise.allSettled([...this.#comparators.values()].map(async (value) => (await value).close())); this.#store.close(); }, 'assistant-skills.store');
282
+ ctx.inject(['assistantGoals', 'assistantPolicy', 'assistantDelivery'], runtime => {
283
+ this.#benchmarkGeneration++;
284
+ runtime.effect(() => () => {
285
+ this.#benchmarkGeneration++;
286
+ for (const controller of this.#issuedBenchmarkArms)
287
+ controller.abort();
288
+ this.#issuedBenchmarkArms.clear();
289
+ for (const arm of this.#mountedBenchmarkArms.values())
290
+ arm.revoke();
291
+ this.#mountedBenchmarkArms.clear();
292
+ }, 'assistant-skills.benchmark-service-generation');
293
+ });
230
294
  if (this.#repairProfiles.length > 0)
231
295
  this.#installRepairRuntime(ctx);
232
296
  ctx.inject(['tools', 'agents', 'assistantGoals', 'assistantPolicy', 'assistantDelivery'], runtime => {
233
297
  runtime.tools.register(defineTool({ name: 'skill_save', description: 'Save the exact successful tool trace of this owner session’s independently achieved Goal as a private versioned skill. Requires the current human request. Historical acceptance is provenance, never permission or acceptance for a future run.',
234
298
  parameters: { goal_id: { type: 'string', required: true, description: businessGoalId }, name: { type: 'string', required: true }, description: { type: 'string', required: true }, bindings_json: { type: 'string', description: 'JSON array of {name,stepId,path}; path is a scalar argument JSON pointer. Empty array keeps the original arguments.' }, expected_version: { type: 'integer' } }, output,
235
299
  execute: async (args, exec) => ({ context: JSON.stringify(this.save(exec.agent, args.goal_id, { name: args.name, description: args.description, bindings: parse(args.bindings_json ?? '[]', true) }, args.expected_version ?? 0)) }) }));
236
- runtime.tools.register(defineTool({ name: 'skill_run', description: 'Replay a saved skill’s fixed tool steps in a fresh native Goal. If the current owner Goal is active but has no admitted native round, this returns awaiting-native-round with no steps or durable run, ends the turn, and the next native round must repeat the same invocation_id. Every nested call retains native permissions, approvals, cancellation and budgets. Tool success still requires fresh independent Goal acceptance.',
300
+ const delegatedRun = defineTool({ name: 'skill_run', description: 'Replay a saved skill’s fixed tool steps in a fresh native Goal. If the current owner Goal is active but has no admitted native round, this returns awaiting-native-round with no steps or durable run, ends the turn, and the next native round must repeat the same invocation_id. Every nested call retains native permissions, approvals, cancellation and budgets. Tool success still requires fresh independent Goal acceptance.',
237
301
  parameters: { goal_id: { type: 'string', required: true, description: businessGoalId }, name: { type: 'string', required: true }, version: { type: 'integer', required: true }, inputs_json: { type: 'string', description: 'JSON object with only declared typed input parameters.' }, invocation_id: { type: 'string', required: true } }, output,
238
- execute: async (args, exec) => ({ context: JSON.stringify(await this.run(exec, args.goal_id, args.name, args.version, parse(args.inputs_json ?? '{}'), args.invocation_id)) }) }));
239
- runtime.tools.register(defineTool({ name: 'skill_status', description: 'Read this owner’s active saved skill definitions, typed inputs and source acceptance, or inspect a specific durable invocation. Success means steps executed, not Goal achievement.', parameters: { run_id: { type: 'string' } }, output,
240
- execute: async (args, exec) => ({ context: JSON.stringify(this.inspect(exec.agent, args.run_id)) }) }));
302
+ execute: async (args, exec) => ({ context: JSON.stringify(await this.run(exec, args.goal_id, args.name, args.version, parse(args.inputs_json ?? '{}'), args.invocation_id)) }) });
303
+ runtime.tools.register(delegatedRun);
304
+ if (runtime.assistantPolicy.delegatedSkillToolPreauthorizationVersion?.() === 1)
305
+ runtime.assistantPolicy.registerPreauthorizedTool(runtime, delegatedRun, execution => this.#preauthorizeBenchmarkTool(execution));
306
+ const delegatedStatus = defineTool({ name: 'skill_status', description: 'Read this owner’s active saved skill definitions, typed inputs and source acceptance, or inspect a specific durable invocation. Success means steps executed, not Goal achievement.', parameters: { run_id: { type: 'string' } }, output,
307
+ execute: async (args, exec) => ({ context: JSON.stringify(this.inspect(exec.agent, args.run_id)) }) });
308
+ runtime.tools.register(delegatedStatus);
309
+ if (runtime.assistantPolicy.delegatedSkillToolPreauthorizationVersion?.() === 1)
310
+ runtime.assistantPolicy.registerPreauthorizedTool(runtime, delegatedStatus, execution => this.#preauthorizeBenchmarkTool(execution));
241
311
  runtime.tools.register(defineTool({ name: 'skill_retire', description: 'Retire the current saved skill version following the current authenticated owner request. Pending steps recheck retirement; past effects remain and require explicit repair.', parameters: { name: { type: 'string', required: true }, expected_version: { type: 'integer', required: true } }, output,
242
312
  execute: async (args, exec) => ({ context: JSON.stringify(this.retire(exec.agent, args.name, args.expected_version)) }) }));
243
313
  });
@@ -515,9 +585,39 @@ export class AssistantSkillsService extends Service {
515
585
  #capturedDefinition(source, options, scope) {
516
586
  return createDefinition(source, options, this.#allowed, captureRunExpansions(source, scope, this.#store));
517
587
  }
588
+ /** Only a live delegated, isolation-only workflow may cross Isolation's tool gate. */
589
+ #preauthorizeBenchmarkTool(execution) {
590
+ try {
591
+ if (execution.signal.aborted || !['skill_run', 'skill_status'].includes(execution.name))
592
+ return false;
593
+ const scope = this.#scope(execution.agent, execution.name === 'skill_run' ? 'run' : 'inspect');
594
+ const mounted = this.#mountedBenchmarkArms.get(acceptanceDigest(scope));
595
+ if (!mounted || mounted.skill.steps.some(step => step.toolName !== 'isolation_run'))
596
+ return false;
597
+ mounted.assertCurrent();
598
+ const args = plainArguments(execution.arguments);
599
+ if (!args)
600
+ return false;
601
+ if (execution.name === 'skill_status')
602
+ return Object.keys(args).every(key => key === 'run_id') && (args.run_id === undefined
603
+ || typeof args.run_id === 'string' && this.#store.getRun(scope, args.run_id)?.delegationDigest === mounted.bindingDigest);
604
+ return Object.keys(args).length === 5 && typeof args.goal_id === 'string' && typeof args.inputs_json === 'string' && typeof args.invocation_id === 'string'
605
+ && args.name === mounted.skill.name && args.version === mounted.skill.version;
606
+ }
607
+ catch {
608
+ return false;
609
+ }
610
+ }
518
611
  inspect(agent, runId) {
519
612
  const scope = this.#scope(agent, 'inspect');
520
- return runId ? this.#store.getRun(scope, runId) : this.#store.list(scope);
613
+ if (runId)
614
+ return this.#store.getRun(scope, runId);
615
+ const mounted = this.#mountedBenchmarkArms.get(acceptanceDigest(scope));
616
+ if (mounted) {
617
+ mounted.assertCurrent();
618
+ return [{ ...structuredClone(mounted.skill), delegation: structuredClone(mounted.binding), delegationDigest: mounted.bindingDigest }];
619
+ }
620
+ return this.#store.list(scope);
521
621
  }
522
622
  retire(agent, name, expectedVersion) {
523
623
  const scope = this.#scope(agent, 'retire');
@@ -938,6 +1038,107 @@ export class AssistantSkillsService extends Service {
938
1038
  stageOwnerAuthorizedFailureCandidate = (exec, input, authority) => {
939
1039
  return this.stageFailureCandidate(exec, input, authority);
940
1040
  };
1041
+ /**
1042
+ * Host-only autonomous SUCCESS deposit for the growth driver. Unlike the
1043
+ * repair/failure path it is deliberately NOT anchored to a durable repair
1044
+ * continuation: a successful goal creates none. Instead the caller supplies
1045
+ * a narrow, short-lived OwnerSuccessCandidateAuthority minted from a frozen
1046
+ * configured owner scope, and this method:
1047
+ * 1. validates the authority (live background agent, exact scope, unexpired,
1048
+ * caller assertCurrent lease) and re-anchors the authenticated owner route
1049
+ * through delivery.validateOwnerRoute — never trusting a model-supplied id;
1050
+ * 2. independently re-reads EVERY model-suggested locator through
1051
+ * goals.inspectOwnerVerifiedWorkflowSource (owner-root, succeeded,
1052
+ * quiescent, double-read), counting DISTINCT (sessionId,goalId);
1053
+ * 3. stages one PENDING candidate only when the distinct verified-success
1054
+ * count meets minimumOccurrences. It never writes a current version, so
1055
+ * skill_run cannot reach it; activation stays behind the existing owner
1056
+ * gates. A retired same-name skill or any version conflict fails closed.
1057
+ */
1058
+ async stageOwnerVerifiedSuccessCandidate(exec, rawInput, authority) {
1059
+ // --- authority + exact owner scope (no repair continuation is consulted) ---
1060
+ exec.signal.throwIfAborted();
1061
+ this.#lifecycle.signal.throwIfAborted();
1062
+ if (!this.#active || !exec.agent || this.ctx.get('agents')?.get(exec.agent.id) !== exec.agent
1063
+ || !Number.isSafeInteger(authority.expiresAt) || authority.expiresAt <= Date.now()
1064
+ || exec.agent.session.header.cwd !== authority.scope.workspace
1065
+ || exec.agent.session.header.agentPreset !== authority.scope.preset) {
1066
+ throw new Error('assistant-skills: growth success authority unavailable');
1067
+ }
1068
+ authority.assertCurrent();
1069
+ const scope = { principalId: authority.scope.principalId, principalRecordId: authority.scope.principalRecordId,
1070
+ principalVersion: authority.scope.principalVersion, workspace: authority.scope.workspace, preset: authority.scope.preset };
1071
+ const route = this.#watchRoute(scope, authority.ownerRouteId);
1072
+ if (rawInput === null || typeof rawInput !== 'object' || rawInput.ownerRouteId !== authority.ownerRouteId) {
1073
+ throw new Error('assistant-skills: growth success input does not match authority');
1074
+ }
1075
+ const window = successWindow(rawInput);
1076
+ const goalsApi = this.#goals();
1077
+ if (typeof goalsApi.inspectOwnerVerifiedWorkflowSource !== 'function') {
1078
+ throw new Error('assistant-skills: upgrade assistant-goals to use growth success deposit');
1079
+ }
1080
+ const signal = AbortSignal.any([exec.signal, this.#lifecycle.signal]);
1081
+ // --- independent Host re-read of every model-suggested locator ---
1082
+ const sources = await Promise.all(window.successes.map(async (locator) => {
1083
+ const source = await goalsApi.inspectOwnerVerifiedWorkflowSource({ ownerRouteId: authority.ownerRouteId, principalId: scope.principalId, workspace: scope.workspace, preset: scope.preset,
1084
+ sessionId: locator.sessionId, goalId: locator.goalId }, signal);
1085
+ // The attestation must be for THIS exact owner scope, not a sibling route.
1086
+ if (acceptanceDigest(source.scope) !== acceptanceDigest(scope)) {
1087
+ throw new Error('assistant-skills: verified success source owner mismatch');
1088
+ }
1089
+ return { locator, source };
1090
+ }));
1091
+ // Distinct owner-root successes: different native Session AND business goal.
1092
+ // The embedded source carries no cross-goal signature, so the repeat gate is
1093
+ // a count of independently verified locators, never a model claim.
1094
+ const distinct = new Map();
1095
+ for (const item of sources) {
1096
+ const sessionId = item.source.goal.sessionId;
1097
+ if (sessionId !== item.locator.sessionId || item.source.goal.id !== item.locator.goalId) {
1098
+ throw new Error('assistant-skills: verified success locator mismatch');
1099
+ }
1100
+ distinct.set(`${sessionId}${item.locator.goalId}`, { sessionId, goalId: item.locator.goalId, source: item.source });
1101
+ }
1102
+ if (distinct.size < window.minimumOccurrences) {
1103
+ throw new Error(`assistant-skills: only ${distinct.size} distinct verified successes; ${window.minimumOccurrences} required`);
1104
+ }
1105
+ // --- resolve the current parent; a retired name blocks any new deposit ---
1106
+ const options = { name: rawInput.name, description: rawInput.description, ...(rawInput.bindings === undefined ? {} : { bindings: rawInput.bindings }) };
1107
+ const activeParent = this.#store.get(scope, options.name);
1108
+ const parentVersion = activeParent?.version ?? 0;
1109
+ // Build the definition from ONE representative verified source; provenance
1110
+ // below binds the full distinct-success set so the repeat evidence is durable.
1111
+ const representative = [...distinct.values()].sort((left, right) => left.sessionId < right.sessionId ? -1 : left.sessionId > right.sessionId ? 1
1112
+ : left.goalId < right.goalId ? -1 : left.goalId > right.goalId ? 1 : 0)[0];
1113
+ const definition = this.#capturedDefinition(representative.source, options, scope);
1114
+ // --- re-validate authority/route right before the write ---
1115
+ authority.assertCurrent();
1116
+ this.#watchRoute(scope, authority.ownerRouteId, route);
1117
+ const provenance = Object.freeze({
1118
+ protocol: 'assistant-skills/growth-success-provenance/v1',
1119
+ ownerRouteId: authority.ownerRouteId,
1120
+ distinctSuccessCount: distinct.size,
1121
+ requiredCount: window.minimumOccurrences,
1122
+ successes: [...distinct.values()]
1123
+ .map(value => ({ sessionId: value.sessionId, goalId: value.goalId,
1124
+ sourceDigest: acceptanceDigest(value.source), goalDefinitionDigest: value.source.goal.definition.digest,
1125
+ runId: value.source.runId, acceptanceContractId: value.source.acceptance.contractId }))
1126
+ .sort((left, right) => left.sourceDigest < right.sourceDigest ? -1 : left.sourceDigest > right.sourceDigest ? 1 : 0),
1127
+ });
1128
+ this.#authorize(exec.agent, 'draft', [scope, definition, parentVersion, acceptanceDigest(provenance)]);
1129
+ // stageCandidate enforces retired/version conflict and pending idempotency.
1130
+ // The distinct-success set is not stored in the failure-provenance slot
1131
+ // (that shape is repair-specific); its digest is folded into trigger, which
1132
+ // is part of the durable candidate identity formula, making the repeat
1133
+ // evidence tamper-evident without a schema migration.
1134
+ const provenanceDigest = acceptanceDigest(provenance);
1135
+ const reason = `Host-verified autonomous growth deposit after ${distinct.size} independently owner-verified repeated successes.`;
1136
+ const trigger = `growth-success:${distinct.size}:${provenanceDigest}`;
1137
+ const candidate = this.#store.stageCandidate(scope, definition, {
1138
+ expectedVersion: parentVersion, reason, trigger, expiresAt: Date.now() + this.#candidateTtl,
1139
+ });
1140
+ return this.#preview(scope, candidate);
1141
+ }
941
1142
  #preview(scope, candidate) {
942
1143
  const parent = candidate.parentVersion ? this.#store.get(scope, candidate.definition.name, candidate.parentVersion) : undefined;
943
1144
  return publicCandidate(candidate, parent);
@@ -949,6 +1150,142 @@ export class AssistantSkillsService extends Service {
949
1150
  const candidate = this.#store.getCandidate(scope, id);
950
1151
  return candidate ? this.#preview(scope, candidate) : null;
951
1152
  }
1153
+ /**
1154
+ * Host-only read of the active current skills of a delivery-anchored owner
1155
+ * scope. The caller MUST first establish the scope through
1156
+ * delivery.validateOwnerRoute (exact owner route); this enumeration consumes
1157
+ * no live owner turn and exists for bounded background growth agents. It is
1158
+ * discovery-only and cannot write.
1159
+ */
1160
+ inspectOwnerActiveSkills(scope) {
1161
+ if (!this.#active)
1162
+ throw new Error('assistant-skills: inactive');
1163
+ return this.#store.list(scope).map(publicDefinition);
1164
+ }
1165
+ /** Host-only read of every skill candidate of a delivery-anchored owner scope. */
1166
+ inspectOwnerSkillCandidates(scope) {
1167
+ if (!this.#active)
1168
+ throw new Error('assistant-skills: inactive');
1169
+ return this.#store.listCandidates(scope).map(candidate => this.#preview(scope, candidate));
1170
+ }
1171
+ #benchmarkSource(selection) {
1172
+ if (!this.#active)
1173
+ throw new Error('assistant-skills: benchmark source inactive');
1174
+ this.#watchRoute(selection.scope, selection.ownerRouteId);
1175
+ const policy = this.ctx.get('assistantPolicy', false);
1176
+ if (policy?.evaluate({ ...this.#watchPolicy(selection.scope, 'watch'), action: 'compare' }).effect !== 'allow')
1177
+ throw new Error('assistant-skills: benchmark comparison policy denied');
1178
+ const candidate = selection.candidateId === undefined ? undefined : this.#pending(selection.scope, selection.candidateId);
1179
+ const selected = candidate?.definition ?? this.#store.get(selection.scope, selection.skillName);
1180
+ const version = candidate ? candidate.parentVersion + 1 : selected?.version;
1181
+ if (!selected || selected.name !== selection.skillName || version !== selection.version
1182
+ || acceptanceDigest(selected.source.scope) !== acceptanceDigest(selection.scope))
1183
+ throw new Error('assistant-skills: exact benchmark source required');
1184
+ const definition = benchmarkArmDefinition(selected);
1185
+ const expiresAt = Math.min(definition.source.acceptance.validUntil, candidate?.expiresAt ?? Number.MAX_SAFE_INTEGER);
1186
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now())
1187
+ throw new Error('assistant-skills: benchmark source expired');
1188
+ return { definition, definitionDigest: acceptanceDigest(definition), sourceDigest: acceptanceDigest(definition.source), version, expiresAt };
1189
+ }
1190
+ /** Re-read original Goals provenance before freezing an arm. This grants no execution authority. */
1191
+ inspectOwnerBenchmarkArm = async (selection, signal) => {
1192
+ selection = structuredClone(selection);
1193
+ const combined = AbortSignal.any([this.#lifecycle.signal, AbortSignal.timeout(10000), ...(signal ? [signal] : [])]);
1194
+ combined.throwIfAborted();
1195
+ const before = this.#benchmarkSource(selection), goals = this.#goals(), generation = this.#benchmarkGeneration;
1196
+ const route = this.#watchRoute(selection.scope, selection.ownerRouteId);
1197
+ const source = await awaitBenchmarkSignal(goals.inspectOwnerVerifiedWorkflowSource({ ownerRouteId: selection.ownerRouteId,
1198
+ principalId: selection.scope.principalId, workspace: selection.scope.workspace, preset: selection.scope.preset,
1199
+ sessionId: before.definition.source.goal.sessionId, goalId: before.definition.source.goal.id }, combined), combined);
1200
+ combined.throwIfAborted();
1201
+ this.#watchRoute(selection.scope, selection.ownerRouteId, route);
1202
+ if (this.#benchmarkGeneration !== generation || !sameBenchmarkArm(before, this.#benchmarkSource(selection))
1203
+ || acceptanceDigest(source) !== before.sourceDigest)
1204
+ throw new Error('assistant-skills: benchmark provenance changed');
1205
+ return structuredClone(before);
1206
+ };
1207
+ /** Host-only, single-use delegation to one exact live recipient service and frozen cell. */
1208
+ mintBenchmarkArm = async (input, signal) => {
1209
+ const recipient = AssistantSkillsService.#benchmarkRecipients.get(input.recipient.benchmarkRecipientIdentity());
1210
+ if (!recipient)
1211
+ throw new Error('assistant-skills: unrecognized benchmark recipient');
1212
+ const selection = structuredClone(input.selection), scope = structuredClone(input.recipientScope), binding = structuredClone(input.binding);
1213
+ const recipientOwnerRouteId = input.recipientOwnerRouteId;
1214
+ const snapshot = await this.inspectOwnerBenchmarkArm(selection, signal);
1215
+ const route = this.#watchRoute(selection.scope, selection.ownerRouteId);
1216
+ const recipientRoute = recipient.#watchRoute(scope, recipientOwnerRouteId);
1217
+ if (recipient.#benchmarkIdentity === this.#benchmarkIdentity || !recipient.#active || binding.protocol !== 'assistant-skills/delegated-arm/v1'
1218
+ || binding.definitionDigest !== snapshot.definitionDigest || binding.sourceDigest !== snapshot.sourceDigest
1219
+ || binding.recipientScopeDigest !== acceptanceDigest(scope) || binding.skillName !== snapshot.definition.name || binding.version !== snapshot.version
1220
+ || !Number.isSafeInteger(binding.expiresAt) || binding.expiresAt <= Date.now() || binding.expiresAt > snapshot.expiresAt
1221
+ || binding.expiresAt - Date.now() > 300000)
1222
+ throw new Error('assistant-skills: invalid benchmark delegation');
1223
+ if (!/^[a-f0-9]{64}$/u.test(binding.planDigest) || !binding.cellId || !binding.variantId)
1224
+ throw new Error('assistant-skills: invalid benchmark cell');
1225
+ const cellKey = acceptanceDigest([binding.planDigest, binding.cellId]);
1226
+ if (this.#issuedBenchmarkCells.has(cellKey))
1227
+ throw new Error('assistant-skills: benchmark cell already delegated');
1228
+ const authorization = { idempotencyKey: `skill-benchmark-${acceptanceDigest(binding)}` };
1229
+ if (this.ctx.get('assistantPolicy', false)?.authorize({ ...this.#watchPolicy(selection.scope, 'watch'), action: 'compare' }, authorization).effect !== 'allow'
1230
+ || recipient.ctx.get('assistantPolicy', false)?.authorize({ ...recipient.#watchPolicy(scope, 'watch'), action: 'compare' }, authorization).effect !== 'allow')
1231
+ throw new Error('assistant-skills: benchmark comparison authorization denied');
1232
+ const controller = new AbortController();
1233
+ const combined = AbortSignal.any([controller.signal, this.#lifecycle.signal, recipient.#lifecycle.signal, ...(signal ? [signal] : [])]);
1234
+ const assertCurrent = () => {
1235
+ combined.throwIfAborted();
1236
+ if (!recipient.#active || Date.now() >= binding.expiresAt || !sameBenchmarkArm(snapshot, this.#benchmarkSource(selection)))
1237
+ throw new Error('assistant-skills: benchmark delegation unavailable');
1238
+ this.#watchRoute(selection.scope, selection.ownerRouteId, route);
1239
+ recipient.#watchRoute(scope, recipientOwnerRouteId, recipientRoute);
1240
+ if (recipient.ctx.get('assistantPolicy', false)?.evaluate({ ...recipient.#watchPolicy(scope, 'watch'), action: 'compare' }).effect !== 'allow')
1241
+ throw new Error('assistant-skills: recipient comparison policy denied');
1242
+ };
1243
+ assertCurrent();
1244
+ const capability = Object.freeze({ protocol: 'assistant-skills/benchmark-capability/v1' });
1245
+ AssistantSkillsService.#benchmarkCapabilities.set(capability, { recipient: recipient.#benchmarkIdentity, scope, binding, snapshot, controller, signal: combined, assertCurrent,
1246
+ refresh: async (runSignal) => {
1247
+ assertCurrent();
1248
+ const current = await this.inspectOwnerBenchmarkArm(selection, AbortSignal.any([combined, runSignal]));
1249
+ assertCurrent();
1250
+ if (!sameBenchmarkArm(snapshot, current))
1251
+ throw new Error('assistant-skills: benchmark source changed');
1252
+ }, consumed: false });
1253
+ this.#issuedBenchmarkCells.add(cellKey);
1254
+ this.#issuedBenchmarkArms.add(controller);
1255
+ this.ctx.effect(() => {
1256
+ const timer = setTimeout(() => controller.abort(), Math.max(1, binding.expiresAt - Date.now()));
1257
+ timer.unref?.();
1258
+ const stop = () => { clearTimeout(timer); this.#issuedBenchmarkArms.delete(controller); };
1259
+ combined.addEventListener('abort', stop, { once: true });
1260
+ return () => { combined.removeEventListener('abort', stop); stop(); controller.abort(); };
1261
+ }, 'assistant-skills.benchmark-grant');
1262
+ combined.addEventListener('abort', () => this.#issuedBenchmarkArms.delete(controller), { once: true });
1263
+ return capability;
1264
+ };
1265
+ /** No definition is imported into the active/candidate tables; a restart cannot restore the authority. */
1266
+ mountBenchmarkArm = async (capability, expected) => {
1267
+ const grant = AssistantSkillsService.#benchmarkCapabilities.get(capability);
1268
+ if (!grant || grant.consumed || grant.recipient !== this.#benchmarkIdentity || acceptanceDigest(expected) !== acceptanceDigest(grant.binding))
1269
+ throw new Error('assistant-skills: unrecognized benchmark capability');
1270
+ grant.consumed = true;
1271
+ await grant.refresh(this.#lifecycle.signal);
1272
+ grant.assertCurrent();
1273
+ const scopeKey = acceptanceDigest(grant.scope);
1274
+ if (this.#mountedBenchmarkArms.has(scopeKey))
1275
+ throw new Error('assistant-skills: benchmark recipient already mounted');
1276
+ this.#store.reserveDelegatedArm(grant.scope, grant.binding);
1277
+ const bindingDigest = acceptanceDigest(grant.binding), now = Date.now();
1278
+ const skill = { ...structuredClone(grant.snapshot.definition), version: grant.snapshot.version, parentVersion: null, retired: false, createdAt: now, updatedAt: now };
1279
+ const arm = { binding: grant.binding, bindingDigest, skill, scope: grant.scope, signal: grant.signal, revoke: () => grant.controller.abort(), assertCurrent: grant.assertCurrent, refresh: grant.refresh };
1280
+ this.#mountedBenchmarkArms.set(scopeKey, arm);
1281
+ const dispose = this.ctx.effect(() => {
1282
+ const timer = setTimeout(() => grant.controller.abort(), Math.max(1, grant.binding.expiresAt - Date.now()));
1283
+ timer.unref?.();
1284
+ return () => { clearTimeout(timer); grant.controller.abort(); if (this.#mountedBenchmarkArms.get(scopeKey) === arm)
1285
+ this.#mountedBenchmarkArms.delete(scopeKey); };
1286
+ }, 'assistant-skills.benchmark-arm');
1287
+ return { binding: structuredClone(grant.binding), bindingDigest, skill: { name: skill.name, version: skill.version, inputs: structuredClone(skill.inputs) }, dispose };
1288
+ };
952
1289
  #pending(scope, id) {
953
1290
  const candidate = this.#store.getCandidate(scope, id);
954
1291
  if (!candidate || candidate.state !== 'pending' || candidate.expiresAt <= Date.now())
@@ -1053,6 +1390,20 @@ export class AssistantSkillsService extends Service {
1053
1390
  return profile;
1054
1391
  }
1055
1392
  #repairProfile(record) { return this.#repairProfileAt(record, record.iteration); }
1393
+ /**
1394
+ * New authorizations persist this exact route. Legacy records predate the
1395
+ * field, so they may run only when their still-digest-bound profile supplied
1396
+ * an explicit pair; inherited routes must never be selected again on resume.
1397
+ */
1398
+ #repairModelSelection(record) {
1399
+ const frozen = record.authorization.modelSelection;
1400
+ if (frozen)
1401
+ return frozen;
1402
+ const primary = this.#repairProfileAt(record, 1);
1403
+ if (primary.provider !== undefined && primary.model !== undefined)
1404
+ return { provider: primary.provider, model: primary.model };
1405
+ throw new Error('assistant-skills: legacy inherited repair model selection unavailable');
1406
+ }
1056
1407
  #repairSource(record) {
1057
1408
  if (record.iteration === 1)
1058
1409
  return record.authorization.source;
@@ -1153,6 +1504,14 @@ export class AssistantSkillsService extends Service {
1153
1504
  if (!this.#repairRuntime || !profile || !holdout?.profile.canaryAdmissionTemplate || !Number.isSafeInteger(input.expiresAt)
1154
1505
  || input.expiresAt <= Date.now() || input.expiresAt > Math.min(holdout.profile.execution.expiresAt, Date.now() + 604800000))
1155
1506
  throw new Error('assistant-skills: finite current repair profile required');
1507
+ const selected = profile.provider === undefined
1508
+ ? this.ctx.get('assistantDelivery', false)?.inspectOwnerModelSelection({ authorityId: input.ownerRouteId, principalId: scope.principalId,
1509
+ workspace: scope.workspace, agentPreset: scope.preset, sourceAgent: agent })
1510
+ : { provider: profile.provider, model: profile.model };
1511
+ if (!repairModelRoute(selected))
1512
+ throw new Error('assistant-skills: exact current repair model selection required');
1513
+ const modelSelection = { provider: selected.provider, model: selected.model,
1514
+ ...(selected.reasoningEffort === undefined ? {} : { reasoningEffort: selected.reasoningEffort }) };
1156
1515
  const parent = this.#store.get(scope, profile.skillName);
1157
1516
  const sessionId = input.sessionId ?? String(agent.session.id);
1158
1517
  const snapshot = this.#goals().inspectOwnerGoalExecution({ ownerRouteId: input.ownerRouteId, principalId: scope.principalId, workspace: scope.workspace, preset: scope.preset, sessionId, goalId: input.goalId });
@@ -1164,7 +1523,8 @@ export class AssistantSkillsService extends Service {
1164
1523
  const profileSequence = sequenceIds.map(id => {
1165
1524
  const next = this.#repairProfiles.find(value => value.id === id && acceptanceDigest(value.scope) === acceptanceDigest(scope));
1166
1525
  const nextHoldout = next && this.inspectRepairProfile(next.externalHoldoutProfileId, scope);
1167
- if (!next || !nextHoldout || next.skillName !== profile.skillName || next.provider !== profile.provider || next.model !== profile.model
1526
+ if (!next || !nextHoldout || next.skillName !== profile.skillName
1527
+ || next.provider !== undefined && (next.provider !== modelSelection.provider || next.model !== modelSelection.model)
1168
1528
  || next.allowedTools.some(tool => !profile.allowedTools.includes(tool)) || next.maxOutputTokens > profile.maxOutputTokens || next.maxGoalRounds > profile.maxGoalRounds || next.maxDurationMs > profile.maxDurationMs
1169
1529
  || input.expiresAt > nextHoldout.profile.execution.expiresAt)
1170
1530
  throw new Error('assistant-skills: followup profile exceeds the frozen repair authority');
@@ -1185,7 +1545,7 @@ export class AssistantSkillsService extends Service {
1185
1545
  const authorization = { invocationId: input.invocationId, ownerRouteId: input.ownerRouteId,
1186
1546
  source: { goalId: input.goalId, sessionId, nativeGoalId: snapshot.storedGoal.nativeAtLastObservation.goalId, definitionDigest: snapshot.storedGoal.definition.digest },
1187
1547
  profileId: profile.id, profileDigest: acceptanceDigest([profile, holdout.digest]), skillName: profile.skillName,
1188
- parentVersion: parent.version, parentDigest: acceptanceDigest(parent), maxIterations: profile.maxIterations ?? 1, expiresAt: input.expiresAt,
1548
+ parentVersion: parent.version, parentDigest: acceptanceDigest(parent), maxIterations: profile.maxIterations ?? 1, expiresAt: input.expiresAt, modelSelection,
1189
1549
  ...(profileSequence.length > 1 ? { profileSequence } : {}), ...(feedbackAuthority ? { feedbackAuthority } : {}) };
1190
1550
  for (const action of ['draft', 'compare', 'canary', 'watch']) {
1191
1551
  this.#scope(agent, action);
@@ -1279,10 +1639,11 @@ export class AssistantSkillsService extends Service {
1279
1639
  }
1280
1640
  #repairAgentInput(record, trigger) {
1281
1641
  const profile = this.#repairProfile(record), primary = this.#repairProfileAt(record, 1);
1642
+ const modelSelection = this.#repairModelSelection(record);
1282
1643
  const usage = this.#store.repairUsage(record.scope, record.id);
1283
1644
  const callback = () => this.#assertRepair(record);
1284
1645
  this.#repairAssertions.set(record.id, { callback, triggerDigest: acceptanceDigest(trigger) });
1285
- return { ...profile, id: record.id, iteration: record.iteration, maxModelCalls: primary.maxModelCalls, maxToolCalls: primary.maxToolCalls,
1646
+ return { ...profile, ...modelSelection, id: record.id, iteration: record.iteration, maxModelCalls: primary.maxModelCalls, maxToolCalls: primary.maxToolCalls,
1286
1647
  initialModelCalls: usage.modelCalls, initialToolCalls: usage.toolCalls,
1287
1648
  recordUsage: kind => { this.#store.chargeRepairUsage(record.scope, record.id, kind, kind === 'model' ? primary.maxModelCalls : primary.maxToolCalls); },
1288
1649
  authorizationDigest: record.authorizationDigest, ownerRouteId: record.authorization.ownerRouteId,
@@ -1740,14 +2101,41 @@ export class AssistantSkillsService extends Service {
1740
2101
  }
1741
2102
  async run(exec, goalId, name, version, inputs, invocationId) {
1742
2103
  const scope = this.#scope(exec.agent, 'run');
2104
+ const delegated = this.#mountedBenchmarkArms.get(acceptanceDigest(scope));
2105
+ if (delegated) {
2106
+ delegated.assertCurrent();
2107
+ if (delegated.skill.name !== name || delegated.skill.version !== version)
2108
+ throw new Error('assistant-skills: exact delegated arm required');
2109
+ const task = this.#run(exec, goalId, delegated.skill, inputs, invocationId, undefined, delegated);
2110
+ this.#benchmarkRuns.add(task);
2111
+ try {
2112
+ return await task;
2113
+ }
2114
+ finally {
2115
+ this.#benchmarkRuns.delete(task);
2116
+ }
2117
+ }
1743
2118
  const skill = this.#store.get(scope, name);
1744
2119
  if (!skill || skill.retired || skill.version !== version)
1745
2120
  throw new Error('assistant-skills: active skill version required');
1746
2121
  return this.#run(exec, goalId, skill, inputs, invocationId);
1747
2122
  }
1748
- async #run(exec, goalId, skill, inputs, invocationId, candidateId) {
2123
+ async #run(exec, goalId, skill, inputs, invocationId, candidateId, delegated) {
1749
2124
  const action = candidateId ? 'trial' : 'run';
1750
2125
  const scope = this.#scope(exec.agent, action), { name, version } = skill;
2126
+ const assertDelegated = () => {
2127
+ if (!delegated)
2128
+ return;
2129
+ delegated.assertCurrent();
2130
+ if (this.#mountedBenchmarkArms.get(acceptanceDigest(scope)) !== delegated
2131
+ || acceptanceDigest(benchmarkArmDefinition(skill)) !== delegated.binding.definitionDigest)
2132
+ throw new Error('assistant-skills: delegated arm changed');
2133
+ };
2134
+ assertDelegated();
2135
+ if (delegated) {
2136
+ await delegated.refresh(exec.signal);
2137
+ assertDelegated();
2138
+ }
1751
2139
  const definition = instantiate(skill, inputs);
1752
2140
  const steps = definition.steps;
1753
2141
  // These declarations are part of the immutable saved definition, rather
@@ -1785,8 +2173,9 @@ export class AssistantSkillsService extends Service {
1785
2173
  const tool = candidateId ? 'skill_trial' : 'skill_run';
1786
2174
  return { state: 'awaiting-native-round', performed: false, context: 'No skill step or durable invocation has been claimed or executed; this is not a queued background task.', next: `After the Host starts this Goal's native round, call ${tool} again with this same goal, skill or candidate, inputs, and invocation_id.`, goalId, ...(candidateId ? { candidateId } : {}), skillName: name, version, invocationId, inputs };
1787
2175
  }
1788
- if (acceptanceDigest(scope) !== acceptanceDigest(current.scope) || goalId === skill.source.goal.id)
1789
- throw new Error('assistant-skills: fresh owner Goal required');
2176
+ // Consume the typed precondition family rather than an ad-hoc condition so
2177
+ // an unknown/tampered declaration fails closed before a run is claimed.
2178
+ evaluatePreconditions(skill.preconditions, { ownerScopeDigest: acceptanceDigest(scope), currentScopeDigest: acceptanceDigest(current.scope), goalId, sourceGoalId: skill.source.goal.id });
1790
2179
  const identity = acceptanceDigest(current);
1791
2180
  const goalContext = current;
1792
2181
  // claim() reconciles a deployed version while holding its transaction. Do
@@ -1812,7 +2201,9 @@ export class AssistantSkillsService extends Service {
1812
2201
  throw new Error('assistant-skills: deployment authority ended');
1813
2202
  }
1814
2203
  }
1815
- const claim = this.#store.claim(scope, { invocationId, goalId, sessionId: current.sessionId, skillName: name, version, inputs, goalExecutionRunId: current.goalExecutionRunId, goalDefinitionDigest: current.definition.digest, ...(typeof goalContext.nativeGoalId === 'string' ? { nativeGoalId: goalContext.nativeGoalId } : {}), ...(candidateId ? { candidateId } : {}) });
2204
+ assertDelegated();
2205
+ const claimInput = { invocationId, goalId, sessionId: current.sessionId, skillName: name, version, inputs, goalExecutionRunId: current.goalExecutionRunId, goalDefinitionDigest: current.definition.digest, ...(typeof goalContext.nativeGoalId === 'string' ? { nativeGoalId: goalContext.nativeGoalId } : {}), ...(candidateId ? { candidateId } : {}) };
2206
+ const claim = delegated ? this.#store.claimDelegated(scope, claimInput, delegated.bindingDigest) : this.#store.claim(scope, claimInput);
1816
2207
  if (!claim.claimed) {
1817
2208
  if (claim.run.state !== 'succeeded')
1818
2209
  throw new Error(`assistant-skills: invocation ${claim.run.id} is ${claim.run.state}; inspect skill_status, do not replay`);
@@ -1838,18 +2229,25 @@ export class AssistantSkillsService extends Service {
1838
2229
  const timeout = new AbortController();
1839
2230
  const timer = setTimeout(() => timeout.abort(), this.#duration);
1840
2231
  timer.unref?.();
1841
- const signal = AbortSignal.any([exec.signal, timeout.signal, this.#lifecycle.signal, ...(deploymentRevision ? [deploymentRevision.signal] : [])]);
2232
+ const signal = AbortSignal.any([exec.signal, timeout.signal, this.#lifecycle.signal, ...(delegated ? [delegated.signal] : []), ...(deploymentRevision ? [deploymentRevision.signal] : [])]);
2233
+ const settle = (work) => delegated ? awaitBenchmarkSignal(work, signal) : work;
1842
2234
  const completed = [];
1843
2235
  let state = 'unknown';
1844
2236
  let dispatched = false;
2237
+ // Resolve the compensation family up front; an unsupported/tampered value
2238
+ // fails closed before any step is dispatched.
2239
+ const compensation = compensationDirective(definition.compensation);
1845
2240
  const revalidate = () => {
1846
2241
  signal.throwIfAborted();
2242
+ assertDelegated();
1847
2243
  if (acceptanceDigest(this.#scope(exec.agent, action)) !== acceptanceDigest(scope)
1848
2244
  || acceptanceDigest(this.#goals().inspectWorkflowRunContext(exec.agent, goalId)) !== identity)
1849
2245
  throw new Error('assistant-skills: current authority changed');
1850
2246
  if (this.#store.getRun(scope, claim.run.id)?.state !== 'running')
1851
2247
  throw new Error('assistant-skills: invocation no longer owns dispatch');
1852
- if (candidateId)
2248
+ if (delegated)
2249
+ assertDelegated();
2250
+ else if (candidateId)
1853
2251
  this.#pending(scope, candidateId);
1854
2252
  else {
1855
2253
  const live = this.#store.get(scope, name);
@@ -1868,11 +2266,13 @@ export class AssistantSkillsService extends Service {
1868
2266
  for (const observation of observations) {
1869
2267
  if (observation.beforeStepId !== step.id)
1870
2268
  continue;
2269
+ if (delegated)
2270
+ await delegated.refresh(signal);
1871
2271
  revalidate();
1872
2272
  if (!this.#allowed.includes('read'))
1873
2273
  throw new Error('assistant-skills: current tool allowlist denied required file observation');
1874
2274
  dispatched = true;
1875
- const observed = await exec.agent.ctx.get('tools').execute({
2275
+ const observed = await settle(exec.agent.ctx.get('tools').execute({
1876
2276
  callId: ToolCallId(`${exec.callId}:skill-observation:${index + 1}`),
1877
2277
  rootCallId: exec.rootCallId,
1878
2278
  parent: exec.token,
@@ -1880,7 +2280,9 @@ export class AssistantSkillsService extends Service {
1880
2280
  name: 'read',
1881
2281
  arguments: { file_path: observation.filePath, limit: 1 },
1882
2282
  signal,
1883
- });
2283
+ }));
2284
+ if (delegated)
2285
+ await delegated.refresh(signal);
1884
2286
  revalidate();
1885
2287
  for (const context of observed.additionalContexts ?? [])
1886
2288
  exec.deferContext(context);
@@ -1902,14 +2304,29 @@ export class AssistantSkillsService extends Service {
1902
2304
  state = 'failed';
1903
2305
  break stepsLoop;
1904
2306
  }
2307
+ if (delegated)
2308
+ await delegated.refresh(signal);
1905
2309
  revalidate();
1906
2310
  dispatched = false;
1907
2311
  }
2312
+ if (delegated)
2313
+ await delegated.refresh(signal);
1908
2314
  revalidate();
2315
+ // Dependency DAG gate: every declared predecessor must have completed
2316
+ // successfully. Topological storage means a missing predecessor can only
2317
+ // result from an earlier failure, at which point the resolved compensation
2318
+ // family stops the remaining steps (the v1 family always does).
2319
+ const succeededById = new Set(completed.filter(entry => entry.state === 'succeeded').map(entry => entry.id));
2320
+ if (compensation.stopRemainingSteps && step.dependsOn.some(dependency => !succeededById.has(dependency))) {
2321
+ state = 'failed';
2322
+ break stepsLoop;
2323
+ }
1909
2324
  if (!this.#allowed.includes(step.toolName))
1910
2325
  throw new Error('assistant-skills: current tool allowlist denied');
1911
2326
  dispatched = true;
1912
- const result = await exec.agent.ctx.get('tools').execute({ callId: ToolCallId(`${exec.callId}:skill:${index + 1}`), rootCallId: exec.rootCallId, parent: exec.token, agent: exec.agent, name: step.toolName, arguments: step.arguments, signal });
2327
+ const result = await settle(exec.agent.ctx.get('tools').execute({ callId: ToolCallId(`${exec.callId}:skill:${index + 1}`), rootCallId: exec.rootCallId, parent: exec.token, agent: exec.agent, name: step.toolName, arguments: step.arguments, signal }));
2328
+ if (delegated)
2329
+ await delegated.refresh(signal);
1913
2330
  revalidate();
1914
2331
  for (const context of result.additionalContexts ?? [])
1915
2332
  exec.deferContext(context);
@@ -1922,6 +2339,8 @@ export class AssistantSkillsService extends Service {
1922
2339
  }
1923
2340
  if (result.concludesTurn)
1924
2341
  exec.concludeTurn();
2342
+ if (delegated)
2343
+ await delegated.refresh(signal);
1925
2344
  revalidate();
1926
2345
  dispatched = false;
1927
2346
  if (index === steps.length - 1)