@deepdockai/cli 1.0.22 → 1.0.25

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.25
4
+
5
+ - Three new error codes for writes over a customer system's own HTTP interface: `applied_unverified` (409, exit 2: the write landed but its read-back did not finish; `error.created` names the entry as `{ chartId }`, or null), `credentials_rejected` and `certificate_changed` (503, exit 3: a person at the practice renews the login or confirms the certificate in the Deepdock client). An earlier CLI already exits with the same codes through its status fallback.
6
+ - `system_version_unverified` can carry `error.reason: unreported` with `error.version: null`, and a system's `writes` can read `unverified` with reason `unreported`: such a write is verified for one exact release, and the system reported none.
7
+ - `error.uncertain` is documented on `outcome_unknown`. The skill's error reference describes all of it.
8
+
9
+ ## 1.0.24
10
+
11
+ - `devices list` and `incidents list` read every page of the API's paged answer (`limit` and `cursor`, following `nextCursor`), so no device or incident past the first 500 or 100 stays invisible. Output is unchanged. They stop after 200 pages with a warning on stderr. This needs the API that pages these lists; an older answer is refused with exit 3.
12
+
13
+ ## 1.0.23
14
+
15
+ - An incident's program version is named `programVersion` instead of `version`, in `GET /v1/incidents`, in `incidents list --json` and in the skill's concepts. Read the new name where you read the old one; the value is unchanged.
16
+
3
17
  ## 1.0.22
4
18
 
5
19
  - `keys create --write-env` writes `DEEPDOCK_BASE_URL=<url>` beside the key when `DEEPDOCK_BASE_URL` points the CLI at another API than production, since a key works only on the API it was created on and the bundled clients default to production. A production key points such a line, left by an earlier key, at production. `meta.written.baseUrl` names the URL when the line was written. Revoking a key removes only the key line.
package/README.md CHANGED
@@ -154,14 +154,14 @@ your code will receive; `auth`, `org select` and `keys create` build it from nam
154
154
  failure is one document on stderr, `{ "error": { "code", "message" }, "meta": { "hint" } }`,
155
155
  with the API's error object unchanged. Switch on `code` or on the exit code, never on `message`.
156
156
 
157
- | Exit | Meaning |
158
- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
159
- | 0 | done |
160
- | 1 | something on the command line to fix (`invalid_usage`, `org_required`) |
161
- | 2 | the API refused the request; do not repeat it unchanged (`invalid_request`, `not_found`, `forbidden`, `rate_limited` and the other refusals) |
162
- | 3 | retry later (`device_offline`, `device_syncing`, `device_outdated`, `system_version_unverified`, `action_paused`, `access_outdated`, `access_missing`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `outcome_unknown`, `connection_lost`) |
163
- | 4 | the CLI is too old for the API (`client_outdated`): run `npx @deepdockai/cli@latest` |
164
- | 5 | sign-in needed (`sign_in_needed`, `approval_denied`, `approval_expired`); `auth status` when nobody is signed in |
157
+ | Exit | Meaning |
158
+ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
159
+ | 0 | done |
160
+ | 1 | something on the command line to fix (`invalid_usage`, `org_required`) |
161
+ | 2 | the API refused the request; do not repeat it unchanged (`invalid_request`, `not_found`, `forbidden`, `rate_limited` and the other refusals) |
162
+ | 3 | retry later (`device_offline`, `device_syncing`, `device_outdated`, `system_version_unverified`, `action_paused`, `access_outdated`, `access_missing`, `credentials_rejected`, `certificate_changed`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `outcome_unknown`, `connection_lost`) |
163
+ | 4 | the CLI is too old for the API (`client_outdated`): run `npx @deepdockai/cli@latest` |
164
+ | 5 | sign-in needed (`sign_in_needed`, `approval_denied`, `approval_expired`); `auth status` when nobody is signed in |
165
165
 
166
166
  The CLI never retries on its own: exit 3 says retry, you decide when. `deepdock commands --json`
167
167
  prints the whole command tree.
