@giovannijecha/jecode 0.8.4 → 0.8.5

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 (53) hide show
  1. package/README.md +8 -6
  2. package/dist/accounts.js +47 -10
  3. package/dist/atomic.js +24 -14
  4. package/dist/batch.js +37 -4
  5. package/dist/bounded-file.js +212 -0
  6. package/dist/commands.js +2 -0
  7. package/dist/config.js +2 -1
  8. package/dist/context/automatic.js +35 -0
  9. package/dist/context/compactor.js +32 -2
  10. package/dist/context/manual.js +20 -3
  11. package/dist/context/request-projection.js +130 -0
  12. package/dist/controller-request.js +33 -11
  13. package/dist/credential-commands.js +22 -6
  14. package/dist/credentials.js +56 -18
  15. package/dist/directory-anchor.js +91 -0
  16. package/dist/file-identity.js +12 -0
  17. package/dist/model-command.js +5 -4
  18. package/dist/openai-account-command.js +13 -2
  19. package/dist/process-lease.js +329 -0
  20. package/dist/provider-commands.js +11 -5
  21. package/dist/provider-errors.js +37 -4
  22. package/dist/provider-label.js +13 -0
  23. package/dist/providers/anthropic-stream.js +4 -1
  24. package/dist/providers/anthropic-wire.js +8 -3
  25. package/dist/providers/anthropic.js +39 -19
  26. package/dist/providers/catalog.js +4 -4
  27. package/dist/providers/failure.js +181 -0
  28. package/dist/providers/http.js +82 -23
  29. package/dist/providers/ollama-stream.js +5 -1
  30. package/dist/providers/ollama.js +31 -19
  31. package/dist/providers/openai-codex.js +67 -41
  32. package/dist/providers/openai-stream.js +26 -2
  33. package/dist/providers/openai.js +51 -24
  34. package/dist/providers/sse.js +52 -8
  35. package/dist/request-identity.js +32 -0
  36. package/dist/sessions/lease.js +132 -49
  37. package/dist/sessions/runtime.js +15 -8
  38. package/dist/sessions/store.js +366 -142
  39. package/dist/settings.js +62 -10
  40. package/dist/stable-directory.js +148 -0
  41. package/dist/store-lock.js +68 -84
  42. package/dist/tools/args.js +2 -2
  43. package/dist/tools/fs.js +124 -102
  44. package/dist/tools/search.js +65 -100
  45. package/dist/tools/text-boundary.js +7 -33
  46. package/dist/tui/app-workflows.js +32 -4
  47. package/dist/tui/components/footer.js +1 -1
  48. package/dist/tui/feedback.js +4 -0
  49. package/dist/tui/session-view.js +7 -2
  50. package/dist/tui/workspace.js +21 -7
  51. package/dist/user-store.js +23 -31
  52. package/package.json +3 -4
  53. package/dist/tools/ripgrep.js +0 -230
@@ -4,8 +4,9 @@ import { openAICodexAccount } from "../accounts.js";
4
4
  import { openAIAuthorization } from "../openai-account.js";
5
5
  import { applicationVersion } from "../version.js";
6
6
  import { EFFORTS, isEffort, requireSupportedEffort } from "../effort.js";
7
+ import { isRetryableGenerationFailure, isRetryableReadFailure, throwProviderError, } from "./failure.js";
7
8
  import { getJson, postSse } from "./http.js";
8
- import { assembleOpenAI } from "./openai-stream.js";
9
+ import { assembleOpenAI, openAIStreamProgress } from "./openai-stream.js";
9
10
  import { fromWireResponse, stopNotice, toWireItems, toWireTool, } from "./openai-wire.js";
10
11
  const ID = "openai-codex";
11
12
  const BASE = "https://chatgpt.com/backend-api/codex";
@@ -13,7 +14,6 @@ const BASE = "https://chatgpt.com/backend-api/codex";
13
14
  // own catalogue updater uses this sentinel to request the complete current
14
15
  // manifest; Jecode then keeps only entries explicitly visible in that manifest.
15
16
  const CATALOG_COMPATIBILITY_VERSION = "99.99.99";
16
- const SESSION_ID = randomUUID();
17
17
  const MAX_CATALOG_ITEMS = 4_000;
18
18
  const MAX_MODELS = 1_000;
19
19
  const MAX_MODEL_CHARS = 256;
