@aviaratech/ai-delivery 0.3.18 → 0.3.20

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.
@@ -792,9 +792,13 @@ function observeOwnedProcesses(state, snapshot) {
792
792
  state.sampled = true;
793
793
  return [...selected.values()];
794
794
  }
795
- function sampleOwnedTree(state, repoRoot, bounds, baseline, newOutputBytes, controllerRssBytes = 0) {
796
- const owned = observeOwnedProcesses(state, processSnapshot());
797
- const rssBytes = owned.reduce((total, member) => total + member.rssBytes, controllerRssBytes);
795
+ function sampleOwnedTree(state, repoRoot, bounds, baseline, newOutputBytes, controllerRssBytes = 0, includeControllerTree = false) {
796
+ const snapshot = processSnapshot();
797
+ const owned = observeOwnedProcesses(state, snapshot);
798
+ const controller = includeControllerTree
799
+ ? controllerTreeRssBytes(snapshot, new Set(owned.map((member) => member.pid)))
800
+ : controllerRssBytes;
801
+ const rssBytes = owned.reduce((total, member) => total + member.rssBytes, controller);
798
802
  if (!Number.isSafeInteger(rssBytes))
799
803
  throw new DeliveryError('Owned aggregate RSS is outside the safe integer range.');
800
804
  if (rssBytes > bounds.maxAggregateRssBytes) {
@@ -818,6 +822,24 @@ function sampleOwnedTree(state, repoRoot, bounds, baseline, newOutputBytes, cont
818
822
  ownedProcessCount: owned.length,
819
823
  };
820
824
  }
825
+ /** Accounting includes the caller's tree; containment remains restricted to the recorded command. */
826
+ function controllerTreeRssBytes(snapshot = processSnapshot(), excluded = new Set()) {
827
+ const controller = snapshot.get(process.pid);
828
+ if (controller === undefined || controller.status.startsWith('Z'))
829
+ throw new DeliveryError('Runtime setup controller resource observation is unavailable.');
830
+ const selected = new Set([process.pid]);
831
+ let bytes = process.memoryUsage().rss;
832
+ for (const pid of selected)
833
+ for (const member of snapshot.values())
834
+ if (member.ppid === pid && !member.status.startsWith('Z') && !selected.has(member.pid)) {
835
+ selected.add(member.pid);
836
+ if (!excluded.has(member.pid))
837
+ bytes += member.rssBytes;
838
+ }
839
+ if (!Number.isSafeInteger(bytes))
840
+ throw new DeliveryError('Controller aggregate RSS observation overflowed.');
841
+ return bytes;
842
+ }
821
843
  async function confirmOwnedCleanup(state) {
822
844
  if (!state.sampled || state.rootIdentity === undefined) {
823
845
  throw new DeliveryError('Owned process cleanup cannot be verified without a root identity.');
@@ -1316,7 +1338,7 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
1316
1338
  executable,
1317
1339
  ...args,
1318
1340
  ], {
1319
- cwd: repoRoot,
1341
+ cwd: execution?.cwd ?? repoRoot,
1320
1342
  detached: process.platform !== 'win32',
1321
1343
  stdio: ['pipe', 'pipe', 'pipe'],
1322
1344
  ...(baseline?.gate === undefined && execution === undefined
@@ -1334,6 +1356,8 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
1334
1356
  ? undefined
1335
1357
  : { rootPid: child.pid, sampled: false, tracked: new Map() };
1336
1358
  let outputBytes = 0;
1359
+ let retainedOutputBytes = 0;
1360
+ const outputLimit = execution?.maxCapturedOutputBytes ?? 8 * 1024 * 1024;
1337
1361
  let sampledOutputBytes = 0;
1338
1362
  let failure;
1339
1363
  let cleanupFailure;
@@ -1438,11 +1462,19 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
1438
1462
  stop('source-phase captured output exceeded remaining cumulative allowance');
1439
1463
  return;
1440
1464
  }
1441
- if (outputBytes > 8 * 1024 * 1024) {
1442
- stop('captured output exceeded 8 MiB');
1465
+ if (outputBytes > outputLimit) {
1466
+ const remaining = outputLimit - retainedOutputBytes;
1467
+ if (remaining > 0) {
1468
+ chunks.push(Buffer.from(chunk.subarray(0, remaining)));
1469
+ retainedOutputBytes += Math.min(remaining, chunk.length);
1470
+ }
1471
+ stop(execution?.maxCapturedOutputBytes === undefined
1472
+ ? 'captured output exceeded 8 MiB'
1473
+ : `captured output exceeded ${outputLimit} bytes`);
1443
1474
  return;
1444
1475
  }
1445
1476
  chunks.push(chunk);
1477
+ retainedOutputBytes += chunk.length;
1446
1478
  };
1447
1479
  child.stdout.on('data', (chunk) => capture(stdout, chunk));
1448
1480
  child.stderr.on('data', (chunk) => capture(stderr, chunk));
@@ -1474,7 +1506,7 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
1474
1506
  observeOwnedProcesses(owned, processSnapshot());
1475
1507
  writer?.observed(owned);
1476
1508
  if (resourceBounds !== undefined) {
1477
- const result = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, undefined, execution === undefined ? 0 : process.memoryUsage().rss);
1509
+ const result = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, undefined, execution === undefined ? 0 : process.memoryUsage().rss, execution?.includeControllerTree);
1478
1510
  onResourceSample?.(result);
1479
1511
  onRunning?.(outputBytes, result);
1480
1512
  }
@@ -1490,7 +1522,7 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
1490
1522
  if (failure !== undefined)
1491
1523
  return;
1492
1524
  sampledOutputBytes = bytes;
1493
- const result = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, bytes + (execution === undefined ? 0 : outputBytes), execution === undefined ? 0 : process.memoryUsage().rss);
1525
+ const result = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, bytes + (execution === undefined ? 0 : outputBytes), execution === undefined ? 0 : process.memoryUsage().rss, execution?.includeControllerTree);
1494
1526
  onResourceSample?.(result);