package/dist/deepdock.js CHANGED
@@ -10866,6 +10866,7 @@ var EXIT_CODES = {
10866
10866
  rate_limited: 2,
10867
10867
  target_rejected: 2,
10868
10868
  readback_failed: 2,
10869
+ applied_unverified: 2,
10869
10870
  idempotency_conflict: 2,
10870
10871
  conflict: 2,
10871
10872
  already_member: 2,
@@ -10880,6 +10881,8 @@ var EXIT_CODES = {
10880
10881
  action_paused: 3,
10881
10882
  access_outdated: 3,
10882
10883
  access_missing: 3,
10884
+ credentials_rejected: 3,
10885
+ certificate_changed: 3,
10883
10886
  system_unavailable: 3,
10884
10887
  target_busy: 3,
10885
10888
  temporarily_unavailable: 3,
@@ -11429,7 +11432,7 @@ import { spawn } from "node:child_process";
11429
11432
  import { homedir, hostname } from "node:os";
11430
11433
 
11431
11434
  // src/version.ts
11432
- var VERSION = "1.0.22";
11435
+ var VERSION = "1.0.25";
11433
11436
 
11434
11437
  // src/runtime.ts
11435
11438
  var DEFAULT_BASE_URL = "https://api.deepdock.ai";
@@ -12160,6 +12163,7 @@ var EXIT_CODES_HELP = `Exit codes
12160
12163
  2 the API refused the request; do not repeat it unchanged
12161
12164
  3 retry later (device_offline, device_syncing, device_outdated,
12162
12165
  system_version_unverified, action_paused, access_outdated, access_missing,
12166
+ credentials_rejected, certificate_changed,
12163
12167
  system_unavailable, target_busy, temporarily_unavailable, internal_error,
12164
12168
  outcome_unknown, connection_lost)
12165
12169
  4 the CLI is too old for the API (client_outdated)
@@ -12828,6 +12832,40 @@ var registerAuth = (program, run) => {
12828
12832
  auth.command("logout").description("Revoke the sign-in and the API keys this CLI created").option("--keep-keys", "revoke only the sign-in and leave the API keys active").action((options, cmd) => run(cmd, (ctx) => logout(ctx, options)));
12829
12833
  };
12830
12834
 
12835
+ // src/commands/pages.ts
12836
+ var PAGE_LIMIT = 500;
12837
+ var MAX_PAGES = 200;
12838
+ var unreadable = () => new CliError({
12839
+ code: "connection_lost",
12840
+ message: "The API answered a list in a shape this CLI does not read.",
12841
+ hint: "Update the CLI with `npm install -g @deepdockai/cli@latest`, then retry."
12842
+ });
12843
+ var readAllPages = async (ctx, path, org) => {
12844
+ const items = [];
12845
+ let cursor = null;
12846
+ let requestId;
12847
+ for (let page = 1;page <= MAX_PAGES; page++) {
12848
+ const query = new URLSearchParams({ limit: String(PAGE_LIMIT) });
12849
+ if (cursor !== null)
12850
+ query.set("cursor", cursor);
12851
+ const url = `${path}?${query.toString()}`;
12852
+ const response = org ? await ctx.call({ method: "GET", path: url, org }) : await ctx.callInOrg({ method: "GET", path: url });
12853
+ const data = response.data;
12854
+ if (!isRecord(data) || !Array.isArray(data.items))
12855
+ throw unreadable();
12856
+ const next = data.nextCursor;
12857
+ if (next !== null && (typeof next !== "string" || next === ""))
12858
+ throw unreadable();
12859
+ items.push(...data.items);
12860
+ requestId ??= response.requestId;
12861
+ if (next === null)
12862
+ return { items, requestId };
12863
+ cursor = next;
12864
+ }
12865
+ ctx.note(`Warning: stopped after ${MAX_PAGES} pages; the list is longer than the ${items.length} rows shown.`);
12866
+ return { items, requestId };
12867
+ };
12868
+
12831
12869
  // src/commands/devices.ts
12832
12870
  var meta = (requestId) => requestId ? { requestId } : {};
12833
12871
  var updateNotes = (ctx, devices) => {
@@ -12840,19 +12878,18 @@ var updateNotes = (ctx, devices) => {
12840
12878
  }
12841
12879
  };
12842
12880
  var list = async (ctx) => {
12843
- const response = await ctx.callInOrg({ method: "GET", path: "/partner/devices" });
12844
- const devices = Array.isArray(response.data) ? response.data : [];
12881
+ const { items: devices, requestId } = await readAllPages(ctx, "/partner/devices");
12845
12882
  if (devices.length === 0) {
12846
12883
  return {
12847
- data: response.data,
12848
- meta: meta(response.requestId),
12884
+ data: devices,
12885
+ meta: meta(requestId),
12849
12886
  text: "No devices are paired. Pair one with `deepdock devices pair <code>`."
12850
12887
  };
12851
12888
  }
12852
12889
  updateNotes(ctx, devices);
12853
12890
  return {
12854
- data: response.data,
12855
- meta: meta(response.requestId),
12891
+ data: devices,
12892
+ meta: meta(requestId),
12856
12893
  text: deviceTable(devices, ctx.rt.now(), ctx.out)
12857
12894
  };
12858
12895
  };
@@ -12906,8 +12943,8 @@ var release = async (ctx, id) => {
12906
12943
  if (!ctx.opts.yes) {
12907
12944
  let label = "";
12908
12945
  try {
12909
- const response = await ctx.call({ method: "GET", path: "/partner/devices", org: org.slug });
12910
- const found = Array.isArray(response.data) ? response.data.filter(isRecord).find((device) => field(device, "id") === deviceId) : undefined;
12946
+ const { items } = await readAllPages(ctx, "/partner/devices", org.slug);
12947
+ const found = items.filter(isRecord).find((device) => field(device, "id") === deviceId);
12911
12948
  label = found ? field(found, "label") : "";
12912
12949
  } catch {}
12913
12950
  const named = label ? ` (${stripControl(label)})` : "";
@@ -12966,10 +13003,9 @@ var registerDevices = (program, run) => {
12966
13003
  var registerIncidents = (program, run) => {
12967
13004
  const incidents = program.command("incidents").description("Incidents that paused an action on one of your devices");
12968
13005
  incidents.command("list").description("List open incidents and those closed within 30 days").action((_options, cmd) => run(cmd, async (ctx) => {
12969
- const response = await ctx.callInOrg({ method: "GET", path: "/partner/incidents" });
12970
- const items = Array.isArray(response.data) ? response.data : [];
13006
+ const { items, requestId } = await readAllPages(ctx, "/partner/incidents");
12971
13007
  const text = items.length === 0 ? "No incidents." : genericTable(items, ctx.out, INCIDENT_COLUMNS);
12972
- return { data: response.data, meta: { requestId: response.requestId }, text };
13008
+ return { data: items, meta: { requestId }, text };
12973
13009
  }));
12974
13010
  };
12975
13011
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepdockai/cli",
3
- "version": "1.0.22",
3
+ "version": "1.0.25",
4
4
  "description": "Deepdock partner CLI: pair devices, see systems and actions, create the API key your code runs them with.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -84,7 +84,7 @@ A failure is one document on stderr: the API's error object unchanged plus what
84
84
  | 0 | done | |
85
85
  | 1 | something on the command line to fix | `invalid_usage` (a bad flag, an unknown slug, a question with no terminal and no `--yes`, an env file git does not ignore), `org_required` |
86
86
  | 2 | the API refused the request; do not repeat it unchanged | `invalid_request`, `not_found`, `device_not_found`, `pairing_code_invalid`, `other_partner_build`, `rate_limited`, `forbidden`, `conflict` and the other refusals (not `access_outdated` and `access_missing`, see exit 3) |
87
- | 3 | retry later (`access_outdated` and `access_missing`: after the customer repeats "Erneut einrichten" in the Deepdock client) | `device_offline`, `device_syncing`, `device_outdated`, `system_version_unverified`, `action_paused`, `access_outdated`, `access_missing`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `outcome_unknown`, `connection_lost` |
87
+ | 3 | retry later (`access_outdated` and `access_missing`: after the customer repeats "Erneut einrichten" in the Deepdock client) | `device_offline`, `device_syncing`, `device_outdated`, `system_version_unverified`, `action_paused`, `access_outdated`, `access_missing`, `credentials_rejected`, `certificate_changed`, `system_unavailable`, `target_busy`, `temporarily_unavailable`, `internal_error`, `outcome_unknown`, `connection_lost` |
88
88
  | 4 | the CLI is too old for the API: run `npx @deepdockai/cli@latest` | `client_outdated` |
89
89
  | 5 | sign-in needed: run `deepdock auth login` | `sign_in_needed`, `approval_denied`, `approval_expired` |
90
90
 
@@ -20,14 +20,17 @@ class DeepdockError(Exception):
20
20
 
21
21
  def __init__(self, status: int, code: str, message: str, request_id: Optional[str] = None,
22
22
  systems: Optional[List[str]] = None, version: Optional[str] = None,
23
- reason: Optional[str] = None):
23
+ reason: Optional[str] = None, created: Optional[Dict[str, Any]] = None,
24
+ uncertain: Optional[bool] = None):
24
25
  super().__init__(message)
25
26
  self.status = status
26
27
  self.code = code
27
28
  self.request_id = request_id
28
29
  self.systems = systems
29
- self.version = version # system_version_unverified: the version the system last reported
30
- self.reason = reason # system_version_unverified: unverified_version or unparseable
30
+ self.version = version # system_version_unverified: the version the system last reported, None when it reported none
31
+ self.reason = reason # system_version_unverified: unverified_version, unparseable or unreported
32
+ self.created = created # applied_unverified, outcome_unknown with no rollback: {"chartId"} or None
33
+ self.uncertain = uncertain # outcome_unknown: True when the write may or may not have landed
31
34
 
32
35
 
33
36
  class DeepdockTransportError(Exception):
@@ -67,7 +70,7 @@ class Deepdock:
67
70
  if isinstance(body, dict) and "error" in body:
68
71
  e = body["error"]
69
72
  raise DeepdockError(r.status_code, e["code"], e["message"], e.get("requestId"), e.get("systems"),
70
- e.get("version"), e.get("reason"))
73
+ e.get("version"), e.get("reason"), e.get("created"), e.get("uncertain"))
71
74
  if not isinstance(body, dict) or "data" not in body:
72
75
  raise DeepdockTransportError(f"unexpected body with status {r.status_code}")
73
76
  return body["data"], r.headers
@@ -166,8 +169,11 @@ def attempt(dd: Deepdock, row: PendingWrite) -> PendingWrite:
166
169
  if row.conflict_retries < CONFLICT_RETRIES:
167
170
  return replace(row, status="sent", conflict_retries=row.conflict_retries + 1)
168
171
  return replace(row, status="unknown")
169
- if e.code == "outcome_unknown":
170
- return replace(row, status="unknown", request_id=e.request_id)
172
+ # applied_unverified: the entry exists but was not verified; never write it again under a new
173
+ # key. result["created"] names it for the check.
174
+ if e.code in ("outcome_unknown", "applied_unverified"):
175
+ return replace(row, status="unknown", request_id=e.request_id,
176
+ result={"created": e.created, "uncertain": e.uncertain})
171
177
  # invalid_input, target_rejected, action-specific 422s, not_found, readback_failed, auth and
172
178
  # resolution codes: nothing was written. Fix the cause; a changed input gets a new key.
173
179
  return replace(row, status="refused", refused_code=e.code, result={"message": str(e)})
@@ -13,15 +13,21 @@ export class DeepdockError extends Error {
13
13
  readonly code: string
14
14
  readonly requestId?: string
15
15
  readonly systems?: string[]
16
- /** system_version_unverified: the version the system last reported. */
17
- readonly version?: string
18
- /** system_version_unverified: `unverified_version` or `unparseable`. */
16
+ /** system_version_unverified: the version the system last reported, `null` when it reported none. */
17
+ readonly version?: string | null
18
+ /** system_version_unverified: `unverified_version`, `unparseable` or `unreported`. */
19
19
  readonly reason?: string
20
+ /** applied_unverified, outcome_unknown on a system with no rollback: the entry, or `null` when not known. */
21
+ readonly created?: { chartId?: string } | null
22
+ /** outcome_unknown: `true` when the write may or may not have landed. */
23
+ readonly uncertain?: boolean
20
24
 
21
25
  constructor(status: number, code: string, message: string, requestId?: string,
22
26
  systems?: string[],
23
- version?: string,
27
+ version?: string | null,
24
28
  reason?: string,
29
+ created?: { chartId?: string } | null,
30
+ uncertain?: boolean,
25
31
  ) {
26
32
  super(message)
27
33
  this.name = 'DeepdockError'
@@ -31,6 +37,8 @@ export class DeepdockError extends Error {
31
37
  this.systems = systems
32
38
  this.version = version
33
39
  this.reason = reason
40
+ this.created = created
41
+ this.uncertain = uncertain
34
42
  }
35
43
  }
36
44
 
@@ -42,7 +50,7 @@ export class DeepdockTransportError extends Error {
42
50
  }
43
51
  }
44
52
 
45
- type ErrorBody = { code: string; message: string; requestId?: string; systems?: string[]; version?: string; reason?: string }
53
+ type ErrorBody = { code: string; message: string; requestId?: string; systems?: string[]; version?: string | null; reason?: string; created?: { chartId?: string } | null; uncertain?: boolean }
46
54
  type Envelope<T> = { data: T } | { error: ErrorBody }
47
55
 
48
56
  export type Page<Item> = { items: Item[]; nextCursor: string | null }
@@ -56,11 +64,12 @@ export type SystemAccess = 'ok' | 'outdated' | 'missing' | 'password_missing' |
56
64
  /**
57
65
  * Whether writes on the system run. `unverified` makes writes answer system_version_unverified:
58
66
  * `unverified_version` clears once Deepdock verifies the version; `unparseable` does not clear by
59
- * waiting (contact Deepdock).
67
+ * waiting (contact Deepdock); `unreported` clears once the device reports the system's version (a
68
+ * write verified for one exact release needs it).
60
69
  */
61
70
  export type SystemWrites =
62
71
  | { state: 'verified' | 'unknown' | 'partial' }
63
- | { state: 'unverified'; reason: 'unverified_version' | 'unparseable' }
72
+ | { state: 'unverified'; reason: 'unverified_version' | 'unparseable' | 'unreported' }
64
73
 
65
74
  export type Device = {
66
75
  id: string
@@ -165,8 +174,8 @@ export class Deepdock {
165
174
  throw new DeepdockTransportError(`non-JSON answer with status ${res.status}`)
166
175
  }
167
176
  if (body !== null && typeof body === 'object' && 'error' in body) {
168
- const { code, message, requestId, systems, version, reason } = body.error
169
- throw new DeepdockError(res.status, code, message, requestId, systems, version, reason)
177
+ const { code, message, requestId, systems, version, reason, created, uncertain } = body.error
178
+ throw new DeepdockError(res.status, code, message, requestId, systems, version, reason, created, uncertain)
170
179
  }
171
180
  if (body === null || typeof body !== 'object' || !('data' in body)) {
172
181
  throw new DeepdockTransportError(`unexpected body with status ${res.status}`)
@@ -305,7 +314,15 @@ export const attempt = async (dd: Deepdock, row: PendingWrite): Promise<PendingW
305
314
  ? { ...row, status: 'sent', conflictRetries: row.conflictRetries + 1 }
306
315
  : { ...row, status: 'unknown' }
307
316
  }
308
- if (err.code === 'outcome_unknown') return { ...row, status: 'unknown', requestId: err.requestId }
317
+ // applied_unverified: the entry exists but was not verified; never write it again under a new
318
+ // key. `result.created` names it for the check.
319
+ if (err.code === 'outcome_unknown' || err.code === 'applied_unverified')
320
+ return {
321
+ ...row,
322
+ status: 'unknown',
323
+ requestId: err.requestId,
324
+ result: { created: err.created, uncertain: err.uncertain },
325
+ }
309
326
  // invalid_input, target_rejected, action-specific 422s, not_found, readback_failed, auth and
310
327
  // resolution codes: nothing was written. Fix the cause; a changed input gets a new key.
311
328
  return { ...row, status: 'refused', refusedCode: err.code, result: { message: err.message } }
@@ -95,7 +95,8 @@ Each pause opens an **incident** (`GET /v1/incidents`). Only Deepdock lifts a pa
95
95
  customer's site see that an action is paused but cannot resume it; there is no partner call to
96
96
  resume. A stopped or missing system needs no resume: its incident closes once the system is back.
97
97
 
98
- An incident has `id`, `system`, `version`, `action` (`null` for a system-level incident), `reason`,
98
+ An incident has `id`, `system`, `programVersion` (the version of the vendor's program its devices
99
+ run, `null` when they reported none), `action` (`null` for a system-level incident), `reason`,
99
100
  `openedAt`, `closedAt`, and `devices` with `deviceId`, `pausedAt`, `resumedAt` and `inactiveAt`. It
100
101
  closes once none of its devices is still paused. `GET /v1/incidents` lists incidents that are open
101
102
  or closed within the last 30 days, newest first, at most 100.
@@ -62,10 +62,12 @@ dashboard-only.
62
62
  | `device_offline` | 503 | the device is not connected, is shutting down, did not answer within 20 s, or the call was cut off inside Deepdock by a restart or deploy. **A write may still have landed** | yes, with backoff (seconds to minutes); a write only with the same key | "the customer PC is offline; we will retry" |
63
63
  | `device_syncing` | 503 | the device has not reported its Deepdock client yet (just paired, or its report did not come through); Deepdock asks it again. Nothing was written | yes, after `Retry-After` (5 s); a write with the same key | "the customer PC is connecting; retrying" |
64
64
  | `device_outdated` | 409 | the device's Deepdock client is too old for this call. It updates itself when a newer version is released for your organisation, once nobody is signed in on the PC. Nothing was written | after the update (`outdated` on `GET /v1/devices/{deviceId}` turns `false`); retrying before it gets the same answer. A write keeps its key | "the Deepdock client on the customer PC needs an update" |
65
- | `system_version_unverified` | 409 | a write on a system whose vendor version Deepdock has not verified yet. Nothing was sent to the device; reads still work. `error.version` is the version the system last reported and `error.reason` is `unverified_version`, or `unparseable` when that version is not dotted digits | `unverified_version`: after Deepdock has verified that version (`writes` on the system in `GET /v1/devices/{deviceId}` turns `verified`); retrying before it gets the same answer. `unparseable`: retrying does not help, contact Deepdock; only a different reported version changes it. A write keeps its key | "the customer's system was updated to a version Deepdock has not yet verified; changes will work once it has" |
65
+ | `system_version_unverified` | 409 | a write on a system whose vendor version Deepdock has not verified yet. Nothing was sent to the device; reads still work. `error.version` is the version the system last reported (null when it reported none) and `error.reason` is `unverified_version`, `unparseable` when that version is not dotted digits, or `unreported` when the system reported no version: a write over the system's own HTTP interface is verified for one exact release and needs it | `unverified_version`: after Deepdock has verified that version (`writes` on the system in `GET /v1/devices/{deviceId}` turns `verified`); retrying before it gets the same answer. `unparseable`: retrying does not help, contact Deepdock; only a different reported version changes it. `unreported`: later, once the device reports the system's version. A write keeps its key | "the customer's system was updated to a version Deepdock has not yet verified; changes will work once it has" |
66
66
  | `action_paused` | 503 | the action is paused on the device (`GET .../systems` shows `reason`); only Deepdock resumes it | later, when `GET .../systems` shows it active; do not loop | "this feature is paused while Deepdock checks the customer system" |
67
67
  | `system_unavailable` | 503 | the customer's program is stopped or missing on the device | later; often resolves when the customer restarts the program | "the customer's software is not running" |
68
68
  | `target_busy` | 503 | the customer's system is busy or unreachable | yes, with backoff | "the customer's system is busy; retrying" |
69
+ | `credentials_rejected` | 503 | the customer's system refused the device's login. Nothing was sent | later, after a person at the customer has renewed the access in the Deepdock client; do not loop. A write keeps its key | "the connection to the customer's software needs to be renewed at the practice" |
70
+ | `certificate_changed` | 503 | the customer's system shows a certificate other than the one the device trusts. Nothing was sent | later, after a person at the customer has confirmed the new certificate in the Deepdock client; do not loop. A write keeps its key | "the connection to the customer's software needs to be confirmed at the practice" |
69
71
  | `temporarily_unavailable` | 503 | Deepdock could not check the key or reach its own database (sent with `Retry-After: 5`) | yes, with backoff | "temporary problem; retrying" |
70
72
 
71
73
  ## Write outcomes
@@ -73,8 +75,9 @@ dashboard-only.
73
75
  | Code | Status | Meaning | Retry | Show |
74
76
  |---|---|---|---|---|
75
77
  | `readback_failed` | 409 | the write's result did not match what the action expects, so nothing was saved; **the action is now paused** and an incident is open | no; the next calls answer `action_paused` | "the change was not saved; Deepdock is checking the customer system" |
78
+ | `applied_unverified` | 409 | a write on a system with no rollback was applied, but its read-back did not finish. The entry exists: `error.created` names it as `{ chartId }`, or is null when the entry is not known | **no.** The entry is there; writing again creates a second one. Check it in the customer system | "the change was saved but could not be confirmed; please check it in the customer system" |
76
79
  | `idempotency_conflict` | 409 | the key was used with a different input, its write is still running, or its outcome is unknown. One code, three causes; `message` must not be parsed to tell them apart | retry with the **same key and the same input** a bounded number of times with backoff (a running write finishes within seconds); if it still conflicts, handle it like `outcome_unknown`: read back, then decide | "a previous attempt is still in progress" |
77
- | `outcome_unknown` | 500 | the system did not confirm the write; it may or may not have landed. The key stays blocked | **no.** Read the record back (a search) to see whether it landed before writing again under a new key; involve a human when the read cannot tell | "we could not confirm the change; please check in the customer system before trying again" |
80
+ | `outcome_unknown` | 500 | the system did not confirm the write; it may or may not have landed. The key stays blocked. On a system with no rollback, `error.uncertain` is true and `error.created` names the entry when one is known | **no.** Read the record back (a search) to see whether it landed before writing again under a new key; involve a human when the read cannot tell | "we could not confirm the change; please check in the customer system before trying again" |
78
81
  | `internal_error` | 500 | something failed inside Deepdock; `error.requestId` is set | once, later; a write with the same key | "something went wrong"; log `requestId` for Deepdock support |
79
82
 
80
83
  ## A reaction table for code
@@ -104,6 +107,8 @@ const REACTION: Record<string, Reaction> = {
104
107
  payload_too_large: 'fix-code',
105
108
  access_outdated: 'wait-setup',
106
109
  access_missing: 'wait-setup',
110
+ credentials_rejected: 'wait-setup',
111
+ certificate_changed: 'wait-setup',
107
112
  device_offline: 'retry-same-key',
108
113
  device_syncing: 'retry-same-key',
109
114
  device_outdated: 'retry-later',
@@ -113,6 +118,7 @@ const REACTION: Record<string, Reaction> = {
113
118
  target_busy: 'retry-same-key',
114
119
  temporarily_unavailable: 'retry-same-key',
115
120
  readback_failed: 'wait-paused',
121
+ applied_unverified: 'check-then-decide', // the entry exists; reading it back shows it
116
122
  idempotency_conflict: 'retry-same-key', // bounded; then 'check-then-decide'
117
123
  outcome_unknown: 'check-then-decide',
118
124
  internal_error: 'retry-same-key',
@@ -120,7 +126,7 @@ const REACTION: Record<string, Reaction> = {
120
126
  const reactionFor = (code: string): Reaction => REACTION[code] ?? 'stop'
121
127
  ```
122
128
 
123
- Action-specific 422 codes beyond the ones above (any the OpenAPI description lists) map to `fix-input`. Add them from the document, not from memory. `wait-setup` means a person at the customer must repeat the database access setup ("Erneut einrichten") in the Deepdock client first; there is no `Retry-After`, so do not loop. Check `access` on the system in `GET .../systems` (`ok` again), then retry; a write keeps its key. `retry-same-key` means the
129
+ Action-specific 422 codes beyond the ones above (any the OpenAPI description lists) map to `fix-input`. Add them from the document, not from memory. `wait-setup` means a person at the customer must act in the Deepdock client first: repeat the database access setup ("Erneut einrichten") for `access_*`, renew the login for `credentials_rejected`, confirm the new certificate for `certificate_changed`; there is no `Retry-After`, so do not loop. Check `access` on the system in `GET .../systems` (`ok` again), then retry; a write keeps its key. `retry-same-key` means the
124
130
  pending write stays persisted and is retried from that state with backoff, also across process
125
131
  restarts; a device can be offline for hours, so the retry loop lives in a job or queue, not in the
126
132
  request that started the write.
@@ -74,6 +74,7 @@ pending ──send──▶ sent ──2xx (fresh or replayed)──────
74
74
  │ ──idempotency_conflict, retries left ───▶ sent (retry, same key, same input)
75
75
  │ ──idempotency_conflict, retries spent ──▶ unknown (read back before anything else)
76
76
  │ ──outcome_unknown ──────────────────────▶ unknown (read back before anything else)
77
+ │ ──applied_unverified ───────────────────▶ unknown (the entry exists: check it by result.created, never write it again)
77
78
  │ ──invalid_input, target_rejected, action-specific 422, not_found, readback_failed ▶ refused (key is free; new input gets a new key)
78
79
  │ ──action_paused, system_unavailable ────▶ sent (retry once the action is active again)
79
80
  ```