@@ -29,60 +29,84 @@ export const openaiCodex = {
29
29
  },
30
30
  location: () => "cloud",
31
31
  async models(signal, onStatus) {
32
- const catalog = await loadCatalog(signal, onStatus);
33
- rememberCatalog(catalog);
34
- return catalog.ids;
32
+ try {
33
+ const catalog = await loadCatalog(signal, onStatus);
34
+ rememberCatalog(catalog);
35
+ return catalog.ids;
36
+ }
37
+ catch (error) {
38
+ throwProviderError(ID, signal, error);
39
+ }
35
40
  },
36
41
  async efforts(model, signal, onStatus) {
37
42
  const cached = effortByModel.get(model);
38
43
  if (cached !== undefined)
39
44
  return cached;
40
- const catalog = await loadCatalog(signal, onStatus);
41
- rememberCatalog(catalog);
42
- return effortByModel.get(model) ?? fallbackEfforts(model);
45
+ try {
46
+ const catalog = await loadCatalog(signal, onStatus);
47
+ rememberCatalog(catalog);
48
+ return effortByModel.get(model) ?? fallbackEfforts(model);
49
+ }
50
+ catch (error) {
51
+ throwProviderError(ID, signal, error);
52
+ }
43
53
  },
44
54
  async contextWindow(model, signal, onStatus) {
45
55
  if (contextByModel.has(model))
46
56
  return contextByModel.get(model);
47
- const catalog = await loadCatalog(signal, onStatus);
48
- rememberCatalog(catalog);
49
- const context = contextByModel.get(model);
50
- if (!contextByModel.has(model))
51
- contextByModel.set(model, undefined);
52
- return context;
57
+ try {
58
+ const catalog = await loadCatalog(signal, onStatus);
59
+ rememberCatalog(catalog);
60
+ const context = contextByModel.get(model);
61
+ if (!contextByModel.has(model))
62
+ contextByModel.set(model, undefined);
63
+ return context;
64
+ }
65
+ catch (error) {
66
+ throwProviderError(ID, signal, error);
67
+ }
53
68
  },
54
69
  async send(req) {
55
70
  const efforts = effortByModel.get(req.model) ?? fallbackEfforts(req.model);
56
71
  const effort = requireSupportedEffort(req.model, req.effort, efforts);
57
- return withAuthorization(async (authorization) => {
58
- const events = await postSse(`${BASE}/responses`, {
59
- ...headers(authorization, randomUUID()),
60
- "openai-beta": "responses=experimental",
61
- }, {
62
- model: req.model,
63
- store: false,
64
- stream: true,
65
- instructions: req.system,
66
- input: req.messages.flatMap((message) => toWireItems(message, ID)),
67
- tools: req.tools.map(toWireTool),
68
- tool_choice: "auto",
69
- parallel_tool_calls: true,
70
- reasoning: { effort, summary: "auto" },
71
- text: { verbosity: "low" },
72
- include: ["reasoning.encrypted_content"],
73
- prompt_cache_key: SESSION_ID,
74
- }, req.maxTokens, req.signal, req.onStatus);
75
- const data = await assembleOpenAI(events, req.onStream, req.onStatus);
76
- const notice = stopNotice(data);
77
- if (notice !== undefined)
78
- req.onStream?.({ kind: "text", text: `\n${notice}` });
79
- return fromWireResponse(data, ID);
80
- }, req.signal, req.onStatus);
72
+ const sessionId = req.identity?.conversationId ?? randomUUID();
73
+ const cacheKey = req.identity?.cacheKey ?? sessionId;
74
+ try {
75
+ return await withAuthorization(async (authorization) => {
76
+ const events = await postSse(`${BASE}/responses`, {
77
+ ...headers(authorization, sessionId, randomUUID()),
78
+ "openai-beta": "responses=experimental",
79
+ }, {
80
+ model: req.model,
81
+ store: false,
82
+ stream: true,
83
+ instructions: req.system,
84
+ input: req.messages.flatMap((message) => toWireItems(message, ID)),
85
+ tools: req.tools.map(toWireTool),
86
+ tool_choice: "auto",
87
+ parallel_tool_calls: true,
88
+ reasoning: { effort, summary: "auto" },
89
+ text: { verbosity: "low" },
90
+ include: ["reasoning.encrypted_content"],
91
+ ...(req.identity?.purpose === "compaction"
92
+ ? {}
93
+ : { prompt_cache_key: cacheKey }),
94
+ }, req.maxTokens, req.signal, req.onStatus, openAIStreamProgress, (error) => isRetryableGenerationFailure(ID, error));
95
+ const data = await assembleOpenAI(events, req.onStream, req.onStatus);
96
+ const notice = stopNotice(data);
97
+ if (notice !== undefined)
98
+ req.onStream?.({ kind: "text", text: `\n${notice}` });
99
+ return fromWireResponse(data, ID);
100
+ }, req.signal, req.onStatus);
101
+ }
102
+ catch (error) {
103
+ throwProviderError(ID, req.signal, error);
104
+ }
81
105
  },
