deepline 0.3.121 → 0.3.123

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.
@@ -152,6 +152,43 @@ export function buildAsyncPlayRunIdentity(input: {
152
152
  };
153
153
  }
154
154
 
155
+ /**
156
+ * Build the retry-stable identity for one caller-keyed `runs rerun` request.
157
+ *
158
+ * The digest is namespaced (`-rerun-`) so a keyed replay can never collide
159
+ * with an ordinary entropy-suffixed run, an async child (`-async-`), or a
160
+ * differently keyed replay of the same source run. Everything else about a
161
+ * replay (input, revision, artifact, lane) is fixed by the source run, so the
162
+ * same `sourceRunId + idempotencyKey` always names the same intended Run and
163
+ * the existing run-ledger admission fence resolves repeats to it.
164
+ */
165
+ export function buildRerunPlayRunIdentity(input: {
166
+ playName: string;
167
+ sourceRunId: string;
168
+ idempotencyKey: string;
169
+ /** Fallback ordering timestamp when the source id carries no canonical one. */
170
+ sourceStartedAtMs?: number | null;
171
+ }): PlayRunIdentity {
172
+ const digest = sha256Hex(
173
+ `rerun\0${input.sourceRunId}\0${input.idempotencyKey}`,
174
+ ).slice(0, 20);
175
+ const startedAt =
176
+ canonicalPlayRunStartedAtMs(input.sourceRunId) ??
177
+ (typeof input.sourceStartedAtMs === 'number' &&
178
+ Number.isFinite(input.sourceStartedAtMs)
179
+ ? input.sourceStartedAtMs
180
+ : 0);
181
+ const base = buildPlayWorkflowId(input.playName, new Date(startedAt), {
182
+ unique: false,
183
+ });
184
+ const workflowId = `${base}-rerun-${digest}`;
185
+ return {
186
+ workflowId,
187
+ runId: workflowId,
188
+ workflowFamilyKey: buildPlayWorkflowFamilyKey(input.playName),
189
+ };
190
+ }
191
+
155
192
  /** Immutable authored request identity, separate from the resolved revision. */