1495
1527
  onRunning?.(outputBytes, result);
1496
1528
  }
@@ -1533,7 +1565,7 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
1533
1565
  const sample = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, baseline === undefined
1534
1566
  ? undefined
1535
1567
  : (await positiveNewOutputBytes(baseline, scanCancellation.signal)) +
1536
- (execution === undefined ? 0 : outputBytes), execution === undefined ? 0 : process.memoryUsage().rss);
1568
+ (execution === undefined ? 0 : outputBytes), execution === undefined ? 0 : process.memoryUsage().rss, execution?.includeControllerTree);
1537
1569
  onResourceSample?.(sample);
1538
1570
  onRunning?.(outputBytes, sample);
1539
1571
  }
@@ -1990,7 +2022,12 @@ const IssueSourcePhaseInputSchema = z.strictObject({
1990
2022
  path: z.string().min(1),
1991
2023
  effect: z.discriminatedUnion('kind', [
1992
2024
  z.strictObject({ kind: z.literal('preserve') }),
1993
- z.strictObject({ kind: z.literal('commitOnce'), parent: SourceSha, tree: SourceSha }),
2025
+ z.strictObject({
2026
+ kind: z.literal('commitOnce'),
2027
+ parent: SourceSha,
2028
+ tree: SourceSha,
2029
+ configDigest: SourceDigest.optional(),
2030
+ }),
1994
2031
  ]),
1995
2032
  }),
1996
2033
  caller: SourceArtifactSchema,