82
106
  };
83
107
  async function loadCatalog(signal, onStatus) {
84
108
  return withAuthorization(async (authorization) => {
85
- const body = await getJson(`${BASE}/models?client_version=${CATALOG_COMPATIBILITY_VERSION}`, headers(authorization, randomUUID()), signal, onStatus);
109
+ const body = await getJson(`${BASE}/models?client_version=${CATALOG_COMPATIBILITY_VERSION}`, headers(authorization, randomUUID(), randomUUID()), signal, onStatus, (error) => isRetryableReadFailure(ID, error));
86
110
  return modelCatalog(body);
87
111
  }, signal, onStatus);
88
112
  }
@@ -102,14 +126,14 @@ async function withAuthorization(operation, signal, onStatus) {
102
126
  return operation(authorization);
103
127
  }
104
128
  }
105
- function headers(authorization, requestId) {
129
+ function headers(authorization, sessionId, requestId) {
106
130
  const version = applicationVersion();
107
131
  return {
108
132
  authorization: `Bearer ${authorization.accessToken}`,
109
133
  "chatgpt-account-id": authorization.accountId,
110
134
  originator: "jecode",
111
135
  "user-agent": `jecode/${version} (${process.platform}; ${process.arch})`,
112
- "session-id": SESSION_ID,
136
+ "session-id": sessionId,
113
137
  "x-client-request-id": requestId,
114
138
  };
115
139
  }
@@ -185,6 +209,8 @@ function reasoningLevels(entry, model) {
185
209
  return efforts.length === 0 ? fallbackEfforts(model) : efforts;
186
210
  }
187
211
  function fallbackEfforts(model) {
212
+ if (/^gpt-6-astra(?:-|$)/.test(model))
213
+ return EFFORTS;
188
214
  if (/^gpt-5\.6-(?:sol|terra|luna)(?:-|$)/.test(model))
189
215
  return EFFORTS;
190
216
  return XHIGH_EFFORTS;
@@ -4,6 +4,7 @@
4
4
  // response in `response.completed`. The ChatGPT Codex backend can instead send
5
5
  // an empty final `output` after complete `response.output_item.done` events, so
6
6
  // those streamed items remain the fallback when the final envelope is empty.
7
+ import { providerWireError } from "./failure.js";
7
8
  export async function assembleOpenAI(events, onStream, onStatus) {
8
9
  const items = [];
9
10
  const announcedTools = { identities: new Set(), anonymous: false };
@@ -69,6 +70,9 @@ export async function assembleOpenAI(events, onStream, onStatus) {
69
70
  if (isFunctionCall(event.item)) {
70
71
  announceTool(event, event.item, announcedTools, onStream, status);
71
72
  }
73
+ else if (itemType(event.item) === "reasoning") {
74
+ status("Working");
75
+ }
72
76
  items.push(event.item);
73
77
  }
74
78
  break;
@@ -82,16 +86,36 @@ export async function assembleOpenAI(events, onStream, onStatus) {
82
86
  };
83
87
  case "response.failed": {
84
88
  const response = event.response;
85
- throw new Error(`openai stream error: ${response?.error?.message ?? "unspecified"}`);
89
+ throw providerWireError("openai stream error", response?.error?.message, {
90
+ code: response?.error?.code,
91
+ type: response?.error?.type,
92
+ });
86
93
  }
87
94
  case "error":
88
- throw new Error(`openai stream error: ${event.error?.message ?? event.message ?? "unspecified"}`);
95
+ throw providerWireError("openai stream error", event.error?.message ?? event.message, { code: event.error?.code, type: event.error?.type });
89
96
  default:
90
97
  break;
91
98
  }
92
99
  }
93
100
  throw new Error("openai stream ended before a terminal response event");
94
101
  }