156
193
  export function buildAsyncPlayRequestFingerprint(input: {
157
194
  key: string;
package/dist/cli/index.js CHANGED
@@ -3068,7 +3068,7 @@ var SDK_RELEASE = {
3068
3068
  // getters keep their established compatibility behavior.
3069
3069
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3070
3070
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3071
- version: "0.3.121",
3071
+ version: "0.3.123",
3072
3072
  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.",
3073
3073
  packageCapabilities: {
3074
3074
  updatePreferences: 1
@@ -6242,6 +6242,11 @@ async function* observeRunEvents(options) {
6242
6242
  }
6243
6243
  }
6244
6244
 
6245
+ // src/types.ts
6246
+ function isStopAllPlayRunsDryRunResult(result) {
6247
+ return "dryRun" in result && result.dryRun === true;
6248
+ }
6249
+
6245
6250
  // ../integration-contracts/theirstack/execution-policy.ts
6246
6251
  var THEIRSTACK_SLOW_JOB_SEARCH_PROVIDER_TIMEOUT_MS = 135e3;
6247
6252
  var THEIRSTACK_SLOW_JOB_SEARCH_CLIENT_TIMEOUT_MS = 6e4 + THEIRSTACK_SLOW_JOB_SEARCH_PROVIDER_TIMEOUT_MS + 15e3;
@@ -6946,10 +6951,10 @@ var DeeplineClient = class {
6946
6951
  tail: (runId, options2) => this.tailRun(runId, options2),
6947
6952
  logs: (runId, options2) => this.getRunLogs(runId, options2),
6948
6953
  input: (runId) => this.getRunInput(runId),
6949
- rerun: (runId) => this.rerun(runId),
6954
+ rerun: (runId, options2) => this.rerun(runId, options2),
6950
6955
  exportDatasetRows: (input2) => this.getPlaySheetRows(input2),
6951
6956
  stop: (runId, options2) => this.stopRun(runId, options2),
6952
- stopAll: (options2) => this.stopAllRuns(options2)
6957
+ stopAll: ((options2) => this.stopAllRuns(options2))
6953
6958
  };
6954
6959
  this.db = {
6955
6960
  query: (input2) => this.queryDb(input2)
@@ -8465,11 +8470,28 @@ var DeeplineClient = class {
8465
8470
  async getRunInput(runId) {
8466
8471
  return this.http.get(`/api/v2/runs/${encodeURIComponent(runId)}/input`);
8467
8472
  }
8468
- /** Start a fresh run from a prior run's retained input and pinned revision. */
8469
- async rerun(runId) {
8473
+ /**
8474
+ * Start a fresh run from a prior run's retained input and pinned revision.
8475
+ *
8476
+ * Every invocation sends an `Idempotency-Key`. When the caller does not
8477
+ * supply one, a fresh key is generated once per call so the transport's own
8478
+ * network retries converge on a single rerun. Pass the same explicit key
8479
+ * across separate invocations to repeat an ambiguous result safely; a
8480
+ * different key intentionally starts another rerun.
8481
+ */
8482
+ async rerun(runId, options) {
8483
+ const idempotencyKey = options?.idempotencyKey?.trim();
8484
+ if (options?.idempotencyKey !== void 0 && !idempotencyKey) {
8485
+ throw new DeeplineError(
8486
+ "runs.rerun idempotencyKey must not be empty.",
8487
+ void 0,
8488
+ "INVALID_IDEMPOTENCY_KEY"
8489
+ );
8490
+ }
8470
8491
  return this.http.post(
8471
8492
  `/api/v2/runs/${encodeURIComponent(runId)}/rerun`,
8472
- {}
8493
+ {},
8494
+ { "Idempotency-Key": idempotencyKey || globalThis.crypto.randomUUID() }
8473
8495
  );
8474
8496
  }
8475
8497
  /**
@@ -8614,11 +8636,13 @@ var DeeplineClient = class {
8614
8636
  );
8615
8637
  }
8616
8638
  /**
8617
- * Stop every active run visible to the current workspace.
8639
+ * Stop every active run visible to the current workspace, or with
8640
+ * `dryRun: true` enumerate the exact candidates without cancelling anything.
8618
8641
  *
8619
8642
  * This is the SDK equivalent of:
8620
8643
  *
8621
8644
  * ```bash
8645
+ * deepline runs stop-all --dry-run --json
8622
8646
  * deepline runs stop-all --reason "stale lock" --json
8623
8647
  * ```
8624
8648
  *
@@ -8626,10 +8650,22 @@ var DeeplineClient = class {
8626
8650
  * need to clear the workspace run-slot state without knowing each run id.
8627
8651
  */
8628
8652
  async stopAllRuns(options) {
8629
- return this.http.post(
8653
+ const dryRun = options?.dryRun === true;
8654
+ const result = await this.http.post(
8630
8655
  "/api/v2/runs/stop-all",
8631
- options?.reason ? { reason: options.reason } : {}
8656
+ {
8657
+ ...options?.reason ? { reason: options.reason } : {},
8658
+ ...dryRun ? { dryRun: true } : {}
8659
+ }
8632
8660
  );
8661
+ if (dryRun && !isStopAllPlayRunsDryRunResult(result)) {
8662
+ throw new DeeplineError(
8663
+ "This Deepline server does not support runs stop-all --dry-run and performed a real stop-all. Update the server before using --dry-run.",
8664
+ void 0,
8665
+ "STOP_ALL_DRY_RUN_UNSUPPORTED"
8666
+ );
8667
+ }
8668
+ return result;
8633
8669
  }
8634
8670
  /**
8635
8671
  * List one page of callable plays; grep retains exhaustive matching results.
@@ -24441,6 +24477,77 @@ function authoredPackageStackFrames(packaged) {
24441
24477
  (line) => !line.includes("/virtual/deepline-plays/") && !line.includes("/apps/play-runner/") && !line.includes("/shared_libs/play-runtime/") && !line.includes("node:internal")
24442
24478
  ).slice(0, 5);
24443
24479
  }
24480
+ function activityTargetLabel(target) {
24481
+ const kind = typeof target.kind === "string" ? target.kind : "target";
24482
+ const label = typeof target.label === "string" ? target.label.trim() : "";
24483
+ if (label) return `${kind} ${label}`;
24484
+ switch (kind) {
24485
+ case "provider":
24486
+ return `provider ${String(target.provider ?? "?")} ${String(target.operation ?? "?")}`;
24487
+ case "dataset":
24488
+ return `dataset ${String(target.tableNamespace ?? "?")} (${String(target.operation ?? "?")})`;
24489
+ case "lease":
24490
+ return `${String(target.resourceKind ?? "resource")} lease ${String(target.resourceLabel ?? "?")}`;
24491
+ case "runner":
24492
+ return `runner ${String(target.backend ?? "?")}`;
24493
+ case "step":
24494
+ return `step ${String(target.stepId ?? "?")}`;
24495
+ case "capacity":
24496
+ return `${String(target.pool ?? "?")} capacity`;
24497
+ default:
24498
+ return kind;
24499
+ }
24500
+ }
24501
+ function activityIsoTimestamp(value) {
24502
+ return typeof value === "number" || typeof value === "string" ? isoTimestamp(value) : null;
24503
+ }
24504
+ function buildRunActivityTextLines(run) {
24505
+ const activity = readRecord(run.activity);
24506
+ const observation = readRecord(activity?.observation);
24507
+ const state = readRecord(observation?.state);
24508
+ const target = readRecord(observation?.target);
24509
+ if (!activity || !observation || !state) return [];
24510
+ const kind = typeof state.kind === "string" ? state.kind : "unknown";
24511
+ if (kind === "completed") return [];
24512
+ const summary = typeof activity.summary === "string" && activity.summary.trim() ? activity.summary.trim() : null;
24513
+ const unclassified = activity.classified === false;
24514
+ const lines = [];
24515
+ lines.push(
24516
+ ` activity: ${kind}${summary ? ` \u2014 ${summary}` : ""}${unclassified ? " (unclassified evidence; may be stale)" : ""}`
24517
+ );
24518
+ if (target) lines.push(` target: ${activityTargetLabel(target)}`);
24519
+ if (typeof observation.stepId === "string" && observation.stepId.trim()) {
24520
+ lines.push(` step: ${observation.stepId.trim()}`);
24521
+ }
24522
+ const reason = readRecord(state.reason);
24523
+ if (kind === "retrying") {
24524
+ const attempt = typeof state.attempt === "number" ? ` (attempt ${state.attempt})` : "";
24525
+ const why = typeof state.reason === "string" ? ` after ${state.reason}` : "";
24526
+ lines.push(
24527
+ ` next retry: ${activityIsoTimestamp(state.retryAt) ?? "unknown"}${why}${attempt}`
24528
+ );
24529
+ } else if (kind === "scheduled") {
24530
+ lines.push(
24531
+ ` resumes: ${activityIsoTimestamp(state.resumeAt) ?? "unknown"}`
24532
+ );
24533
+ } else if (kind === "waiting" && reason) {
24534
+ if (reason.kind === "external_event") {
24535
+ const deadline = activityIsoTimestamp(reason.deadlineAt);
24536
+ if (deadline) lines.push(` deadline: ${deadline}`);
24537
+ if (typeof reason.eventKey === "string" && reason.eventKey.trim()) {
24538
+ lines.push(` event key: ${reason.eventKey.trim()}`);
24539
+ }
24540
+ } else if (reason.kind === "lease") {
24541
+ const expires = activityIsoTimestamp(reason.expiresAt);
24542
+ if (expires) lines.push(` lease expires: ${expires}`);
24543
+ } else if (reason.kind === "unknown") {
24544
+ lines.push(" wait reason: unknown (legacy producer)");
24545
+ }
24546
+ }
24547
+ const observedAt = activityIsoTimestamp(observation.observedAt);
24548
+ if (observedAt) lines.push(` observed: ${observedAt}`);
24549
+ return lines;
24550
+ }
24444
24551
  function buildRunPackageTextLines(packaged) {
24445
24552
  const run = readRunPackageRun(packaged);
24446
24553
  const runId = typeof run.id === "string" ? run.id : "unknown";
@@ -24491,6 +24598,7 @@ function buildRunPackageTextLines(packaged) {
24491
24598
  if (playName) {
24492
24599
  lines.push(` play: ${playName}`);
24493
24600
  }
24601
+ lines.push(...buildRunActivityTextLines(run));
24494
24602
  lines.push(...formatPackageValueOutputLines(packaged));
24495
24603
  lines.push(...formatPackageDatasetActionLines(packaged));
24496
24604
  const next = packaged.next && typeof packaged.next === "object" && !Array.isArray(packaged.next) ? packaged.next : {};
@@ -24634,6 +24742,11 @@ function writePlayResult(status, jsonOutput, options) {
24634
24742
  const displayError = formatPlayErrorForDisplay(status, progressError) ?? progressError;
24635
24743
  lines.push(` error: ${truncateErrorForDisplay(displayError, runId)}`);
24636
24744
  }
24745
+ lines.push(
24746
+ ...buildRunActivityTextLines(
24747
+ readRecord(status.run) ?? {}
24748
+ )
24749
+ );
24637
24750
  if (status.status === "failed") {
24638
24751
  const failedLogEntries = status.failedLogs?.entries ?? [];
24639
24752
  if (failedLogEntries.length > 0) {
@@ -26804,7 +26917,7 @@ function parseRunIdPositional(args, usage) {
26804
26917
  continue;
26805
26918
  }
26806
26919
  if (arg.startsWith("--log-level=")) continue;
26807
- if ((arg === "--out" || arg === "--reason" || arg === "--dataset") && args[index + 1]) {
26920
+ if ((arg === "--out" || arg === "--reason" || arg === "--dataset" || arg === "--idempotency-key") && args[index + 1]) {
26808
26921
  index += 1;
26809
26922
  continue;
26810
26923
  }
@@ -26839,7 +26952,7 @@ async function handleRunGet(args) {
26839
26952
  return 0;
26840
26953
  }
26841
26954
  async function handleRunsRerun(args) {
26842
- const usage = "Usage: deepline runs rerun <run-id> [--json]";
26955
+ const usage = "Usage: deepline runs rerun <run-id> [--idempotency-key <key>] [--json]";
26843
26956
  let runId;
26844
26957
  try {
26845
26958
  runId = parseRunIdPositional(args, usage);
@@ -26847,16 +26960,33 @@ async function handleRunsRerun(args) {
26847
26960
  console.error(error instanceof Error ? error.message : usage);
26848
26961
  return 1;
26849
26962
  }
26850
- const result = await new DeeplineClient().runs.rerun(runId);
26963
+ let idempotencyKey;
26964
+ for (let index = 0; index < args.length; index += 1) {
26965
+ if (args[index] === "--idempotency-key") {
26966
+ const value = args[index + 1];
26967
+ if (!value || value.startsWith("--") || !value.trim()) {
26968
+ console.error("--idempotency-key requires a non-empty value.");
26969
+ return 1;
26970
+ }
26971
+ idempotencyKey = value.trim();
26972
+ index += 1;
26973
+ }
26974
+ }
26975
+ const result = await new DeeplineClient().runs.rerun(runId, {
26976
+ ...idempotencyKey ? { idempotencyKey } : {}
26977
+ });
26851
26978
  if (argsWantJson(args)) {
26852
26979
  printCommandEnvelope(result, { json: true });
26853
26980
  } else {
26854
- console.log(`
26855
- Rerun started from ${runId}.`);
26981
+ console.log(
26982
+ result.deduplicated === true ? `
26983
+ Rerun already exists for ${runId} (matched by idempotency key): ${result.runId}` : `
26984
+ Rerun started from ${runId}.`
26985
+ );
26856
26986
  console.log(` inspect: deepline runs get ${result.runId} --json`);
26857
26987
  console.log(` original input: deepline runs get ${result.runId} --input`);
26858
26988
  console.log(
26859
- " This is a fresh run using the original input and pinned revision. Normal tool receipt reuse still applies."
26989
+ result.deduplicated === true ? " No new run was started. Use a different --idempotency-key to start another rerun." : " This is a fresh run using the original input and pinned revision. Normal tool receipt reuse still applies."
26860
26990
  );
26861
26991
  }
26862
26992
  return 0;
@@ -27334,8 +27464,46 @@ async function handleRunStop(args) {
27334
27464
  );
27335
27465
  return 0;
27336
27466
  }
27467
+ function padColumn(value, width) {
27468
+ return value.length >= width ? value : value + " ".repeat(width - value.length);
27469
+ }
27470
+ function buildStopAllDryRunTextLines(result) {
27471
+ const count = result.candidateCount;
27472
+ const lines = [
27473
+ `Dry run: ${count} active run${count === 1 ? "" : "s"} would be stopped`,
27474
+ `Scope: ${result.scope}`,
27475
+ `Includes child runs: ${result.includesChildren ? "yes" : "no"}`
27476
+ ];
27477
+ if (result.runs.length > 0) {
27478
+ const rows = result.runs.map((run) => [
27479
+ run.runId,
27480
+ run.playName ?? "\u2014",
27481
+ run.status ?? "\u2014"
27482
+ ]);
27483
+ const header = ["RUN_ID", "PLAY", "STATUS"];
27484
+ const widths = header.map(
27485
+ (label, column) => Math.max(label.length, ...rows.map((row) => row[column].length))
27486
+ );
27487
+ lines.push("");
27488
+ for (const row of [header, ...rows]) {
27489
+ lines.push(
27490
+ row.map(
27491
+ (cell, column) => column === row.length - 1 ? cell : padColumn(cell, widths[column])
27492
+ ).join(" ")
27493
+ );
27494
+ }
27495
+ }
27496
+ if (result.truncated) {
27497
+ lines.push("");
27498
+ lines.push(
27499
+ `Showing the first ${result.runs.length} matching runs. Results are truncated; no runs were stopped.`
27500
+ );
27501
+ }
27502
+ return lines;
27503
+ }
27337
27504
  async function handleRunStopAll(args) {
27338
27505
  let reason;
27506
+ const dryRun = args.includes("--dry-run");
27339
27507
  for (let index = 0; index < args.length; index += 1) {
27340
27508
  const arg = args[index];
27341
27509
  if (arg === "--reason" && args[index + 1]) {
@@ -27343,6 +27511,21 @@ async function handleRunStopAll(args) {
27343
27511
  }
27344
27512
  }
27345
27513
  const client2 = new DeeplineClient();
27514
+ if (dryRun) {
27515
+ const result2 = await client2.runs.stopAll({ dryRun: true, reason });
27516
+ const lines2 = buildStopAllDryRunTextLines(result2);
27517
+ printCommandEnvelope(
27518
+ {
27519
+ ...result2,
27520
+ render: {
27521
+ sections: [{ title: "workspace stop-all (dry run)", lines: lines2 }]
27522
+ }
27523
+ },
27524
+ { json: argsWantJson(args), text: `${lines2.join("\n")}
27525
+ ` }
27526
+ );
27527
+ return 0;
27528
+ }
27346
27529
  const result = await client2.runs.stopAll({ reason });
27347
27530
  const lines = [
27348
27531
  `Stopped ${result.stopped} active run${result.stopped === 1 ? "" : "s"}`,
@@ -29111,13 +29294,23 @@ Notes:
29111
29294
  The rerun uses the original input and exact saved revision. Normal completed
29112
29295
  tool receipts may be reused; inspect the new run before forcing any external
29113
29296
  side effect.
29297
+ Every call sends an Idempotency-Key. Pass --idempotency-key to reuse the
29298
+ same key across separate invocations (for example after an ambiguous or
29299
+ lost response): the same source run and key always resolve to the same
29300
+ rerun, and the JSON result reports deduplicated: true when nothing new was
29301
+ started. A different key intentionally starts another rerun.
29114
29302
 
29115
29303
  Examples:
29116
29304
  deepline runs rerun play/my-play/webhook/abc --json
29305
+ deepline runs rerun play/my-play/webhook/abc --idempotency-key incident-42 --json
29117
29306
  `
29307
+ ).option(
29308
+ "--idempotency-key <key>",
29309
+ "Reuse this key so repeated calls resolve to one rerun"
29118
29310
  ).option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (runId, options) => {
29119
29311
  process.exitCode = await handleRunsRerun([
29120
29312
  runId,
29313
+ ...options.idempotencyKey ? ["--idempotency-key", options.idempotencyKey] : [],
29121
29314
  ...options.json ? ["--json"] : []
29122
29315
  ]);
29123
29316
  });
@@ -29148,14 +29341,24 @@ Examples:
29148
29341
  `
29149
29342
  Notes:
29150
29343
  Stops all active runs visible to this workspace, including child runs created by ctx.runPlay.
29344
+ --dry-run lists the exact candidates (same scope, statuses, and order) and
29345
+ never cancels or mutates anything. A real stop-all exits nonzero when any
29346
+ cancellation failed or the candidate list was truncated; failed runs stay
29347
+ active and are reported truthfully.
29151
29348
 
29152
29349
  Examples:
29350
+ deepline runs stop-all --dry-run
29351
+ deepline runs stop-all --dry-run --json
29153
29352
  deepline runs stop-all --reason "unjam workspace"
29154
29353
  deepline runs stop-all --json
29155
29354
  `
29156
- ).option("--reason <text>", "Reason to include with each stop request").option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (options) => {
29355
+ ).option("--reason <text>", "Reason to include with each stop request").option(
29356
+ "--dry-run",
29357
+ "List the runs that would be stopped without cancelling anything"
29358
+ ).option("--json", "Emit JSON output. Also automatic when stdout is piped").action(async (options) => {
29157
29359
  process.exitCode = await handleRunStopAll([
29158
29360
  ...options.reason ? ["--reason", options.reason] : [],
29361
+ ...options.dryRun ? ["--dry-run"] : [],
29159
29362
  ...options.json ? ["--json"] : []
29160
29363
  ]);
29161
29364
  });