@@ -2309,7 +2346,10 @@ export async function withIssueSourcePhase(supplied, operation) {
2309
2346
  const branch = git(row.path, 'branch', '--show-current');
2310
2347
  const dirty = sourceDirty(row.path);
2311
2348
  const candidate = await loadDeliveryConfig(row.path);
2312
- if (branch !== input.source.branch || candidate.configDigest !== input.source.configDigest)
2349
+ const configDigest = head === input.source.head || input.source.effect.kind !== 'commitOnce'
2350
+ ? input.source.configDigest
2351
+ : (input.source.effect.configDigest ?? input.source.configDigest);
2352
+ if (branch !== input.source.branch || candidate.configDigest !== configDigest)
2313
2353
  throw new DeliveryError('Source-phase candidate branch or configuration changed.');
2314
2354
  if (head === input.source.head) {
2315
2355
  if (git(row.path, 'diff', '--cached', '--name-only', input.source.indexTree) !== '' ||
@@ -2886,63 +2926,172 @@ export async function withIssueSourcePhase(supplied, operation) {
2886
2926
  return sourceReceipt(record);
2887
2927
  }
2888
2928
  /** @internal Runtime setup shares the existing writer, ownership handshake and bounded command runner. */
2889
- export async function withRuntimeSetupWriter(root, operation) {
2890
- return withVerificationWriter(root, async (writer) => operation({
2891
- assertQuiescent: () => writer.assertQuiescent(),
2892
- run: async (argv, bounds, signal) => {
2893
- assertResourceBounds(bounds);
2894
- const roots = bounds.outputRoots?.map((path) => resolve(root, path)).sort();
2895
- if (roots !== undefined) {
2896
- for (let i = 1; i < roots.length; i++)
2897
- if (roots[i] === roots[i - 1] || roots[i].startsWith(`${roots[i - 1]}${sep}`))
2898
- throw new DeliveryError('Filesystem output roots must not overlap.');
2899
- }
2900
- return withOutputObservationGate(root, roots !== undefined, async (gate) => {
2901
- const links = new Map();
2902
- const files = roots === undefined
2903
- ? undefined
2904
- : await withOutputGateLock(gate, 'scan', () => scanOutputRoots(roots, links), signal);
2905
- const baseline = roots === undefined || files === undefined
2906
- ? undefined
2907
- : {
2908
- roots,
2909
- files,
2910
- links,
2911
- baselineId: digestValue([...files]),
2912
- ...(gate === undefined ? {} : { gate }),
2929
+ export async function withRuntimeSetupWriter(root, operation, scope) {
2930
+ const cancellation = new AbortController();
2931
+ const signal = scope?.signal === undefined ? cancellation.signal : AbortSignal.any([scope.signal, cancellation.signal]);
2932
+ let failure;
2933
+ let baseline;
2934
+ let captured = 0;
2935
+ const started = Date.now();
2936
+ const guard = () => {
2937
+ if (failure !== undefined)
2938
+ throw failure;
2939
+ if (signal.aborted)
2940
+ throw new DeliveryError('Runtime setup cancelled.');
2941
+ const rss = scope === undefined ? 0 : controllerTreeRssBytes();
2942
+ if (scope !== undefined) {
2943
+ if (rss > scope.bounds.maxAggregateRssBytes)
2944
+ throw new DeliveryError(`Controller aggregate RSS ${rss} exceeded limit ${scope.bounds.maxAggregateRssBytes}.`);
2945
+ assertDiskHeadroom(root, scope.bounds, baseline);
2946
+ }
2947
+ return rss;
2948
+ };
2949
+ if (scope !== undefined) {
2950
+ assertResourceBounds(scope.bounds);
2951
+ if (!Number.isSafeInteger(scope.maxCapturedOutputBytes) ||
2952
+ scope.maxCapturedOutputBytes <= 0 ||
2953
+ scope.maxCapturedOutputBytes > 8 * 1024 ** 2)
2954
+ throw new DeliveryError('Invalid runtime setup captured-output bound; maximum is 8 MiB.');
2955
+ guard();
2956
+ }
2957
+ root = gitRoot(root);
2958
+ return withVerificationWriter(root, async (writer) => withOutputObservationGate(root, scope !== undefined, async (scopeGate) => {
2959
+ if (scope !== undefined) {
2960
+ const paths = [...(scope.bounds.outputRoots ?? []), join(gitCommonDir(root), 'ai-delivery')]
2961
+ .map((path) => resolve(root, path))
2962
+ .sort();
2963
+ const roots = paths.filter((path, index) => !paths.slice(0, index).some((prior) => path === prior || path.startsWith(`${prior}${sep}`)));
2964
+ const links = new Map();
2965
+ const files = await withOutputGateLock(scopeGate, 'scan', () => scanOutputRoots(roots, links), signal);
2966
+ baseline = {
2967
+ roots,
2968
+ links,
2969
+ files,
2970
+ baselineId: digestValue([...files]),
2971
+ ...(scopeGate === undefined ? {} : { gate: scopeGate }),
2972
+ };
2973
+ }
2974
+ const checkResources = async () => {
2975
+ const rss = guard();
2976
+ if (scope === undefined || baseline === undefined)
2977
+ return;
2978
+ const newOutputBytes = captured + (await positiveNewOutputBytes(baseline, signal));
2979
+ guard();
2980
+ if (newOutputBytes > scope.bounds.maxNewOutputBytes)
2981
+ throw new DeliveryError(`Runtime setup output ${newOutputBytes} exceeded limit ${scope.bounds.maxNewOutputBytes}.`);
2982
+ reportVerificationProgress({
2983
+ state: 'running',
2984
+ stageId: 'runtime-setup',
2985
+ completedStages: 0,
2986
+ reusedStages: 0,
2987
+ remainingStages: 1,
2988
+ elapsedMs: Date.now() - started,
2989
+ capturedOutputBytes: captured,
2990
+ sampledAggregateRssBytes: rss,
2991
+ sampledFreeDiskBytes: freeDiskBytes(root, baseline),
2992
+ sampledNewOutputBytes: newOutputBytes,
2993
+ });
2994
+ };
2995
+ await checkResources();
2996
+ const timer = scope === undefined
2997
+ ? undefined
2998
+ : setInterval(() => {
2999
+ try {
3000
+ guard();
3001
+ }
3002
+ catch (error) {
3003
+ failure ??= error instanceof Error ? error : new DeliveryError(String(error));
3004
+ cancellation.abort();
3005
+ }
3006
+ }, 1_000);
3007
+ timer?.unref();
3008
+ try {
3009
+ const result = await operation({
3010
+ assertQuiescent: () => writer.assertQuiescent(),
3011
+ checkResources,
3012
+ signal,
3013
+ run: async (argv, bounds, commandSignal, environment, cwd) => {
3014
+ await checkResources();
3015
+ assertResourceBounds(bounds);
3016
+ const roots = bounds.outputRoots?.map((path) => resolve(root, path)).sort();
3017
+ if (roots !== undefined) {
3018
+ for (let i = 1; i < roots.length; i++)
3019
+ if (roots[i] === roots[i - 1] || roots[i].startsWith(`${roots[i - 1]}${sep}`))
3020
+ throw new DeliveryError('Filesystem output roots must not overlap.');
3021
+ }
3022
+ const run = async (gate) => {
3023
+ const links = new Map();
3024
+ const files = roots === undefined
3025
+ ? undefined
3026
+ : await withOutputGateLock(gate, 'scan', () => scanOutputRoots(roots, links), signal);
3027
+ const commandBaseline = roots === undefined || files === undefined
3028
+ ? undefined
3029
+ : {
3030
+ roots,
3031
+ files,
3032
+ links,
3033
+ baselineId: digestValue([...files]),
3034
+ ...(gate === undefined ? {} : { gate }),
3035
+ };
3036
+ assertDiskHeadroom(root, bounds, commandBaseline);
3037
+ let commandCaptured = 0;
3038
+ const output = await runStageCommand(root, argv, commandSignal === undefined ? signal : AbortSignal.any([signal, commandSignal]), (capturedOutputBytes, sample, reason) => reportVerificationProgress({
3039
+ state: 'running',
3040
+ stageId: 'runtime-setup:install',
3041
+ completedStages: 0,
3042
+ reusedStages: 0,
3043
+ remainingStages: 1,
3044
+ elapsedMs: Date.now() - started,
3045
+ capturedOutputBytes,
3046
+ ...(reason === undefined ? {} : { reason }),
3047
+ ...(sample === undefined
3048
+ ? {}
3049
+ : {
3050
+ sampledAggregateRssBytes: sample.aggregateRssBytes,
3051
+ sampledFreeDiskBytes: sample.freeDiskBytes,
3052
+ sampledNewOutputBytes: sample.newOutputBytes,
3053
+ }),
3054
+ }), bounds, commandBaseline, undefined, writer, scope === undefined && environment === undefined && cwd === undefined
3055
+ ? undefined
3056
+ : {
3057
+ environment: environment ?? process.env,
3058
+ ...(cwd === undefined ? {} : { cwd }),
3059
+ ...(scope === undefined
3060
+ ? {}
3061
+ : { maxCapturedOutputBytes: scope.maxCapturedOutputBytes, includeControllerTree: true }),
3062
+ onCaptured: (bytes) => {
3063
+ captured += bytes - commandCaptured;
3064
+ commandCaptured = bytes;
3065
+ },
3066
+ });
3067
+ await checkResources();
3068
+ return output;
2913
3069
  };
2914
- assertDiskHeadroom(root, bounds, baseline);
2915
- const started = Date.now();
2916
- const output = await runStageCommand(root, argv, signal, (capturedOutputBytes, sample, reason) => reportVerificationProgress({
2917
- state: 'running',
2918
- stageId: 'runtime-setup',
2919
- completedStages: 0,
2920
- reusedStages: 0,
2921
- remainingStages: 1,
2922
- elapsedMs: Date.now() - started,
2923
- capturedOutputBytes,
2924
- ...(reason === undefined ? {} : { reason }),
2925
- ...(sample === undefined
2926
- ? {}
2927
- : {
2928
- sampledAggregateRssBytes: sample.aggregateRssBytes,
2929
- sampledFreeDiskBytes: sample.freeDiskBytes,
2930
- sampledNewOutputBytes: sample.newOutputBytes,
2931
- }),
2932
- }), bounds, baseline, undefined, writer);
2933
- if (signal?.aborted)
2934
- throw new DeliveryError('Runtime setup cancelled.');
2935
- reportVerificationProgress({
2936
- state: 'completed',
2937
- stageId: 'runtime-setup',
2938
- completedStages: 1,
2939
- reusedStages: 0,
2940
- remainingStages: 0,
2941
- elapsedMs: Date.now() - started,
2942
- });
2943
- return output;
3070
+ return scope === undefined ? withOutputObservationGate(root, roots !== undefined, run) : run(scopeGate);
3071
+ },
2944
3072
  });
2945
- },
3073
+ await checkResources();
3074
+ reportVerificationProgress({
3075
+ state: 'completed',
3076
+ stageId: 'runtime-setup',
3077
+ completedStages: 1,
3078
+ reusedStages: 0,
3079
+ remainingStages: 0,
3080
+ elapsedMs: Date.now() - started,
3081
+ });
3082
+ return result;
3083
+ }
3084
+ catch (error) {
3085
+ if (failure !== undefined)
3086
+ throw new DeliveryError(`${failure.message} Runtime setup operation failed (${error instanceof Error ? error.message : String(error)}).`);
3087
+ if (signal.aborted)
3088
+ throw new DeliveryError(`Runtime setup cancelled. ${error instanceof Error ? error.message : String(error)}`);
3089
+ throw error;
3090
+ }
3091
+ finally {
3092
+ if (timer !== undefined)
3093
+ clearInterval(timer);
3094
+ }
2946
3095
  }));
2947
3096
  }
2948
3097
  //# sourceMappingURL=verification.js.map