102
+ /** State-only keepalives prove transport liveness, not forward model progress. */
103
+ export function openAIStreamProgress(raw) {
104
+ if (typeof raw !== "object" || raw === null)
105
+ return false;
106
+ const type = raw["type"];
107
+ if (typeof type !== "string")
108
+ return false;
109
+ if (type === "response.created")
110
+ return true;
111
+ if (type === "response.done" ||
112
+ type === "response.completed" ||
113
+ type === "response.incomplete" ||
114
+ type === "response.failed" ||
115
+ type === "error")
116
+ return true;
117
+ return /\.(?:added|delta|done)$/u.test(type);
118
+ }
95
119
  function isFunctionCall(item) {
96
120
  return typeof item === "object" && item !== null &&
97
121
  item["type"] === "function_call";
@@ -2,15 +2,20 @@
2
2
  //
3
3
  // Responses wire contract verified against the official API reference on
4
4
  // 2026-08-29. Keep final response events authoritative over display deltas.
5
+ import { randomUUID } from "node:crypto";
6
+ import { applicationVersion } from "../version.js";
5
7
  import { postSse } from "./http.js";
6
8
  import { listModels } from "./catalog.js";
7
9
  import { keyFor } from "../credentials.js";
8
10
  import { EFFORTS, requireSupportedEffort } from "../effort.js";
9
- import { assembleOpenAI } from "./openai-stream.js";
11
+ import { isRetryableGenerationFailure, isRetryableReadFailure, throwProviderError, } from "./failure.js";
12
+ import { assembleOpenAI, openAIStreamProgress } from "./openai-stream.js";
10
13
  import { fromWireResponse, stopNotice, toWireItems, toWireTool, } from "./openai-wire.js";
11
14
  const ENDPOINT = "https://api.openai.com/v1/responses";
12
15
  const MODELS = "https://api.openai.com/v1/models";
13
16
  const KEY = "OPENAI_API_KEY";
17
+ const ID = "openai";
18
+ const ASTRA_MODEL = /^gpt-6-astra(?:-|$)/;
14
19
  const RESPONSES_REASONING_MODEL = /^(?:gpt-5(?:[.-]|$)|o(?:1|3|4)(?:[.-]|$)|codex-mini(?:[.-]|$))/;
15
20
  // Jecode's transport always streams and always declares local tools. Hide
16
21
  // catalog entries that cannot satisfy either half of that contract.
@@ -20,11 +25,14 @@ const XHIGH_EFFORTS = ["low", "medium", "high", "xhigh"];
20
25
  const PRO_EFFORTS = ["medium", "high", "xhigh"];
21
26
  const HIGH_ONLY_EFFORT = ["high"];
22
27
  export function supportsOpenAIModel(model) {
23
- return RESPONSES_REASONING_MODEL.test(model) && !INCOMPATIBLE_MODEL.test(model);
28
+ return ASTRA_MODEL.test(model) ||
29
+ (RESPONSES_REASONING_MODEL.test(model) && !INCOMPATIBLE_MODEL.test(model));
24
30
  }
25
31
  export function openAIEfforts(model) {
26
32
  if (!supportsOpenAIModel(model))
27
33
  return [];
34
+ if (ASTRA_MODEL.test(model))
35
+ return EFFORTS;
28
36
  if (/^gpt-5-pro(?:-|$)/.test(model))
29
37
  return HIGH_ONLY_EFFORT;
30
38
  if (/^gpt-5\.[2-5]-pro(?:-|$)/.test(model))
@@ -39,6 +47,8 @@ export function openAIEfforts(model) {
39
47
  }
40
48
  /** Conservative capacities for the reasoning families accepted by this transport. */
41
49
  export function openAIContextWindow(model) {
50
+ if (ASTRA_MODEL.test(model))
51
+ return usableContext(1_050_000);
42
52
  if (/^gpt-5\.6(?:[.-]|$)/.test(model))
43
53
  return usableContext(1_050_000);
44
54
  if (/^gpt-5(?:[.-]|$)/.test(model))
@@ -52,7 +62,7 @@ function usableContext(tokens) {
52
62
  return Object.freeze({ tokens: Math.floor(tokens * 95 / 100) });
53
63
  }
54
64
  export const openai = {
55
- id: "openai",
65
+ id: ID,
56
66
  defaultModel: "gpt-5",
57
67
  auth: { kind: "api-key", keyVar: KEY },
58
68
  blocked() {
@@ -61,10 +71,15 @@ export const openai = {
61
71
  // The endpoint answers in no order worth keeping, so descending puts the
62
72
  // highest-numbered family — usually the newest — at the top of the menu.
63
73
  async models(signal, onStatus) {
64
- const ids = await listModels(MODELS, headers(requireKey()), signal, onStatus);
65
- return ids
66
- .filter(supportsOpenAIModel)
67
- .sort((a, b) => b.localeCompare(a));
74
+ try {
75
+ const ids = await listModels(MODELS, headers(requireKey()), signal, onStatus, (error) => isRetryableReadFailure(ID, error));
76
+ return ids
77
+ .filter(supportsOpenAIModel)
78
+ .sort((a, b) => b.localeCompare(a));
79
+ }
80
+ catch (error) {
81
+ throwProviderError(ID, signal, error);
82
+ }
68
83
  },
69
84
  async efforts(model) {
70
85
  return openAIEfforts(model);
@@ -76,22 +91,30 @@ export const openai = {
76
91
  async send(req) {
77
92
  const key = requireKey();
78
93
  const effort = requireSupportedEffort(req.model, req.effort, openAIEfforts(req.model));
79
- const events = await postSse(ENDPOINT, headers(key), {
80
- model: req.model,
81
- instructions: req.system,
82
- input: req.messages.flatMap((message) => toWireItems(message)),
83
- tools: req.tools.map(toWireTool),
84
- max_output_tokens: req.maxTokens,
85
- reasoning: { effort, summary: "auto" },
86
- store: false,
87
- include: ["reasoning.encrypted_content"],
88
- stream: true,
89
- }, req.maxTokens, req.signal, req.onStatus);
90
- const data = await assembleOpenAI(events, req.onStream, req.onStatus);
91
- const notice = stopNotice(data);
92
- if (notice !== undefined)
93
- req.onStream?.({ kind: "text", text: `\n${notice}` });
94
- return fromWireResponse(data);
94
+ try {
95
+ const events = await postSse(ENDPOINT, headers(key), {
96
+ model: req.model,
97
+ instructions: req.system,
98
+ input: req.messages.flatMap((message) => toWireItems(message)),
99
+ tools: req.tools.map(toWireTool),
100
+ max_output_tokens: req.maxTokens,
101
+ reasoning: { effort, summary: "auto" },
102
+ store: false,
103
+ include: ["reasoning.encrypted_content"],
104
+ stream: true,
105
+ ...(req.identity?.purpose === "turn"
106
+ ? { prompt_cache_key: req.identity.cacheKey }
107
+ : {}),
108
+ }, req.maxTokens, req.signal, req.onStatus, openAIStreamProgress, (error) => isRetryableGenerationFailure(ID, error));
109
+ const data = await assembleOpenAI(events, req.onStream, req.onStatus);
110
+ const notice = stopNotice(data);
111
+ if (notice !== undefined)
112
+ req.onStream?.({ kind: "text", text: `\n${notice}` });
113
+ return fromWireResponse(data);
114
+ }
115
+ catch (error) {
116
+ throwProviderError(ID, req.signal, error);
117
+ }
95
118
  },
96
119
  };
97
120
  function apiKey() {
@@ -104,5 +127,9 @@ function requireKey() {
104
127
  return key;
105
128
  }
106
129
  function headers(key) {
107
- return { authorization: `Bearer ${key}` };
130
+ return {
131
+ authorization: `Bearer ${key}`,
132
+ "user-agent": `jecode/${applicationVersion()} (${process.platform}; ${process.arch})`,
133
+ "x-client-request-id": randomUUID(),
134
+ };
108
135
  }
@@ -10,6 +10,9 @@ export async function* readSseJson(body, maximumChars, idle) {
10
10
  const parser = new SseEventParser();
11
11
  let finished = false;
12
12
  let total = 0;
13
+ const progressDeadline = idle?.progress === undefined
14
+ ? undefined
15
+ : eventDeadline(idle.progress);
13
16
  try {
14
17
  let ended = false;
15
18
  while (!ended) {
@@ -20,7 +23,11 @@ export async function* readSseJson(body, maximumChars, idle) {
20
23
  const payloads = [];
21
24
  try {
22
25
  while (payloads.length === 0 && !ended) {
23
- const { done, value } = await deadline.wait(reader.read());
26
+ const read = reader.read();
27
+ const pending = progressDeadline === undefined
28
+ ? read
29
+ : progressDeadline.wait(read);
30
+ const { done, value } = await deadline.wait(pending);
24
31
  if (done) {
25
32
  ended = true;
26
33
  const text = decoder.decode();
@@ -39,29 +46,66 @@ export async function* readSseJson(body, maximumChars, idle) {
39
46
  finally {
40
47
  deadline.clear();
41
48
  }
42
- for (const payload of payloads)
49
+ for (const payload of payloads) {
50
+ if (idle?.progress?.observed(payload) === true)
51
+ progressDeadline?.reset();
43
52
  yield payload;
53
+ }
44
54
  }
45
55
  finished = true;
46
56
  }
47
57
  finally {
58
+ progressDeadline?.clear();
48
59
  if (!finished)
49
60
  await reader.cancel().catch(() => undefined);
50
61
  reader.releaseLock();
51
62
  }
52
63
  }
53
64
  function eventDeadline(idle) {
54
- if (idle === undefined)
55
- return { wait: (pending) => pending, clear: () => undefined };
65
+ if (idle === undefined) {
66
+ return {
67
+ wait: (pending) => pending,
68
+ reset: () => undefined,
69
+ clear: () => undefined,
70
+ };
71
+ }
56
72
  let timer;
57
- const expired = new Promise((_resolve, reject) => {
58
- timer = setTimeout(() => reject(idle.error()), idle.milliseconds);
59
- });
73
+ let expired;
74
+ let rejectWait;
75
+ const arm = () => {
76
+ if (timer !== undefined)
77
+ clearTimeout(timer);
78
+ expired = undefined;
79
+ timer = setTimeout(() => {
80
+ expired = idle.error();
81
+ const reject = rejectWait;
82
+ rejectWait = undefined;
83
+ reject?.(expired);
84
+ }, idle.milliseconds);
85
+ };
86
+ arm();
60
87
  return {
61
- wait: (pending) => Promise.race([pending, expired]),
88
+ wait: (pending) => {
89
+ if (expired !== undefined)
90
+ return Promise.reject(expired);
91
+ return new Promise((resolve, reject) => {
92
+ rejectWait = reject;
93
+ pending.then((value) => {
94
+ if (rejectWait === reject)
95
+ rejectWait = undefined;
96
+ resolve(value);
97
+ }, (error) => {
98
+ if (rejectWait === reject)
99
+ rejectWait = undefined;
100
+ reject(error);
101
+ });
102
+ });
103
+ },
104
+ reset: arm,
62
105
  clear: () => {
63
106
  if (timer !== undefined)
64
107
  clearTimeout(timer);
108
+ rejectWait = undefined;
65
109
  },
66
110
  };
67
111
  }
@@ -0,0 +1,32 @@
1
+ // Stable provider-facing conversation identity. It is routing metadata only,
2
+ // never authorization, persistence authority, or a user-visible identifier.
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ const ephemeralSeeds = new WeakMap();
5
+ export function requestIdentityForSession(session) {
6
+ let conversation = session.persistence?.conversationId;
7
+ if (conversation === undefined) {
8
+ conversation = ephemeralSeeds.get(session);
9
+ if (conversation === undefined) {
10
+ conversation = randomUUID();
11
+ ephemeralSeeds.set(session, conversation);
12
+ }
13
+ }
14
+ return identityFromSeed(`${session.provider.id}\0${conversation}`);
15
+ }
16
+ export function resetRequestIdentity(session) {
17
+ ephemeralSeeds.delete(session);
18
+ }
19
+ function identityFromSeed(seed) {
20
+ const digest = createHash("sha256").update(seed).digest("hex");
21
+ const conversationId = [
22
+ digest.slice(0, 8),
23
+ digest.slice(8, 12),
24
+ `5${digest.slice(13, 16)}`,
25
+ `${variant(digest[16])}${digest.slice(17, 20)}`,
26
+ digest.slice(20, 32),
27
+ ].join("-");
28
+ return Object.freeze({ conversationId, cacheKey: `jecode-${digest.slice(0, 32)}` });
29
+ }
30
+ function variant(value) {
31
+ return ((Number.parseInt(value, 16) & 0x3) | 0x8).toString(16);
32
+ }