deepline 0.3.201 → 0.3.203

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 (41) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +62 -1
  2. package/dist/bundling-sources/sdk/src/http.ts +55 -6
  3. package/dist/bundling-sources/sdk/src/index.ts +1 -0
  4. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  5. package/dist/bundling-sources/sdk/src/types.ts +8 -0
  6. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +4 -2
  7. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +460 -96
  8. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +11 -1
  9. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-invocation-fence.ts +6 -2
  10. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +401 -2
  11. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +458 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/provider-no-progress.ts +40 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +7 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +25 -3
  15. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +1 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +119 -171
  17. package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +37 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +238 -204
  19. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +16 -7
  20. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +136 -25
  21. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +21 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +64 -15
  23. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +5 -5
  24. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/result-receipt-policy.ts +46 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +14 -5
  27. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +5 -1
  28. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +3 -9
  29. package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +1 -1
  30. package/dist/cli/index.js +362 -55
  31. package/dist/cli/index.mjs +365 -57
  32. package/dist/index.d.mts +129 -1
  33. package/dist/index.d.ts +129 -1
  34. package/dist/index.js +64 -7
  35. package/dist/index.mjs +64 -7
  36. package/dist/install-integrity.json +3 -0
  37. package/dist/release.d.mts +1 -1
  38. package/dist/release.d.ts +1 -1
  39. package/dist/release.js +1 -1
  40. package/dist/release.mjs +1 -1
  41. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -3051,7 +3051,7 @@ var SDK_RELEASE = {
3051
3051
  // getters keep their established compatibility behavior.
3052
3052
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3053
3053
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3054
- version: "0.3.201",
3054
+ version: "0.3.203",
3055
3055
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
3056
3056
  packageCapabilities: {
3057
3057
  updatePreferences: 1
@@ -3642,6 +3642,11 @@ var MAX_DIAGNOSTIC_HEADER_LENGTH = 120;
3642
3642
  var COWORK_NETWORK_HINT = "Claude Cowork appears to be running Deepline in a network-restricted sandbox. In Claude Desktop, open Settings > Capabilities, turn on Allow network egress, and set Domain allowlist to All domains for the Cowork session.";
3643
3643
  var REQUEST_TIMEOUT_MARKER = Symbol("deeplineRequestTimeout");
3644
3644
  var REQUEST_ABORT_MARKER = Symbol("deeplineRequestAbort");
3645
+ function requestCancelledError(signal) {
3646
+ const error = withErrorCause(new Error("Request aborted"), signal.reason);
3647
+ error[REQUEST_ABORT_MARKER] = true;
3648
+ return error;
3649
+ }
3645
3650
  function normalizeRequestAbortError(error, input2) {
3646
3651
  const normalized = error instanceof Error ? error : new Error(String(error));
3647
3652
  const tagged = normalized;
@@ -3820,14 +3825,27 @@ var HttpClient = class {
3820
3825
  const candidateUrls = options?.exactUrlOnly ? [url] : buildCandidateUrls(url);
3821
3826
  let retryAfterDelayMs = null;
3822
3827
  const maxRetries = options?.maxRetries ?? this.config.maxRetries;
3823
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
3828
+ requestAttempts: for (let attempt = 0; attempt <= maxRetries; attempt++) {
3829
+ if (options?.signal?.aborted) {
3830
+ lastError = requestCancelledError(options.signal);
3831
+ break;
3832
+ }
3824
3833
  if (attempt > 0) {
3825
3834
  const backoffMs = Math.min(1e3 * Math.pow(2, attempt - 1), 3e4);
3826
3835
  const delayMs = retryAfterDelayMs === null ? backoffMs : Math.max(backoffMs, retryAfterDelayMs);
3827
3836
  retryAfterDelayMs = null;
3828
- await sleep(delayMs);
3837
+ try {
3838
+ await sleep(delayMs, options?.signal);
3839
+ } catch (error) {
3840
+ lastError = error instanceof Error ? error : new Error(String(error));
3841
+ break;
3842
+ }
3829
3843
  }
3830
3844
  for (const candidateUrl of candidateUrls) {
3845
+ if (options?.signal?.aborted) {
3846
+ lastError = requestCancelledError(options.signal);
3847
+ break requestAttempts;
3848
+ }
3831
3849
  const controller = new AbortController();
3832
3850
  const timeoutMs = options?.timeout ?? this.config.timeout;
3833
3851
  let requestTimedOut = false;
@@ -3835,6 +3853,10 @@ var HttpClient = class {
3835
3853
  requestTimedOut = true;
3836
3854
  controller.abort();
3837
3855
  }, timeoutMs);
3856
+ const abortRequest = () => controller.abort();
3857
+ options?.signal?.addEventListener("abort", abortRequest, {
3858
+ once: true
3859
+ });
3838
3860
  try {
3839
3861
  const response = await fetch(candidateUrl, {
3840
3862
  method,
@@ -3847,8 +3869,8 @@ var HttpClient = class {
3847
3869
  clearTimeout(timeoutId);
3848
3870
  return null;
3849
3871
  }
3850
- clearTimeout(timeoutId);
3851
3872
  const body = await response.text();
3873
+ clearTimeout(timeoutId);
3852
3874
  const parsed = parseResponseBody(body);
3853
3875
  const structuredToolError = options?.toolId && !response.ok ? deserializeToolExecutionFailure(
3854
3876
  apiErrorMessage(parsed, response.status),
@@ -3943,6 +3965,10 @@ var HttpClient = class {
3943
3965
  return parsed;
3944
3966
  } catch (error) {
3945
3967
  clearTimeout(timeoutId);
3968
+ if (options?.signal?.aborted) {
3969
+ lastError = requestCancelledError(options.signal);
3970
+ break requestAttempts;
3971
+ }
3946
3972
  if (error instanceof AuthError || error instanceof DeeplineError) {
3947
3973
  throw error;
3948
3974
  }
@@ -3954,6 +3980,9 @@ var HttpClient = class {
3954
3980
  timeoutMs,
3955
3981
  timedOut: requestTimedOut
3956
3982
  });
3983
+ } finally {
3984
+ clearTimeout(timeoutId);
3985
+ options?.signal?.removeEventListener("abort", abortRequest);
3957
3986
  }
3958
3987
  }
3959
3988
  if (attempt < maxRetries) continue;
@@ -4282,8 +4311,23 @@ function decodeSseFrame(frame) {
4282
4311
  }
4283
4312
  return parsed;
4284
4313
  }
4285
- function sleep(ms) {
4286
- return new Promise((resolve22) => setTimeout(resolve22, ms));
4314
+ function sleep(ms, signal) {
4315
+ return new Promise((resolve22, reject) => {
4316
+ if (signal?.aborted) {
4317
+ reject(requestCancelledError(signal));
4318
+ return;
4319
+ }
4320
+ const timeoutId = setTimeout(() => {
4321
+ signal?.removeEventListener("abort", abort);
4322
+ resolve22();
4323
+ }, ms);
4324
+ const abort = () => {
4325
+ clearTimeout(timeoutId);
4326
+ signal?.removeEventListener("abort", abort);
4327
+ reject(requestCancelledError(signal));
4328
+ };
4329
+ signal?.addEventListener("abort", abort, { once: true });
4330
+ });
4287
4331
  }
4288
4332
  function withCoworkNetworkHint(message) {
4289
4333
  if (!isCoworkLikeSandbox2() || message.includes(COWORK_NETWORK_HINT)) {
@@ -5470,9 +5514,13 @@ function normalizePlayRunLedgerSnapshot(value, fallback) {
5470
5514
  tableNamespace: optionalString(rawDataset.tableNamespace) ?? datasetId,
5471
5515
  phase,
5472
5516
  persistedRows: Math.max(0, finiteNumber(rawDataset.persistedRows) ?? 0),
5517
+ persistedRowsObserved: rawDataset.persistedRowsObserved === true,
5473
5518
  succeededRows: Math.max(0, finiteNumber(rawDataset.succeededRows) ?? 0),
5519
+ succeededRowsObserved: rawDataset.succeededRowsObserved === true,
5474
5520
  failedRows: Math.max(0, finiteNumber(rawDataset.failedRows) ?? 0),
5521
+ failedRowsObserved: rawDataset.failedRowsObserved === true,
5475
5522
  complete: rawDataset.complete === true,
5523
+ completeObserved: rawDataset.completeObserved === true,
5476
5524
  // ADR 0019 birth survives the snapshot round trip. A partial record is
5477
5525
  // dropped rather than half-admitted, so a dataset either states where its
5478
5526
  // rows came from or says nothing.
@@ -7212,6 +7260,7 @@ var DeeplineClient = class _DeeplineClient {
7212
7260
  this.http = new HttpClient(this.config);
7213
7261
  this.runs = {
7214
7262
  get: (runId, options2) => this.getRunStatus(runId, options2),
7263
+ status: (runId) => this.getRunLifecycleStatus(runId),
7215
7264
  list: (options2) => this.listRuns(options2),
7216
7265
  listPage: (options2) => this.listRunsPage(options2),
7217
7266
  tail: (runId, options2) => this.tailRun(runId, options2),
@@ -8624,6 +8673,13 @@ var DeeplineClient = class _DeeplineClient {
8624
8673
  }
8625
8674
  };
8626
8675
  }
8676
+ /** Read only the bounded scalar projection; does not hydrate the run package. */
8677
+ async getRunLifecycleStatus(runId) {
8678
+ const response = await this.http.get(
8679
+ `/api/v2/runs/${encodeURIComponent(runId)}/status`
8680
+ );
8681
+ return response;
8682
+ }
8627
8683
  /**
8628
8684
  * List play runs using the public runs resource model.
8629
8685
  *
@@ -9017,7 +9073,8 @@ var DeeplineClient = class _DeeplineClient {
9017
9073
  params.set("rowMode", "all");
9018
9074
  }
9019
9075
  const result = await this.http.get(
9020
- `/api/v2/plays/${encodeURIComponent(input2.playName)}/sheet?${params.toString()}`
9076
+ `/api/v2/plays/${encodeURIComponent(input2.playName)}/sheet?${params.toString()}`,
9077
+ { signal: input2.signal }
9021
9078
  );
9022
9079
  const requestedRunId = input2.runId?.trim() || "";
9023
9080
  if (requestedRunId) {
@@ -20662,6 +20719,12 @@ var FIXTURE_BEHAVIOR_REPLAY_VERSION = 3;
20662
20719
  var MAX_FIXTURE_RESPONSE_DELAY_SAMPLES = 256;
20663
20720
  var MAX_FIXTURE_RESPONSE_DELAY_MS = 8 * 6e4;
20664
20721
  var MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH = 500;
20722
+ var MAX_FIXTURE_RETRY_AFTER_MS = 60 * 6e4;
20723
+ var FIXTURE_RATE_WINDOWS = [
20724
+ { field: "requestsPerSecond", name: "rps", windowMs: 1e3 },
20725
+ { field: "requestsPerMinute", name: "rpm", windowMs: 6e4 },
20726
+ { field: "requestsPerHour", name: "rph", windowMs: 36e5 }
20727
+ ];
20665
20728
  function validateFixtureBehavior(value) {
20666
20729
  if (value === void 0 || value === null) {
20667
20730
  return { ok: true, behavior: null };
@@ -20673,6 +20736,8 @@ function validateFixtureBehavior(value) {
20673
20736
  const supportedKeys = record3.version === FIXTURE_BEHAVIOR_RESPONSE_VERSION || record3.version === FIXTURE_BEHAVIOR_REPLAY_VERSION ? /* @__PURE__ */ new Set([
20674
20737
  "version",
20675
20738
  "responseSamples",
20739
+ "transient429",
20740
+ "sharedRateLimit",
20676
20741
  ...record3.version === FIXTURE_BEHAVIOR_REPLAY_VERSION ? ["replayBundle"] : []
20677
20742
  ]) : /* @__PURE__ */ new Set(["version", "responseDelaySamplesMs"]);
20678
20743
  const unknownKeys = Object.keys(record3).filter(
@@ -20769,7 +20834,7 @@ function validateFixtureBehavior(value) {
20769
20834
  }
20770
20835
  const error = sample.httpError;
20771
20836
  const errorUnknownKeys = Object.keys(error).filter(
20772
- (key) => key !== "status" && key !== "message"
20837
+ (key) => key !== "status" && key !== "message" && key !== "retryAfterMs" && key !== "retryAfterFormat" && key !== "failuresBeforeSuccess"
20773
20838
  );
20774
20839
  if (errorUnknownKeys.length > 0) {
20775
20840
  return {
@@ -20789,7 +20854,33 @@ function validateFixtureBehavior(value) {
20789
20854
  error: `fixtureBehavior.responseSamples[${index}].httpError.message must contain 1-${MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH} characters.`
20790
20855
  };
20791
20856
  }
20792
- httpError = { status: error.status, message: error.message.trim() };
20857
+ if (error.retryAfterMs !== void 0 && (error.status !== 429 || typeof error.retryAfterMs !== "number" || !Number.isSafeInteger(error.retryAfterMs) || error.retryAfterMs < 1 || error.retryAfterMs > MAX_FIXTURE_RETRY_AFTER_MS)) {
20858
+ return {
20859
+ ok: false,
20860
+ error: `fixtureBehavior.responseSamples[${index}].httpError.retryAfterMs requires status 429 and an integer from 1 through ${MAX_FIXTURE_RETRY_AFTER_MS}.`
20861
+ };
20862
+ }
20863
+ if (error.retryAfterFormat !== void 0 && (error.status !== 429 || error.retryAfterFormat !== "delta_seconds" && error.retryAfterFormat !== "http_date")) {
20864
+ return {
20865
+ ok: false,
20866
+ error: `fixtureBehavior.responseSamples[${index}].httpError.retryAfterFormat requires status 429 and delta_seconds or http_date.`
20867
+ };
20868
+ }
20869
+ if (error.failuresBeforeSuccess !== void 0 && (error.status !== 429 || typeof error.failuresBeforeSuccess !== "number" || !Number.isSafeInteger(error.failuresBeforeSuccess) || error.failuresBeforeSuccess < 1 || error.failuresBeforeSuccess > 20)) {
20870
+ return {
20871
+ ok: false,
20872
+ error: `fixtureBehavior.responseSamples[${index}].httpError.failuresBeforeSuccess requires status 429 and an integer from 1 through 20.`
20873
+ };
20874
+ }
20875
+ httpError = {
20876
+ status: error.status,
20877
+ message: error.message.trim(),
20878
+ ...error.retryAfterMs !== void 0 ? { retryAfterMs: error.retryAfterMs } : {},
20879
+ ...error.retryAfterFormat !== void 0 ? {
20880
+ retryAfterFormat: error.retryAfterFormat
20881
+ } : {},
20882
+ ...error.failuresBeforeSuccess !== void 0 ? { failuresBeforeSuccess: error.failuresBeforeSuccess } : {}
20883
+ };
20793
20884
  }
20794
20885
  samples2.push({
20795
20886
  delayMs: sample.delayMs,
@@ -20797,6 +20888,106 @@ function validateFixtureBehavior(value) {
20797
20888
  ...httpError ? { httpError } : {}
20798
20889
  });
20799
20890
  }
20891
+ let sharedRateLimit;
20892
+ if (record3.sharedRateLimit !== void 0) {
20893
+ const plan = record3.sharedRateLimit;
20894
+ if (!plan || typeof plan !== "object" || Array.isArray(plan)) {
20895
+ return {
20896
+ ok: false,
20897
+ error: "fixtureBehavior.sharedRateLimit must be an object."
20898
+ };
20899
+ }
20900
+ const planRecord = plan;
20901
+ const entries = Object.entries(planRecord);
20902
+ const boundaryAfterMs = planRecord.requestsPerHourBoundaryAfterMs;
20903
+ const quotaEntries = entries.filter(
20904
+ ([name]) => name !== "requestsPerHourBoundaryAfterMs"
20905
+ );
20906
+ if (quotaEntries.length === 0 || quotaEntries.some(
20907
+ ([name, limit]) => !FIXTURE_RATE_WINDOWS.some((window) => window.field === name) || typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 1
20908
+ )) {
20909
+ return {
20910
+ ok: false,
20911
+ error: "fixtureBehavior.sharedRateLimit requires positive integer requestsPerSecond, requestsPerMinute, or requestsPerHour quotas."
20912
+ };
20913
+ }
20914
+ if (boundaryAfterMs !== void 0 && (planRecord.requestsPerHour === void 0 || typeof boundaryAfterMs !== "number" || !Number.isSafeInteger(boundaryAfterMs) || boundaryAfterMs < 1e3 || boundaryAfterMs > 3e5)) {
20915
+ return {
20916
+ ok: false,
20917
+ error: "fixtureBehavior.sharedRateLimit.requestsPerHourBoundaryAfterMs requires requestsPerHour and an integer from 1000 through 300000."
20918
+ };
20919
+ }
20920
+ sharedRateLimit = Object.fromEntries(entries);
20921
+ }
20922
+ let transient429;
20923
+ if (record3.transient429 !== void 0) {
20924
+ const selected = record3.transient429;
20925
+ if (!selected || typeof selected !== "object" || Array.isArray(selected)) {
20926
+ return {
20927
+ ok: false,
20928
+ error: "fixtureBehavior.transient429 must be an object."
20929
+ };
20930
+ }
20931
+ const fault = selected;
20932
+ const unknownFaultKeys = Object.keys(fault).filter(
20933
+ (key) => key !== "everyNthCall" && key !== "retryAfterMs" && key !== "failuresBeforeSuccess" && key !== "when"
20934
+ );
20935
+ if (unknownFaultKeys.length > 0) {
20936
+ return {
20937
+ ok: false,
20938
+ error: `Unsupported fixtureBehavior.transient429 field "${unknownFaultKeys[0]}".`
20939
+ };
20940
+ }
20941
+ for (const [name, value2, max] of [
20942
+ ["everyNthCall", fault.everyNthCall, 1e4],
20943
+ ["retryAfterMs", fault.retryAfterMs, MAX_FIXTURE_RETRY_AFTER_MS],
20944
+ ["failuresBeforeSuccess", fault.failuresBeforeSuccess, 20]
20945
+ ]) {
20946
+ if (typeof value2 !== "number" || !Number.isSafeInteger(value2) || value2 < 1 || value2 > max) {
20947
+ return {
20948
+ ok: false,
20949
+ error: `fixtureBehavior.transient429.${name} must be an integer from 1 through ${max}.`
20950
+ };
20951
+ }
20952
+ }
20953
+ let when;
20954
+ if (fault.when !== void 0) {
20955
+ if (!fault.when || typeof fault.when !== "object" || Array.isArray(fault.when)) {
20956
+ return {
20957
+ ok: false,
20958
+ error: "fixtureBehavior.transient429.when must be an object."
20959
+ };
20960
+ }
20961
+ const condition = fault.when;
20962
+ const unknownConditionKeys = Object.keys(condition).filter(
20963
+ (key) => key !== "provider" && key !== "operation"
20964
+ );
20965
+ if (unknownConditionKeys.length > 0) {
20966
+ return {
20967
+ ok: false,
20968
+ error: `Unsupported fixtureBehavior.transient429.when field "${unknownConditionKeys[0]}".`
20969
+ };
20970
+ }
20971
+ const provider = normalizeFixtureScopeValue(condition.provider, 100);
20972
+ const operation = normalizeFixtureScopeValue(condition.operation, 200);
20973
+ if (provider.error || operation.error || !provider.value && !operation.value) {
20974
+ return {
20975
+ ok: false,
20976
+ error: "fixtureBehavior.transient429.when must select a valid provider or operation."
20977
+ };
20978
+ }
20979
+ when = {
20980
+ ...provider.value ? { provider: provider.value } : {},
20981
+ ...operation.value ? { operation: operation.value } : {}
20982
+ };
20983
+ }
20984
+ transient429 = {
20985
+ everyNthCall: fault.everyNthCall,
20986
+ retryAfterMs: fault.retryAfterMs,
20987
+ failuresBeforeSuccess: fault.failuresBeforeSuccess,
20988
+ ...when ? { when } : {}
20989
+ };
20990
+ }
20800
20991
  let replayBundle;
20801
20992
  if (record3.version === FIXTURE_BEHAVIOR_REPLAY_VERSION) {
20802
20993
  if (!record3.replayBundle || typeof record3.replayBundle !== "object" || Array.isArray(record3.replayBundle)) {
@@ -20859,6 +21050,8 @@ function validateFixtureBehavior(value) {
20859
21050
  behavior: {
20860
21051
  version: FIXTURE_BEHAVIOR_REPLAY_VERSION,
20861
21052
  responseSamples: samples2,
21053
+ ...transient429 ? { transient429 } : {},
21054
+ ...sharedRateLimit ? { sharedRateLimit } : {},
20862
21055
  replayBundle
20863
21056
  }
20864
21057
  };
@@ -20867,7 +21060,9 @@ function validateFixtureBehavior(value) {
20867
21060
  ok: true,
20868
21061
  behavior: {
20869
21062
  version: FIXTURE_BEHAVIOR_RESPONSE_VERSION,
20870
- responseSamples: samples2
21063
+ responseSamples: samples2,
21064
+ ...transient429 ? { transient429 } : {},
21065
+ ...sharedRateLimit ? { sharedRateLimit } : {}
20871
21066
  }
20872
21067
  };
20873
21068
  }
@@ -22751,43 +22946,52 @@ async function readRunIdFile(destination) {
22751
22946
  }
22752
22947
  return parsed;
22753
22948
  }
22754
- async function writePlayRunIdFile(destination, runId) {
22949
+ async function writePlayRunIdFile(destination, runId, options = {}) {
22755
22950
  const existing = await readRunIdFile(destination);
22756
22951
  if (existing) {
22757
- if (existing.runId === runId) return;
22758
- throw new Error(
22759
- `--run-id-file destination already records run ${existing.runId}; refusing to replace it with ${runId}: ${destination}`
22760
- );
22952
+ if (existing.runId !== runId) {
22953
+ throw new Error(
22954
+ `--run-id-file destination already records run ${existing.runId}; refusing to replace it with ${runId}: ${destination}`
22955
+ );
22956
+ }
22957
+ if (!options.launchConfirmed || existing.launchConfirmed) return;
22761
22958
  }
22762
22959
  const tempPath = runIdFileTempPath(destination);
22763
22960
  let handle = null;
22764
22961
  try {
22765
22962
  handle = await (0, import_promises9.open)(tempPath, "wx", 384);
22766
22963
  await handle.writeFile(
22767
- `${JSON.stringify({ version: 1, runId })}
22964
+ `${JSON.stringify({ version: 1, runId, ...options })}
22768
22965
  `,
22769
22966
  "utf8"
22770
22967
  );
22771
22968
  await handle.sync();
22772
22969
  await handle.close();
22773
22970
  handle = null;
22774
- try {
22775
- await (0, import_promises9.link)(tempPath, destination);
22776
- if (process.platform !== "win32") {
22777
- const directory = await (0, import_promises9.open)((0, import_node_path17.dirname)(destination), "r");
22778
- try {
22779
- await directory.sync();
22780
- } finally {
22781
- await directory.close();
22971
+ if (existing) {
22972
+ await (0, import_promises9.rename)(tempPath, destination);
22973
+ } else {
22974
+ try {
22975
+ await (0, import_promises9.link)(tempPath, destination);
22976
+ } catch (error) {
22977
+ if (error.code !== "EEXIST") throw error;
22978
+ const raced = await readRunIdFile(destination);
22979
+ if (raced?.runId !== runId) {
22980
+ throw new Error(
22981
+ `--run-id-file destination already records run ${raced?.runId ?? "<invalid>"}; refusing to replace it with ${runId}: ${destination}`
22982
+ );
22782
22983
  }
22984
+ if (!options.launchConfirmed || raced.launchConfirmed) return;
22985
+ await (0, import_promises9.rename)(tempPath, destination);
22986
+ }
22987
+ }
22988
+ if (process.platform !== "win32") {
22989
+ const directory = await (0, import_promises9.open)((0, import_node_path17.dirname)(destination), "r");
22990
+ try {
22991
+ await directory.sync();
22992
+ } finally {
22993
+ await directory.close();
22783
22994
  }
22784
- } catch (error) {
22785
- if (error.code !== "EEXIST") throw error;
22786
- const raced = await readRunIdFile(destination);
22787
- if (raced?.runId === runId) return;
22788
- throw new Error(
22789
- `--run-id-file destination already records run ${raced?.runId ?? "<invalid>"}; refusing to replace it with ${runId}: ${destination}`
22790
- );
22791
22995
  }
22792
22996
  } finally {
22793
22997
  await handle?.close();
@@ -23844,6 +24048,16 @@ function getRunIdFromLiveEvent(event) {
23844
24048
  function eventConfirmsPlayRunLaunch(event) {
23845
24049
  return event.type === "play.run.status" && getEventPayload(event).launchConfirmed === true;
23846
24050
  }
24051
+ function statusProvesPlayRunExecution(status) {
24052
+ return status === "waiting" || status === "completed";
24053
+ }
24054
+ function eventProvesPlayRunExecution(event) {
24055
+ if (statusProvesPlayRunExecution(getStatusFromLiveEvent(event))) return true;
24056
+ const payload = getEventPayload(event);
24057
+ return Boolean(getStringField(payload, "stepId")) && (event.type === "play.step.progress" || event.type === "play.step.status" && ["running", "completed", "failed", "skipped"].includes(
24058
+ getStringField(payload, "status") ?? ""
24059
+ ));
24060
+ }
23847
24061
  function getStatusFromLiveEvent(event) {
23848
24062
  if (event.type !== "play.run.status" && event.type !== "play.run.snapshot" && event.type !== "play.run.final_status") {
23849
24063
  return null;
@@ -24320,6 +24534,12 @@ async function waitForPlayCompletionByStream(input2) {
24320
24534
  const durableStatusProbeIntervalMs = input2.durableStatusProbeIntervalMs ?? PLAY_DURABLE_STATUS_PROBE_INTERVAL_MS;
24321
24535
  let nextDurableProbeAt = Date.now() + durableStatusProbeIntervalMs;
24322
24536
  let durableStatusProbe = null;
24537
+ let admissionConfirmed = false;
24538
+ const confirmAdmission = async () => {
24539
+ if (admissionConfirmed) return;
24540
+ admissionConfirmed = true;
24541
+ await input2.onRunAdmitted?.(input2.workflowId);
24542
+ };
24323
24543
  const withDashboardUrl = (status) => input2.dashboardUrl ? { ...status, dashboardUrl: input2.dashboardUrl } : status;
24324
24544
  const remainingWaitMs = () => input2.waitTimeoutMs === null ? null : input2.waitTimeoutMs - (Date.now() - input2.startedAt);
24325
24545
  const fetchTerminalStatus = async () => {
@@ -24336,6 +24556,9 @@ async function waitForPlayCompletionByStream(input2) {
24336
24556
  }
24337
24557
  throw error;
24338
24558
  }
24559
+ if (refreshed.runId === input2.workflowId && statusProvesPlayRunExecution(refreshed.status)) {
24560
+ await confirmAdmission();
24561
+ }
24339
24562
  return TERMINAL_PLAY_STATUSES2.has(refreshed.status) ? withDashboardUrl(refreshed) : null;
24340
24563
  })();
24341
24564
  durableStatusProbe = probe;
@@ -24347,6 +24570,9 @@ async function waitForPlayCompletionByStream(input2) {
24347
24570
  };
24348
24571
  const handleLiveEvent = async (event, probeDurableStatus) => {
24349
24572
  assertPlayWaitNotTimedOut({ ...input2, lastPhase });
24573
+ if (getRunIdFromLiveEvent(event) === input2.workflowId && (eventConfirmsPlayRunLaunch(event) || eventProvesPlayRunExecution(event))) {
24574
+ await confirmAdmission();
24575
+ }
24350
24576
  const now = Date.now();
24351
24577
  if (now >= nextDurableProbeAt) {
24352
24578
  nextDurableProbeAt = now + durableStatusProbeIntervalMs;
@@ -24596,6 +24822,11 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
24596
24822
  let eventCount = 0;
24597
24823
  let firstRunIdMs = null;
24598
24824
  let launchConfirmed = false;
24825
+ const confirmAdmission = async (runId) => {
24826
+ if (launchConfirmed || !runId || runId === "pending") return;
24827
+ launchConfirmed = true;
24828
+ await input2.onRunAdmitted?.(runId);
24829
+ };
24599
24830
  let lastPhase = null;
24600
24831
  const timeout = input2.waitTimeoutMs === null ? null : setTimeout(
24601
24832
  () => {
@@ -24619,6 +24850,9 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
24619
24850
  }
24620
24851
  throw error;
24621
24852
  }
24853
+ if (refreshed.runId === lastKnownWorkflowId && statusProvesPlayRunExecution(refreshed.status)) {
24854
+ await confirmAdmission(lastKnownWorkflowId);
24855
+ }
24622
24856
  return TERMINAL_PLAY_STATUSES2.has(refreshed.status) ? {
24623
24857
  ...refreshed,
24624
24858
  dashboardUrl,
@@ -24646,8 +24880,8 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
24646
24880
  await input2.onRunStarted?.(eventRunId);
24647
24881
  }
24648
24882
  }
24649
- if (eventConfirmsPlayRunLaunch(event) && eventRunId === lastKnownWorkflowId) {
24650
- launchConfirmed = true;
24883
+ if (!launchConfirmed && eventConfirmsPlayRunLaunch(event) && eventRunId === lastKnownWorkflowId) {
24884
+ await confirmAdmission(lastKnownWorkflowId);
24651
24885
  }
24652
24886
  const workflowId = lastKnownWorkflowId || "pending";
24653
24887
  if (workflowId !== "pending" && !emittedDashboardUrl) {
@@ -24762,6 +24996,9 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
24762
24996
  firstRunIdMs,
24763
24997
  lastPhase
24764
24998
  });
24999
+ if (statusProvesPlayRunExecution(canonicalTerminal.status)) {
25000
+ await confirmAdmission(lastKnownWorkflowId);
25001
+ }
24765
25002
  return canonicalTerminal;
24766
25003
  }
24767
25004
  if (lastKnownWorkflowId && launchConfirmed) {
@@ -24789,7 +25026,8 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
24789
25026
  waitTimeoutMs: input2.waitTimeoutMs,
24790
25027
  startedAt,
24791
25028
  state,
24792
- progress: input2.progress
25029
+ progress: input2.progress,
25030
+ onRunAdmitted: confirmAdmission
24793
25031
  });
24794
25032
  }
24795
25033
  }
@@ -24849,7 +25087,8 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
24849
25087
  waitTimeoutMs: input2.waitTimeoutMs,
24850
25088
  startedAt,
24851
25089
  state,
24852
- progress: input2.progress
25090
+ progress: input2.progress,
25091
+ onRunAdmitted: confirmAdmission
24853
25092
  });
24854
25093
  }
24855
25094
  if (!lastKnownWorkflowId && isTransientPlayStreamError(error)) {
@@ -24902,7 +25141,8 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
24902
25141
  waitTimeoutMs: input2.waitTimeoutMs,
24903
25142
  startedAt,
24904
25143
  state,
24905
- progress: input2.progress
25144
+ progress: input2.progress,
25145
+ onRunAdmitted: confirmAdmission
24906
25146
  });
24907
25147
  }
24908
25148
  const phaseSuffix = lastPhase && lastPhase.trim() ? ` (last observed phase: ${lastPhase.trim()})` : "";
@@ -25845,8 +26085,15 @@ function formatPackageDatasetActionLines(packaged, options) {
25845
26085
  const isReturned = datasetId && returned.datasetIds.has(datasetId) || returned.paths.has(path);
25846
26086
  const category = isReturned ? "returned" : dataset.recovered === true ? "recovered" : "persisted";
25847
26087
  const rowCount = typeof dataset.rowCount === "number" && Number.isFinite(dataset.rowCount) ? Math.max(0, Math.trunc(dataset.rowCount)) : null;
26088
+ const lifecycle = readRecord(dataset.lifecycle);
26089
+ const countUnconfirmed = lifecycle?.complete === false;
26090
+ const persistedLowerBound = typeof lifecycle?.persistedRowsAtLeast === "number" && Number.isFinite(lifecycle.persistedRowsAtLeast) ? Math.max(0, Math.trunc(lifecycle.persistedRowsAtLeast)) : 0;
25848
26091
  const actions = readRecord(dataset.actions) ?? {};
25849
- if (includeSummary) {
26092
+ if (countUnconfirmed) {
26093
+ lines.push(
26094
+ ` dataset ${path}: row count unconfirmed (at least ${formatInteger(persistedLowerBound)} persisted); export this run to inspect saved rows`
26095
+ );
26096
+ } else if (includeSummary) {
25850
26097
  if (category === "recovered" && Object.keys(actions).length === 0) {
25851
26098
  lines.push(
25852
26099
  ` dataset ${path}: available, ${rowCount === null ? "persisted" : formatInteger(rowCount)} rows persisted; re-running reuses completed work`
@@ -28194,7 +28441,8 @@ async function handleFileBackedRun(options, hooks) {
28194
28441
  waitTimeoutMs: options.waitTimeoutMs,
28195
28442
  open: options.open,
28196
28443
  progress,
28197
- onRunStarted: hooks?.onRunStarted
28444
+ onRunStarted: hooks?.onRunStarted,
28445
+ onRunAdmitted: hooks?.onRunAdmitted
28198
28446
  }).catch(async (error) => {
28199
28447
  throw await normalizePlayStartError(client2, error, playName);
28200
28448
  })
@@ -28235,6 +28483,7 @@ async function handleFileBackedRun(options, hooks) {
28235
28483
  })
28236
28484
  );
28237
28485
  await hooks?.onRunStarted?.(started.workflowId);
28486
+ await hooks?.onRunAdmitted?.(started.workflowId);
28238
28487
  const resolvedDashboardUrl = buildPlayDashboardUrl(client2.baseUrl, playName);
28239
28488
  openPlayDashboard(resolvedDashboardUrl, options.open);
28240
28489
  progress.phase("started run");
@@ -28377,7 +28626,8 @@ async function handleNamedRun(options, hooks) {
28377
28626
  waitTimeoutMs: options.waitTimeoutMs,
28378
28627
  open: options.open,
28379
28628
  progress,
28380
- onRunStarted: hooks?.onRunStarted
28629
+ onRunStarted: hooks?.onRunStarted,
28630
+ onRunAdmitted: hooks?.onRunAdmitted
28381
28631
  }).catch(async (error) => {
28382
28632
  throw await normalizePlayStartError(client2, error, playName, {
28383
28633
  suggestMissingPlay: true
@@ -28423,6 +28673,7 @@ async function handleNamedRun(options, hooks) {
28423
28673
  })
28424
28674
  );
28425
28675
  await hooks?.onRunStarted?.(started.workflowId);
28676
+ await hooks?.onRunAdmitted?.(started.workflowId);
28426
28677
  const resolvedDashboardUrl = buildPlayDashboardUrl(client2.baseUrl, playName);
28427
28678
  openPlayDashboard(resolvedDashboardUrl, options.open);
28428
28679
  progress.phase("started run");
@@ -28460,6 +28711,19 @@ async function handlePlayRun(args, hooks) {
28460
28711
  );
28461
28712
  }
28462
28713
  await hooks?.onRunStarted?.(runId);
28714
+ },
28715
+ onRunAdmitted: async (runId) => {
28716
+ try {
28717
+ await writePlayRunIdFile(options.runIdFile, runId, {
28718
+ launchConfirmed: true
28719
+ });
28720
+ } catch (error) {
28721
+ throw new Error(
28722
+ `Run ${runId} was admitted, but its confirmation could not be persisted to ${options.runIdFile}: ${error instanceof Error ? error.message : String(error)}. Inspect it with 'deepline runs get ${runId} --json'; do not submit a replacement run.`,
28723
+ { cause: error }
28724
+ );
28725
+ }
28726
+ await hooks?.onRunAdmitted?.(runId);
28463
28727
  }
28464
28728
  } : hooks;
28465
28729
  if (options.target.kind === "file") {
@@ -28532,6 +28796,36 @@ async function handleRunGet(args) {
28532
28796
  });
28533
28797
  return 0;
28534
28798
  }
28799
+ async function handleRunLifecycleStatus(args) {
28800
+ const usage = "Usage: deepline runs status <run-id> [--json]";
28801
+ let runId;
28802
+ try {
28803
+ runId = parseRunIdPositional(args, usage);
28804
+ } catch (error) {
28805
+ console.error(error instanceof Error ? error.message : usage);
28806
+ return 1;
28807
+ }
28808
+ const status = await new DeeplineClient().runs.status(runId);
28809
+ if (argsWantJson(args)) {
28810
+ printCommandEnvelope(status, { json: true });
28811
+ return 0;
28812
+ }
28813
+ if (!status.available) {
28814
+ console.log(`Scalar lifecycle status is not available for run ${runId}.`);
28815
+ return 0;
28816
+ }
28817
+ const formatCount = (count) => count.value === null ? "unknown" : `${count.value}${count.confirmed ? "" : " (unconfirmed)"}`;
28818
+ console.log(`${status.lifecycle.status} \xB7 ${runId}`);
28819
+ console.log(
28820
+ `Map-step work (summed, not unique rows): completed ${formatCount(status.mapStepWork.completed)} / ${formatCount(status.mapStepWork.total)}, failed ${formatCount(status.mapStepWork.failed)}`
28821
+ );
28822
+ if (status.lifecycle.status === "waiting") {
28823
+ console.log(
28824
+ status.wait.confirmed && status.wait.kind ? `Waiting: ${status.wait.kind}` : "Waiting reason: unknown"
28825
+ );
28826
+ }
28827
+ return 0;
28828
+ }
28535
28829
  async function handleRunsRerun(args) {
28536
28830
  const usage = "Usage: deepline runs rerun <run-id> [--idempotency-key <key>] [--json]";
28537
28831
  let runId;
@@ -29004,7 +29298,12 @@ async function handleRunStop(args) {
29004
29298
  }
29005
29299
  const client2 = new DeeplineClient();
29006
29300
  const result = await client2.runs.stop(runId, { reason });
29007
- if (result.cancelling === true) {
29301
+ if (result.cancelling === true && result.hitlCancellationPending !== true) {
29302
+ const lines2 = [
29303
+ `Cancellation queued for ${result.runId || runId}; sandbox cleanup is pending`,
29304
+ ...result.hitlCancelledCount > 0 ? [`cancelled HITL waits: ${result.hitlCancelledCount}`] : [],
29305
+ `inspect: deepline runs get ${runId} --full --json`
29306
+ ];
29008
29307
  printCommandEnvelope(
29009
29308
  {
29010
29309
  ...result,
@@ -29012,10 +29311,7 @@ async function handleRunStop(args) {
29012
29311
  sections: [
29013
29312
  {
29014
29313
  title: "run stop",
29015
- lines: [
29016
- `Cancellation queued for ${result.runId || runId}; sandbox cleanup is pending`,
29017
- `inspect: deepline runs get ${runId} --full --json`
29018
- ]
29314
+ lines: lines2
29019
29315
  }
29020
29316
  ]
29021
29317
  }
@@ -30928,6 +31224,12 @@ Examples:
30928
31224
  deepline runs export play/my-play/run/20260501t000000-000 --out output.csv
30929
31225
  `
30930
31226
  );
31227
+ runs.command("status <runId>").description("Read bounded lifecycle and confirmed work counts for a run.").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (runId, options) => {
31228
+ process.exitCode = await handleRunLifecycleStatus([
31229
+ runId,
31230
+ ...options.json ? ["--json"] : []
31231
+ ]);
31232
+ });
30931
31233
  runs.command("get <runId>").description(
30932
31234
  "Get status, progress, result, errors, and recovery metadata for a play run."
30933
31235
  ).addHelpText(
@@ -50500,8 +50802,8 @@ function readPendingToolExecution(path) {
50500
50802
  }
50501
50803
  return saved.idempotencyKey;
50502
50804
  }
50503
- async function completePendingToolExecution(path) {
50504
- if (!path || !(0, import_node_fs26.existsSync)(path)) return;
50805
+ async function completePendingToolExecution(path, idempotencyKey) {
50806
+ if (!path || !idempotencyKey || !(0, import_node_fs26.existsSync)(path)) return;
50505
50807
  await new Promise((resolve22, reject) => {
50506
50808
  const onError = (error) => {
50507
50809
  process.stdout.off("error", onError);
@@ -50517,7 +50819,12 @@ async function completePendingToolExecution(path) {
50517
50819
  }
50518
50820
  });
50519
50821
  });
50520
- if ((0, import_node_fs26.existsSync)(path)) (0, import_node_fs26.unlinkSync)(path);
50822
+ try {
50823
+ if (readPendingToolExecution(path) !== idempotencyKey) return;
50824
+ (0, import_node_fs26.unlinkSync)(path);
50825
+ } catch (error) {
50826
+ if (error.code !== "ENOENT") throw error;
50827
+ }
50521
50828
  }
50522
50829
  function safeFileStem(value) {
50523
50830
  return value.trim().replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "tool";
@@ -50865,7 +51172,7 @@ async function executeTool(args) {
50865
51172
  const executeExitCode = toolExecuteExitCode(baseEnvelope);
50866
51173
  if (!parsed.outPath && (parsed.outputFormat === "json" || parsed.outputFormat === "auto" && shouldEmitJson())) {
50867
51174
  printCommandEnvelope(baseEnvelope, { json: true });
50868
- await completePendingToolExecution(pendingRecoveryPath);
51175
+ await completePendingToolExecution(pendingRecoveryPath, idempotencyKey);
50869
51176
  return executeExitCode;
50870
51177
  }
50871
51178
  if (parsed.outputFormat === "json_file") {
@@ -50883,7 +51190,7 @@ async function executeTool(args) {
50883
51190
  },
50884
51191
  { json: true }
50885
51192
  );
50886
- await completePendingToolExecution(pendingRecoveryPath);
51193
+ await completePendingToolExecution(pendingRecoveryPath, idempotencyKey);
50887
51194
  return executeExitCode;
50888
51195
  }
50889
51196
  if (!listConversion) {
@@ -50912,11 +51219,11 @@ async function executeTool(args) {
50912
51219
  },
50913
51220
  { json: parsed.outputFormat === "csv_file" || shouldEmitJson() }
50914
51221
  );
50915
- await completePendingToolExecution(pendingRecoveryPath);
51222
+ await completePendingToolExecution(pendingRecoveryPath, idempotencyKey);
50916
51223
  return executeExitCode;
50917
51224
  }
50918
51225
  printCommandEnvelope(baseEnvelope, { json: false });
50919
- await completePendingToolExecution(pendingRecoveryPath);
51226
+ await completePendingToolExecution(pendingRecoveryPath, idempotencyKey);
50920
51227
  return executeExitCode;
50921
51228
  }
50922
51229
  const rowOutput = projectRowOutput(listConversion);
@@ -50939,7 +51246,7 @@ async function executeTool(args) {
50939
51246
  });
50940
51247
  if (parsed.outputFormat === "csv_file") {
50941
51248
  printCommandEnvelope(materializedEnvelope, { json: true });
50942
- await completePendingToolExecution(pendingRecoveryPath);
51249
+ await completePendingToolExecution(pendingRecoveryPath, idempotencyKey);
50943
51250
  return executeExitCode;
50944
51251
  }
50945
51252
  if (parsed.outPath) {
@@ -50948,7 +51255,7 @@ async function executeTool(args) {
50948
51255
  text: `Wrote ${csv.rowCount} row(s) to ${csv.path}
50949
51256
  `
50950
51257
  });
50951
- await completePendingToolExecution(pendingRecoveryPath);
51258
+ await completePendingToolExecution(pendingRecoveryPath, idempotencyKey);
50952
51259
  return executeExitCode;
50953
51260
  }
50954
51261
  if (parsed.noPreview) {
@@ -50963,11 +51270,11 @@ async function executeTool(args) {
50963
51270
  { json: shouldEmitJson(), text: `${csv.path}
50964
51271
  ` }
50965
51272
  );
50966
- await completePendingToolExecution(pendingRecoveryPath);
51273
+ await completePendingToolExecution(pendingRecoveryPath, idempotencyKey);
50967
51274
  return executeExitCode;
50968
51275
  }
50969
51276
  printCommandEnvelope(materializedEnvelope, { json: false });
50970
- await completePendingToolExecution(pendingRecoveryPath);
51277
+ await completePendingToolExecution(pendingRecoveryPath, idempotencyKey);
50971
51278
  return executeExitCode;
50972
51279
  }
50973
51280