dsh-workbuddy-connect 0.7.7 → 0.7.9

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.
@@ -1,11 +1,226 @@
1
- import { readFile, rm, writeFile } from "node:fs/promises";
1
+ import { lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
2
  import { homedir, release } from "node:os";
3
3
  import { basename, dirname, join } from "node:path";
4
- import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
5
- import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
6
4
  import { createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
5
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
6
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
7
7
  import { execFile, execFileSync } from "node:child_process";
8
8
  import { accessSync, constants, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
9
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-atomic-write@0.2.1-rc.2_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-atomic-write/lib/index.js
10
+ /**
11
+ * Zero-dependency atomic file replacement and writer coordination.
12
+ * `writeFileAtomic` writes a random-suffix sibling with exclusive create and
13
+ * the caller's permission bits, then renames it over the target, so readers
14
+ * observe either the old or the new complete content and a replaced file ends
15
+ * up with exactly the stated mode. `withFileLock` serializes cross-process
16
+ * writers of one file through a `wx`-created `<file>.lock` sibling, so a
17
+ * read-modify-write cycle can never resurrect a state another writer just
18
+ * replaced; readers stay lock-free because the rename commit is atomic. A lock
19
+ * whose recorded holder process no longer exists is taken over.
20
+ * @module @deepseek-ai/dsh-atomic-write
21
+ */
22
+ const WINDOWS_TRANSIENT_RENAME_ERRORS = /* @__PURE__ */ new Set([
23
+ "EACCES",
24
+ "EBUSY",
25
+ "EPERM"
26
+ ]);
27
+ const WINDOWS_RENAME_RETRY_INITIAL_MS = 20;
28
+ const WINDOWS_RENAME_RETRY_MAX_MS = 200;
29
+ const WINDOWS_RENAME_RETRY_LIMIT = 8;
30
+ /** Whether Windows reported temporary interference with an atomic replacement. */
31
+ function isTransientWindowsRenameError(error) {
32
+ if (process.platform !== "win32") return false;
33
+ return WINDOWS_TRANSIENT_RENAME_ERRORS.has(error?.code ?? "");
34
+ }
35
+ /** Replace the target after bounded retries for transient Windows interference. */
36
+ async function renameAtomicTemp(temp, filename) {
37
+ let delay = WINDOWS_RENAME_RETRY_INITIAL_MS;
38
+ for (let retries = 0;; retries += 1) {
39
+ try {
40
+ await rename(temp, filename);
41
+ return;
42
+ } catch (error) {
43
+ if (!isTransientWindowsRenameError(error)) throw error;
44
+ if (retries >= WINDOWS_RENAME_RETRY_LIMIT) throw error;
45
+ }
46
+ await new Promise((resolve) => setTimeout(resolve, delay));
47
+ delay = Math.min(delay * 2, WINDOWS_RENAME_RETRY_MAX_MS);
48
+ }
49
+ }
50
+ /**
51
+ * Replace `filename` with `content` in one atomic step, creating parent
52
+ * directories. The content is first written to a random-suffix sibling opened
53
+ * with exclusive create (`wx`): the open refuses to follow a symlink planted
54
+ * at the temp path, and the fresh inode carries `options.mode` through the
55
+ * rename, so replacing a wider-permission file narrows it without a chmod
56
+ * race. The rename also replaces a symlinked target itself instead of writing
57
+ * through to its referent, and the same-directory sibling keeps the rename on
58
+ * one filesystem. Windows replacement retries transient `EACCES`, `EBUSY`,
59
+ * and `EPERM` failures for a bounded interval while the complete temp file
60
+ * remains the rename source. On any remaining failure the temp file is
61
+ * removed and the failure rethrown. Crash durability (fsync) is out of scope.
62
+ * @param filename - final path receiving the content.
63
+ * @param content - complete next file content.
64
+ * @param options - permission bits for the replacement inode.
65
+ */
66
+ async function writeFileAtomic(filename, content, options) {
67
+ await mkdir(dirname(filename), {
68
+ recursive: true,
69
+ ...options.dirMode === void 0 ? {} : { mode: options.dirMode }
70
+ });
71
+ const temp = `${filename}.${randomBytes(6).toString("hex")}.tmp`;
72
+ try {
73
+ await writeFile(temp, content, {
74
+ mode: options.mode,
75
+ flag: "wx"
76
+ });
77
+ await renameAtomicTemp(temp, filename);
78
+ } catch (error) {
79
+ await rm(temp, { force: true });
80
+ throw error;
81
+ }
82
+ }
83
+ /** Whether an exclusive create found an existing lock. */
84
+ async function isLockContention(error, lockPath) {
85
+ const code = error?.code;
86
+ if (code === "EEXIST") return true;
87
+ if (code !== "EPERM") return false;
88
+ try {
89
+ await lstat(lockPath);
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+ /** Whether the holder a `<pid>\n` record names is proven gone: a signal probe finds no such process. */
96
+ function holderExited(record) {
97
+ if (!/^\d+\n$/.test(record)) return false;
98
+ const pid = Number(record.trim());
99
+ if (pid === 0 || pid > 2147483647) return false;
100
+ if (pid === process.pid) return false;
101
+ try {
102
+ process.kill(pid, 0);
103
+ return false;
104
+ } catch (error) {
105
+ return error.code === "ESRCH";
106
+ }
107
+ }
108
+ /** The lock file's content, or undefined when it cannot be read. */
109
+ async function readLockRecord(lockPath) {
110
+ try {
111
+ return await readFile(lockPath, "utf8");
112
+ } catch (error) {
113
+ return;
114
+ }
115
+ }
116
+ /**
117
+ * Remove the lock when its recorded holder exited. Contenders that read the
118
+ * same record serialize on a claim file named after it. Under the claim, the
119
+ * claimant re-reads the lock and probes its PID again, and removes it only
120
+ * when it still holds that record and that PID is still gone: the record's
121
+ * holder can no longer release it, and no other contender can remove it
122
+ * without the claim, so a removal never deletes a lock another contender
123
+ * acquired after the dead holder's, including one whose holder reused the PID.
124
+ * @returns Whether this call removed the dead holder's lock.
125
+ */
126
+ async function takeOverExitedLock(lockPath) {
127
+ const record = await readLockRecord(lockPath);
128
+ if (record === void 0 || !holderExited(record)) return false;
129
+ const claim = `${lockPath}.takeover-${createHash("sha256").update(record).digest("hex").slice(0, 16)}`;
130
+ try {
131
+ await writeFile(claim, `${process.pid}\n`, {
132
+ mode: 384,
133
+ flag: "wx"
134
+ });
135
+ } catch (error) {
136
+ const code = error.code;
137
+ if (code === "EEXIST" || code === "EPERM") return false;
138
+ throw error;
139
+ }
140
+ try {
141
+ if (await readLockRecord(lockPath) !== record || !holderExited(record)) return false;
142
+ try {
143
+ await rm(lockPath, { force: true });
144
+ } catch (error) {
145
+ return false;
146
+ }
147
+ return true;
148
+ } finally {
149
+ await rm(claim, { force: true }).catch((error) => {});
150
+ }
151
+ }
152
+ /**
153
+ * Retry cadence for a contended lock. These stay robustness invariants of the
154
+ * cross-process write protocol rather than deployment tunables: they govern how
155
+ * often a contender asks, which no caller has a reason to vary.
156
+ */
157
+ const LOCK_RETRY_INITIAL_MS = 20;
158
+ const LOCK_RETRY_MAX_MS = 200;
159
+ /**
160
+ * How long a contender waits when the caller states no limit — sized for the
161
+ * render-and-rename cycle every call site had when this package was written.
162
+ * Expiry fails the contender rather than guessing whether the existing lock
163
+ * still has an owner. How long is *worth* waiting is a property of the
164
+ * operation the lock holder runs, which is why {@link FileLockOptions.waitMs}
165
+ * exists; the value here is the floor for an operation that does file work
166
+ * alone.
167
+ */
168
+ const DEFAULT_LOCK_WAIT_MS = 2e3;
169
+ /**
170
+ * Hold the cross-process writer lock for `filename` around one operation. The
171
+ * lock is a `wx`-created sibling (`<filename>.lock`); paired with the
172
+ * rename-based commit of {@link writeFileAtomic}, readers stay lock-free and
173
+ * only writers contend. `EEXIST` is contention directly; an `EPERM` is
174
+ * contention only when a fresh `lstat` confirms the lock path exists, covering
175
+ * Windows exclusive-create behavior. Windows retries one unconfirmed EPERM
176
+ * because the holder can release before the probe; a repeated unconfirmed
177
+ * permission error is rethrown. The lock records its holder's PID. A contender
178
+ * removes the lock and retries at once when no process with that PID exists
179
+ * (`ESRCH`); any other lock, including one whose holder exists under another
180
+ * user (`EPERM`) or whose record is incomplete, is waited for. Contention backs
181
+ * off exponentially and times out after the deadline. A holder whose PID a
182
+ * live process reused keeps its lock until an operator removes it. Takeover
183
+ * proves only that the recorded process exited: an operation that starts other
184
+ * writers must stop them with it or leave its successor a way to find them.
185
+ * PIDs are compared on the contender's host, so writers on other hosts or in
186
+ * other PID namespaces sharing the file are unsupported and could both hold
187
+ * the lock. The parent directory must exist.
188
+ * @param filename - the file whose writers this lock serializes.
189
+ * @param operation - the read-render-commit cycle to run while holding the lock.
190
+ * @param options - acquisition options; omitted waits {@link DEFAULT_LOCK_WAIT_MS}.
191
+ * @returns the operation's result; the lock releases on both outcomes.
192
+ */
193
+ async function withFileLock(filename, operation, options) {
194
+ const lockPath = `${filename}.lock`;
195
+ const deadline = Date.now() + (options?.waitMs ?? DEFAULT_LOCK_WAIT_MS);
196
+ let delay = LOCK_RETRY_INITIAL_MS;
197
+ let retriedUnconfirmedPermissionError = false;
198
+ for (;;) {
199
+ options?.signal?.throwIfAborted();
200
+ try {
201
+ await writeFile(lockPath, `${process.pid}\n`, {
202
+ mode: 384,
203
+ flag: "wx"
204
+ });
205
+ break;
206
+ } catch (error) {
207
+ if (!await isLockContention(error, lockPath)) {
208
+ if (process.platform !== "win32" || error?.code !== "EPERM" || retriedUnconfirmedPermissionError) throw error;
209
+ retriedUnconfirmedPermissionError = true;
210
+ } else if (await takeOverExitedLock(lockPath)) continue;
211
+ }
212
+ if (Date.now() >= deadline) throw new Error(`atomic-write: timed out waiting for the writer lock at ${lockPath}`);
213
+ await setTimeout$1(delay, void 0, { signal: options?.signal });
214
+ delay = Math.min(delay * 2, LOCK_RETRY_MAX_MS);
215
+ }
216
+ try {
217
+ options?.signal?.throwIfAborted();
218
+ return await operation();
219
+ } finally {
220
+ await rm(lockPath, { force: true });
221
+ }
222
+ }
223
+ //#endregion
9
224
  //#region src/app-version.ts
10
225
  /**
11
226
  * The international desktop app's version, used as the `/v3/config` UA.
@@ -1519,7 +1734,41 @@ function parseModelCatalog(data, international = false, promoBadges) {
1519
1734
  }
1520
1735
  const models = cliIds.map((id) => byId.get(id)).filter((model) => model !== void 0);
1521
1736
  if (models.length === 0) throw new Error("workbuddy model catalog resolved to an empty list");
1522
- return models;
1737
+ return disambiguateDisplayNames(models);
1738
+ }
1739
+ /**
1740
+ * Make display names unique within one catalog.
1741
+ *
1742
+ * The CN document names both `hy3` and `hy3-x` "Hy3" — two neighbouring rows
1743
+ * whose rates differ (`x0.00` free versus `x0.05`), so the picker and the probe
1744
+ * card render two identical rows and the user cannot tell which one they are
1745
+ * paying for. The static fallback spells the second one "Hy3-X", but the live
1746
+ * fetch overwrote that correction: as soon as the roster gained the id, the
1747
+ * collision came back through the parse path.
1748
+ *
1749
+ * The suffix comes from the id itself — whatever follows the shared name stem
1750
+ * — so `hy3-x` reads "Hy3-X" again. Ids are untouched: they are the wire
1751
+ * contract the request path and the shim route on. Only exact (case-
1752
+ * insensitive) collisions are rewritten; a distinct upstream name such as
1753
+ * `MiniMax-M3` for `minimax-m3-pay` keeps the upstream's own wording.
1754
+ */
1755
+ function disambiguateDisplayNames(models) {
1756
+ const seen = /* @__PURE__ */ new Set();
1757
+ return models.map((model) => {
1758
+ const key = model.name.trim().toLowerCase();
1759
+ if (!seen.has(key)) {
1760
+ seen.add(key);
1761
+ return model;
1762
+ }
1763
+ const tail = model.id.toLowerCase().startsWith(key) ? model.id.slice(key.length) : "";
1764
+ if (tail === "") return model;
1765
+ const name = `${model.name}${tail.toUpperCase()}`;
1766
+ seen.add(name.toLowerCase());
1767
+ return {
1768
+ ...model,
1769
+ name
1770
+ };
1771
+ });
1523
1772
  }
1524
1773
  /** Extract the promotions covering `model` from the `modelPromotions` array. */
1525
1774
  function parsePromotions(value, model) {
@@ -3436,12 +3685,12 @@ var WorkBuddyCredentialStore = class {
3436
3685
  */
3437
3686
  /**
3438
3687
  * Static CLI models observed on the CN endpoint (re-verified against the live
3439
- * `/v3/config` document 2026-09-23, including the thinking-effort and billing
3688
+ * `/v3/config` document 2026-10-17, including the thinking-effort and billing
3440
3689
  * metadata). The upstream refresh replaces this list at startup; it exists so
3441
3690
  * the provider registers with a usable catalog even while the first fetch is
3442
3691
  * in flight or offline.
3443
3692
  *
3444
- * The list tracks the `cli` agent's model roster exactly — the 16 models it
3693
+ * The list tracks the `cli` agent's model roster exactly — the 19 models it
3445
3694
  * offered that day. The roster churns quickly (`auto`, `kimi-k3-1`,
3446
3695
  * `minimax-m3` each appeared or vanished within days, and a competing patch's
3447
3696
  * 2026-09-22 snapshot named three ids that were gone a day later), so this
@@ -3452,6 +3701,11 @@ var WorkBuddyCredentialStore = class {
3452
3701
  * document, and the `free` flag follows the normalized `x0.00` credits
3453
3702
  * marker.
3454
3703
  *
3704
+ * Image capability is also verbatim, and it moved since the previous capture:
3705
+ * `glm-5.2` and `glm-5.1` now report `supportsImages: false`. Claiming it for
3706
+ * them admitted an attachment the upstream then rejected after the message was
3707
+ * durable, so this table follows the document rather than the old snapshot.
3708
+ *
3455
3709
  * Deliberately NOT baked in: promotional badges. `限时免费` and friends are
3456
3710
  * dynamic console-side promotions with no reliable validity window, so a
3457
3711
  * static table would keep them alive long after the offers end. The live
@@ -3463,7 +3717,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
3463
3717
  {
3464
3718
  id: "hy4-preview",
3465
3719
  name: "Hy4 preview",
3466
- contextWindow: 1e6,
3720
+ contextWindow: 96e4,
3467
3721
  maxTokens: 64e3,
3468
3722
  supportsImages: true,
3469
3723
  reasoning: {
@@ -3512,6 +3766,52 @@ const FALLBACK_WORKBUDDY_MODELS = [
3512
3766
  free: false
3513
3767
  }
3514
3768
  },
3769
+ {
3770
+ id: "step-5-preview",
3771
+ name: "Step-5-Preview",
3772
+ contextWindow: 1e6,
3773
+ maxTokens: 64e3,
3774
+ supportsImages: true,
3775
+ reasoning: {
3776
+ supports: true,
3777
+ onlyReasoning: true,
3778
+ supportedEfforts: [
3779
+ "low",
3780
+ "medium",
3781
+ "high"
3782
+ ],
3783
+ defaultEffort: "high",
3784
+ canDisableThinking: false
3785
+ },
3786
+ billing: {
3787
+ credits: "x0.00 credits",
3788
+ free: true
3789
+ }
3790
+ },
3791
+ {
3792
+ id: "space-bunny",
3793
+ name: "Space-Bunny",
3794
+ contextWindow: 1e6,
3795
+ maxTokens: 128e3,
3796
+ supportsImages: true,
3797
+ reasoning: {
3798
+ supports: true,
3799
+ onlyReasoning: true,
3800
+ supportedEfforts: [
3801
+ "low",
3802
+ "medium",
3803
+ "high",
3804
+ "xhigh",
3805
+ "max"
3806
+ ],
3807
+ defaultEffort: "max",
3808
+ canDisableThinking: false
3809
+ },
3810
+ billing: {
3811
+ credits: "x0.08 credits",
3812
+ free: false
3813
+ }
3814
+ },
3515
3815
  {
3516
3816
  id: "deepseek-v4.1-flash",
3517
3817
  name: "Deepseek-V4.1-Flash",
@@ -3521,11 +3821,16 @@ const FALLBACK_WORKBUDDY_MODELS = [
3521
3821
  reasoning: {
3522
3822
  supports: true,
3523
3823
  onlyReasoning: true,
3824
+ supportedEfforts: [
3825
+ "low",
3826
+ "high",
3827
+ "max"
3828
+ ],
3524
3829
  defaultEffort: "high",
3525
- canDisableThinking: false
3830
+ canDisableThinking: true
3526
3831
  },
3527
3832
  billing: {
3528
- credits: "x0.03 credits",
3833
+ credits: "x0.11 credits",
3529
3834
  free: false
3530
3835
  }
3531
3836
  },
@@ -3538,7 +3843,12 @@ const FALLBACK_WORKBUDDY_MODELS = [
3538
3843
  reasoning: {
3539
3844
  supports: true,
3540
3845
  onlyReasoning: true,
3541
- defaultEffort: "medium",
3846
+ supportedEfforts: [
3847
+ "low",
3848
+ "high",
3849
+ "max"
3850
+ ],
3851
+ defaultEffort: "max",
3542
3852
  canDisableThinking: false
3543
3853
  },
3544
3854
  billing: {
@@ -3561,19 +3871,41 @@ const FALLBACK_WORKBUDDY_MODELS = [
3561
3871
  "max"
3562
3872
  ],
3563
3873
  defaultEffort: "high",
3564
- canDisableThinking: true
3874
+ canDisableThinking: false
3565
3875
  },
3566
3876
  billing: {
3567
3877
  credits: "x0.06 credits",
3568
3878
  free: false
3569
3879
  }
3570
3880
  },
3881
+ {
3882
+ id: "glm-5.3-flashx",
3883
+ name: "GLM-5.3-FlashX",
3884
+ contextWindow: 1e6,
3885
+ maxTokens: 131072,
3886
+ supportsImages: true,
3887
+ reasoning: {
3888
+ supports: true,
3889
+ onlyReasoning: true,
3890
+ supportedEfforts: [
3891
+ "low",
3892
+ "high",
3893
+ "max"
3894
+ ],
3895
+ defaultEffort: "high",
3896
+ canDisableThinking: false
3897
+ },
3898
+ billing: {
3899
+ credits: "x0.14 credits",
3900
+ free: false
3901
+ }
3902
+ },
3571
3903
  {
3572
3904
  id: "glm-5.2",
3573
3905
  name: "GLM-5.2",
3574
3906
  contextWindow: 1e6,
3575
3907
  maxTokens: 64e3,
3576
- supportsImages: true,
3908
+ supportsImages: false,
3577
3909
  reasoning: {
3578
3910
  supports: true,
3579
3911
  onlyReasoning: true,
@@ -3590,7 +3922,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
3590
3922
  name: "GLM-5.1",
3591
3923
  contextWindow: 2e5,
3592
3924
  maxTokens: 48e3,
3593
- supportsImages: true,
3925
+ supportsImages: false,
3594
3926
  reasoning: {
3595
3927
  supports: true,
3596
3928
  onlyReasoning: true,
@@ -3620,10 +3952,10 @@ const FALLBACK_WORKBUDDY_MODELS = [
3620
3952
  }
3621
3953
  },
3622
3954
  {
3623
- id: "minimax-m3",
3955
+ id: "minimax-m3-pay",
3624
3956
  name: "MiniMax-M3",
3625
3957
  contextWindow: 512e3,
3626
- maxTokens: 64e3,
3958
+ maxTokens: 128e3,
3627
3959
  supportsImages: true,
3628
3960
  reasoning: {
3629
3961
  supports: true,
@@ -3649,12 +3981,12 @@ const FALLBACK_WORKBUDDY_MODELS = [
3649
3981
  canDisableThinking: false
3650
3982
  },
3651
3983
  billing: {
3652
- credits: "x0.19 credits",
3984
+ credits: "x0.26 credits",
3653
3985
  free: false
3654
3986
  }
3655
3987
  },
3656
3988
  {
3657
- id: "kimi-k3-1",
3989
+ id: "kimi-k3-2",
3658
3990
  name: "Kimi-K3",
3659
3991
  contextWindow: 1e6,
3660
3992
  maxTokens: 32e3,
@@ -3666,7 +3998,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
3666
3998
  canDisableThinking: false
3667
3999
  },
3668
4000
  billing: {
3669
- credits: "x1.62 credits",
4001
+ credits: "x1.62",
3670
4002
  free: false
3671
4003
  }
3672
4004
  },
@@ -4210,7 +4542,7 @@ var WorkBuddyCatalog = class {
4210
4542
  };
4211
4543
  //#endregion
4212
4544
  //#region src/version.ts
4213
- const WORKBUDDY_CONNECT_VERSION = "0.7.7";
4545
+ const WORKBUDDY_CONNECT_VERSION = "0.7.9";
4214
4546
  //#endregion
4215
4547
  //#region src/host-heartbeat.ts
4216
4548
  /**
package/lib/index.d.ts CHANGED
@@ -1129,12 +1129,12 @@ declare class WorkBuddyCredentialStore {
1129
1129
  type WorkBuddyModelInfo = WorkBuddyUpstreamModel;
1130
1130
  /**
1131
1131
  * Static CLI models observed on the CN endpoint (re-verified against the live
1132
- * `/v3/config` document 2026-09-23, including the thinking-effort and billing
1132
+ * `/v3/config` document 2026-10-17, including the thinking-effort and billing
1133
1133
  * metadata). The upstream refresh replaces this list at startup; it exists so
1134
1134
  * the provider registers with a usable catalog even while the first fetch is
1135
1135
  * in flight or offline.
1136
1136
  *
1137
- * The list tracks the `cli` agent's model roster exactly — the 16 models it
1137
+ * The list tracks the `cli` agent's model roster exactly — the 19 models it
1138
1138
  * offered that day. The roster churns quickly (`auto`, `kimi-k3-1`,
1139
1139
  * `minimax-m3` each appeared or vanished within days, and a competing patch's
1140
1140
  * 2026-09-22 snapshot named three ids that were gone a day later), so this
@@ -1145,6 +1145,11 @@ type WorkBuddyModelInfo = WorkBuddyUpstreamModel;
1145
1145
  * document, and the `free` flag follows the normalized `x0.00` credits
1146
1146
  * marker.
1147
1147
  *
1148
+ * Image capability is also verbatim, and it moved since the previous capture:
1149
+ * `glm-5.2` and `glm-5.1` now report `supportsImages: false`. Claiming it for
1150
+ * them admitted an attachment the upstream then rejected after the message was
1151
+ * durable, so this table follows the document rather than the old snapshot.
1152
+ *
1148
1153
  * Deliberately NOT baked in: promotional badges. `限时免费` and friends are
1149
1154
  * dynamic console-side promotions with no reliable validity window, so a
1150
1155
  * static table would keep them alive long after the offers end. The live