@lamplitisles/codex-for-love 0.4.3 → 0.5.0

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 +1 @@
1
- {"version":"1789639998459"}
1
+ {"version":"1789828770771"}
@@ -1,26 +1,27 @@
1
1
  <!doctype html>
2
2
  <html lang="en" data-theme="sticker-messenger">
3
- <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /><script>(function(){try{var appearance=localStorage.getItem('lamplitisles.companion.appearance');var language=localStorage.getItem('lamplitisles.companion.language');var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;var dark=appearance==='dark'||(appearance!=='light'&&systemDark);document.documentElement.dataset.theme=dark?'night-voyage':'sticker-messenger';document.documentElement.lang=language==='zh'?'zh-Hans':'en'}catch(_){var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.dataset.theme=systemDark?'night-voyage':'sticker-messenger'}})();</script><link href="/_app/immutable/entry/start.DN_PDYWk.js" rel="modulepreload">
4
- <link href="/_app/immutable/chunks/tYXWoiZl.js" rel="modulepreload">
5
- <link href="/_app/immutable/chunks/CTZw3jzS.js" rel="modulepreload">
6
- <link href="/_app/immutable/entry/app.C3QvzSBj.js" rel="modulepreload">
3
+ <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /><script>(function(){try{var appearance=localStorage.getItem('her.companion.appearance');var language=localStorage.getItem('her.companion.language');var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;var dark=appearance==='dark'||(appearance!=='light'&&systemDark);document.documentElement.dataset.theme=dark?'night-voyage':'sticker-messenger';document.documentElement.lang=language==='zh'?'zh-Hans':'en'}catch(_){var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.dataset.theme=systemDark?'night-voyage':'sticker-messenger'}})();</script><link href="/_app/immutable/entry/start.DWuEW6S3.js" rel="modulepreload">
4
+ <link href="/_app/immutable/chunks/DFTMMhXH.js" rel="modulepreload">
5
+ <link href="/_app/immutable/chunks/C2DTc7yw.js" rel="modulepreload">
6
+ <link href="/_app/immutable/chunks/CyqZh0gB.js" rel="modulepreload">
7
+ <link href="/_app/immutable/entry/app.Ci6fUWwj.js" rel="modulepreload">
7
8
  <link href="/_app/immutable/chunks/uBIymjUX.js" rel="modulepreload">
8
9
  <link href="/_app/immutable/chunks/xihTtKlq.js" rel="modulepreload">
9
- <link href="/_app/immutable/nodes/0.PSG_XCTv.js" rel="modulepreload">
10
+ <link href="/_app/immutable/nodes/0.C8SV4li-.js" rel="modulepreload">
10
11
 
11
- <link href="/_app/immutable/assets/0.DfSR8oPO.css" rel="stylesheet"></head>
12
+ <link href="/_app/immutable/assets/0.CkWBasWr.css" rel="stylesheet"></head>
12
13
  <body data-sveltekit-preload-data="hover"><div style="display: contents">
13
14
  <script>
14
15
  {
15
- __sveltekit_5wcjb2 = {
16
+ __sveltekit_17ylz81 = {
16
17
  base: ""
17
18
  };
18
19
 
19
20
  const element = document.currentScript.parentElement;
20
21
 
21
22
  Promise.all([
22
- import("/_app/immutable/entry/start.DN_PDYWk.js"),
23
- import("/_app/immutable/entry/app.C3QvzSBj.js")
23
+ import("/_app/immutable/entry/start.DWuEW6S3.js"),
24
+ import("/_app/immutable/entry/app.Ci6fUWwj.js")
24
25
  ]).then(([kit, app]) => {
25
26
  kit.start(app, element);
26
27
  });
@@ -3,6 +3,7 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
3
3
  import { access, copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { isIP } from "node:net";
6
7
  import { zstdDecompressSync } from "node:zlib";
7
8
  import { spawn } from "child_process";
8
9
  import { once } from "events";
@@ -12,7 +13,7 @@ import { delimiter, dirname as dirname$1, isAbsolute as isAbsolute$1, join as jo
12
13
  import { randomUUID as randomUUID$1 } from "crypto";
13
14
  import { createInterface } from "readline";
14
15
  import { homedir } from "os";
15
- import { createConnection, isIP } from "net";
16
+ import { createConnection, isIP as isIP$1 } from "net";
16
17
  import { DatabaseSync } from "node:sqlite";
17
18
  import { execFile } from "node:child_process";
18
19
  import { promisify } from "node:util";
@@ -6196,6 +6197,7 @@ const schema = object({
6196
6197
  user: string().min(1)
6197
6198
  }).strict().optional(),
6198
6199
  port: number$1().int().min(1024).max(65535).default(3082),
6200
+ listen_host: string().refine((value) => isIP(value) === 4, "Listen host must be an IPv4 address").default("127.0.0.1"),
6199
6201
  codex: object({
6200
6202
  command: string().min(1).default("codex-app-server"),
6201
6203
  model: string().min(1).default(DEFAULT_CODEX_MODEL),
@@ -61937,7 +61939,7 @@ function validateBearerToken(value) {
61937
61939
  function isLoopbackHostname(hostname) {
61938
61940
  const normalized = hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
61939
61941
  if (normalized === "localhost") return true;
61940
- const family = isIP(normalized);
61942
+ const family = isIP$1(normalized);
61941
61943
  if (family === 4) return normalized.startsWith("127.");
61942
61944
  if (family === 6) return normalized === "::1" || normalized.startsWith("::ffff:127.");
61943
61945
  return false;
@@ -62597,7 +62599,8 @@ function partnerPaths(workspace) {
62597
62599
  database: join(managedRoot, "session.sqlite"),
62598
62600
  relationshipJournal: join(managedRoot, "relationship.jsonl"),
62599
62601
  attachments: join(managedRoot, "attachments"),
62600
- audio: join(managedRoot, "audio")
62602
+ audio: join(managedRoot, "audio"),
62603
+ conversationImages: join(managedRoot, "conversation-images.json")
62601
62604
  };
62602
62605
  }
62603
62606
  //#endregion
@@ -65478,6 +65481,80 @@ async function readClip(root, name, frameCount) {
65478
65481
  return;
65479
65482
  }
65480
65483
  }
65484
+ //#endregion
65485
+ //#region apps/partner/runtime/conversation-images.ts
65486
+ const CURSOR_BYTES = 40;
65487
+ const CURSOR_TOKEN = /^[A-Za-z0-9_-]{54}$/u;
65488
+ const IMAGE_ID = /^[a-f0-9]{64}$/u;
65489
+ function cursorFor(image) {
65490
+ if (!Number.isSafeInteger(image.created) || image.created < 0 || !IMAGE_ID.test(image.id)) throw new Error("Invalid conversation image cursor data");
65491
+ const value = Buffer.alloc(CURSOR_BYTES);
65492
+ value.writeBigUInt64BE(BigInt(image.created));
65493
+ Buffer.from(image.id, "hex").copy(value, 8);
65494
+ return value.toString("base64url");
65495
+ }
65496
+ function readCursor(cursor) {
65497
+ if (!cursor) return void 0;
65498
+ if (!CURSOR_TOKEN.test(cursor)) return void 0;
65499
+ try {
65500
+ const value = Buffer.from(cursor, "base64url");
65501
+ if (value.length !== CURSOR_BYTES) return void 0;
65502
+ const created = Number(value.readBigUInt64BE());
65503
+ if (!Number.isSafeInteger(created)) return void 0;
65504
+ return [created, value.subarray(8).toString("hex")];
65505
+ } catch {
65506
+ return;
65507
+ }
65508
+ }
65509
+ function conversationImageId(origin, source) {
65510
+ return createHash("sha256").update(`${origin}:${source}`).digest("hex");
65511
+ }
65512
+ function pageConversationImages(images, limit = 5, cursor) {
65513
+ const after = readCursor(cursor);
65514
+ if (cursor && !after) throw new Error("Invalid conversation image cursor");
65515
+ const ordered = images.filter((image) => !after || image.created < after[0] || image.created === after[0] && image.id < after[1]).sort((a, b) => b.created - a.created || b.id.localeCompare(a.id));
65516
+ const page = ordered.slice(0, limit);
65517
+ return {
65518
+ images: page,
65519
+ ...ordered.length > page.length ? { nextCursor: cursorFor(page.at(-1)) } : {}
65520
+ };
65521
+ }
65522
+ async function readConversationImages(path) {
65523
+ try {
65524
+ const value = JSON.parse(await readFile(path, "utf8"));
65525
+ return Array.isArray(value) ? value : [];
65526
+ } catch (error) {
65527
+ if (error.code === "ENOENT") return [];
65528
+ throw error;
65529
+ }
65530
+ }
65531
+ async function withAvailability(images) {
65532
+ return Promise.all(images.map(async (image) => {
65533
+ try {
65534
+ await access(image.path);
65535
+ return {
65536
+ ...image,
65537
+ available: true
65538
+ };
65539
+ } catch {
65540
+ return {
65541
+ ...image,
65542
+ available: false
65543
+ };
65544
+ }
65545
+ }));
65546
+ }
65547
+ /** Atomic metadata projection shared by the HTTP host and the standalone MCP. */
65548
+ async function writeConversationImages(path, images) {
65549
+ await mkdir(dirname(path), {
65550
+ recursive: true,
65551
+ mode: 448
65552
+ });
65553
+ const target = resolve(path);
65554
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
65555
+ await writeFile(temporary, `${JSON.stringify(images)}\n`, { mode: 384 });
65556
+ await rename(temporary, target);
65557
+ }
65481
65558
  const HISTORY_PAGE_MESSAGES = 50;
65482
65559
  /** Match DSH's 50-message history window against CFL's rendered projection. */
65483
65560
  function visibleHistoryPage(messages, results, before) {
@@ -65654,6 +65731,9 @@ function historyImage(path, operationId, index) {
65654
65731
  function noHistoryYet(error) {
65655
65732
  return error instanceof AppServerInvalidRequestError && error.rpcMessage.includes("thread/turns/list is unavailable before first user message");
65656
65733
  }
65734
+ function noRolloutForThread(error) {
65735
+ return error instanceof AppServerInvalidRequestError && error.rpcMessage.startsWith("no rollout found for thread id ");
65736
+ }
65657
65737
  function rpcMessage(error) {
65658
65738
  return error instanceof AppServerInvalidRequestError ? error.rpcMessage : void 0;
65659
65739
  }
@@ -65770,6 +65850,24 @@ async function createPartner(config, credentials, dependencies = {}) {
65770
65850
  recursive: true,
65771
65851
  mode: 448
65772
65852
  });
65853
+ const catalogue = new Map((await readConversationImages(paths.conversationImages)).map((image) => [image.id, image]));
65854
+ let reconcilingCatalogue = false;
65855
+ let reconciledCatalogueIds = /* @__PURE__ */ new Set();
65856
+ async function catalogueImage(origin, image, created) {
65857
+ const id = conversationImageId(origin, `${image.operation_id}:${image.id}`);
65858
+ const previous = catalogue.get(id);
65859
+ catalogue.set(id, {
65860
+ id,
65861
+ filename: image.name,
65862
+ path: resolve(image.path),
65863
+ mediaType: image.media_type,
65864
+ created: reconcilingCatalogue ? created : previous?.created ?? created,
65865
+ origin,
65866
+ available: true
65867
+ });
65868
+ reconciledCatalogueIds.add(id);
65869
+ if (!reconcilingCatalogue) await writeConversationImages(paths.conversationImages, await withAvailability([...catalogue.values()]));
65870
+ }
65773
65871
  const store = new Store(paths.database);
65774
65872
  const now = dependencies.now ?? Date.now;
65775
65873
  const listeners = /* @__PURE__ */ new Set();
@@ -65801,7 +65899,6 @@ async function createPartner(config, credentials, dependencies = {}) {
65801
65899
  let unsubscribeAppServer = [];
65802
65900
  let startupPending = false;
65803
65901
  let compacting = false;
65804
- let compactWaiter;
65805
65902
  let eventChain = Promise.resolve();
65806
65903
  let admission = Promise.resolve();
65807
65904
  let closePromise;
@@ -65977,7 +66074,7 @@ async function createPartner(config, credentials, dependencies = {}) {
65977
66074
  if (previous?.status === "replaced") return;
65978
66075
  if (previous?.status !== "attachment-error" || previous.error !== error) await store.markOutcome(owner, "attachment-error", error);
65979
66076
  }
65980
- async function projectGeneratedImage(operationId, sourceIds, item) {
66077
+ async function projectGeneratedImage(operationId, sourceIds, item, created) {
65981
66078
  if (item.type !== "imageGeneration") return void 0;
65982
66079
  if (item.status === "failed") {
65983
66080
  processError("image.generation_failed", new Error(imageFailure("failed")), {
@@ -65992,6 +66089,7 @@ async function createPartner(config, credentials, dependencies = {}) {
65992
66089
  const existing = await store.generatedImageForItem(operationId, itemId);
65993
66090
  if (existing) try {
65994
66091
  await access(existing.path);
66092
+ await catalogueImage("partner", existing, created);
65995
66093
  return existing.id;
65996
66094
  } catch {
65997
66095
  storageError = true;
@@ -66014,6 +66112,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66014
66112
  return;
66015
66113
  }
66016
66114
  await store.saveGeneratedImage(generated, itemId);
66115
+ await catalogueImage("partner", generated, created);
66017
66116
  return generated.id;
66018
66117
  } catch (error) {
66019
66118
  storageError = true;
@@ -66104,6 +66203,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66104
66203
  await store.ensureMessage(sourceId, turnTime(turn.startedAt, now()), source.input, source.images);
66105
66204
  messageIds.add(sourceId);
66106
66205
  }
66206
+ if (!sourceId.startsWith("keet:")) for (const image of source.images) await catalogueImage(isUnder(join(paths.managedRoot, "historical-media"), image.path) ? "historical" : "owner", image, turnTime(turn.startedAt, now()));
66107
66207
  await store.markObserved(sourceId, segment);
66108
66208
  }
66109
66209
  acknowledgeInputIds(officialIds);
@@ -66116,7 +66216,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66116
66216
  const completedAt = status === "completed" ? completedTurnTime(turn.completedAt) : void 0;
66117
66217
  for (const item of turn.items ?? []) {
66118
66218
  if (item.type !== "imageGeneration") continue;
66119
- const generatedId = await projectGeneratedImage(resultOperationId(turn.id), sourceIds, item);
66219
+ const generatedId = await projectGeneratedImage(resultOperationId(turn.id), sourceIds, item, completedTurnTime(turn.completedAt) ?? turnTime(turn.startedAt, now()));
66120
66220
  if (generatedId) generatedIds.push(generatedId);
66121
66221
  else if (item.status === "failed") {
66122
66222
  resultStatus = "failed";
@@ -66146,15 +66246,23 @@ async function createPartner(config, credentials, dependencies = {}) {
66146
66246
  }
66147
66247
  /** Full history is hydrated once at startup/resume; events update one turn thereafter. */
66148
66248
  async function hydrateHistory() {
66249
+ reconcilingCatalogue = true;
66250
+ reconciledCatalogueIds = /* @__PURE__ */ new Set();
66149
66251
  const nextTurns = await listTurns();
66150
66252
  turns.clear();
66151
66253
  turnInputs.clear();
66152
66254
  turnResults.clear();
66153
66255
  presentationFingerprints.clear();
66154
- for (const turn of nextTurns) {
66155
- registerTurn(turn);
66156
- const complete = await reconcileTurn(turn.id, turn);
66157
- if (complete) await projectTurn(complete, false);
66256
+ try {
66257
+ for (const turn of nextTurns) {
66258
+ registerTurn(turn);
66259
+ const complete = await reconcileTurn(turn.id, turn);
66260
+ if (complete) await projectTurn(complete, false);
66261
+ }
66262
+ } finally {
66263
+ reconcilingCatalogue = false;
66264
+ for (const id of catalogue.keys()) if (!reconciledCatalogueIds.has(id)) catalogue.delete(id);
66265
+ await writeConversationImages(paths.conversationImages, await withAvailability([...catalogue.values()]));
66158
66266
  }
66159
66267
  const unresolved = await unresolvedMessages();
66160
66268
  for (const message of unresolved) unresolvedInputIds.add(message.id);
@@ -66307,7 +66415,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66307
66415
  }
66308
66416
  }
66309
66417
  function drainRejectedSteers() {
66310
- if (closing || activeTurnId || recoveryPromise || !rejectedSteers.length) return recoveryPromise;
66418
+ if (closing || compacting || activeTurnId || recoveryPromise || !rejectedSteers.length) return recoveryPromise;
66311
66419
  const intents = rejectedSteers.splice(0);
66312
66420
  const ids = intents.flatMap((intent) => intent.ids);
66313
66421
  const merged = {
@@ -66331,7 +66439,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66331
66439
  }
66332
66440
  function drainKeet() {
66333
66441
  admission = admission.then(async () => {
66334
- if (closing || activeTurnId || recoveryPromise || !keetEnabled) return;
66442
+ if (closing || compacting || activeTurnId || recoveryPromise || !keetEnabled) return;
66335
66443
  const pending = (await store.pendingMessages()).filter((item) => item.id.startsWith("keet:") && !observedInputIds.has(item.id));
66336
66444
  if (!pending.length || activeTurnId || closing) return;
66337
66445
  const first = pending[0];
@@ -66433,11 +66541,11 @@ async function createPartner(config, credentials, dependencies = {}) {
66433
66541
  scheduleKeetReconnect();
66434
66542
  });
66435
66543
  }
66436
- async function restoreUnconsumed(turnId) {
66544
+ async function restoreUnconsumed(turnId, preserveQueued = false) {
66437
66545
  const ids = turnId ? [...sourceIdsForTurn(turnId)] : [];
66438
66546
  const selectedSteers = pendingSteers.filter((intent) => turnId === void 0 || intent.turnId === turnId);
66439
66547
  const selectedStarts = [...pendingStarts.values()].filter((intent) => turnId === void 0 || intent.turnId === turnId);
66440
- const selectedRejected = rejectedSteers.filter((intent) => turnId === void 0 || intent.turnId === turnId);
66548
+ const selectedRejected = preserveQueued ? [] : rejectedSteers.filter((intent) => turnId === void 0 || intent.turnId === turnId);
66441
66549
  for (const intent of selectedSteers) ids.push(...intent.ids);
66442
66550
  for (const intent of selectedStarts) ids.push(...intent.ids);
66443
66551
  for (const intent of selectedRejected) ids.push(...intent.ids);
@@ -66505,18 +66613,14 @@ async function createPartner(config, credentials, dependencies = {}) {
66505
66613
  endedAt: boundary.time
66506
66614
  };
66507
66615
  await refreshBootstrap();
66508
- compactWaiter?.resolve();
66509
- compactWaiter = void 0;
66510
66616
  }
66511
- function failCompaction(error) {
66617
+ function failCompaction() {
66512
66618
  if (lifecycle?.status === "running") lifecycle = {
66513
66619
  ...lifecycle,
66514
66620
  status: "failed",
66515
66621
  endSeq: eventSequence,
66516
66622
  endedAt: now()
66517
66623
  };
66518
- compactWaiter?.reject(error instanceof Error ? error : new Error(String(error)));
66519
- compactWaiter = void 0;
66520
66624
  }
66521
66625
  async function handleServerEvent(notification) {
66522
66626
  if (closing) return;
@@ -66598,11 +66702,14 @@ async function createPartner(config, credentials, dependencies = {}) {
66598
66702
  turnControllers.delete(turn.id);
66599
66703
  }
66600
66704
  if (reconciled && (turnStatus(reconciled) === "interrupted" || turnStatus(reconciled) === "cancelled")) await restoreUnconsumed(completedTurnId);
66601
- else if (reconciled && !userItems(reconciled).length && completedTurnId) await restoreUnconsumed(completedTurnId);
66705
+ else if (reconciled && !userItems(reconciled).length && completedTurnId) await restoreUnconsumed(completedTurnId, turnStatus(reconciled) === "completed");
66602
66706
  syncActiveTurn();
66603
66707
  if (compacting) {
66604
66708
  compacting = false;
66605
- if (lifecycle?.status === "running") await finishCompaction(reconciled?.id);
66709
+ if (lifecycle?.status === "running") {
66710
+ if (turnStatus(reconciled) === "completed") await finishCompaction(reconciled?.id);
66711
+ else failCompaction();
66712
+ }
66606
66713
  }
66607
66714
  await refreshBootstrap();
66608
66715
  if (!activeTurnId) {
@@ -66626,6 +66733,9 @@ async function createPartner(config, credentials, dependencies = {}) {
66626
66733
  if (notification.method === "turn/started") {
66627
66734
  const turn = registerTurn(record(notification.params).turn);
66628
66735
  if (turn) {
66736
+ if (compacting) {
66737
+ for (const intent of rejectedSteers) if (!intent.turnId) intent.turnId = turn.id;
66738
+ }
66629
66739
  activeTurnId = turn.id;
66630
66740
  activeOperationId = sourceIdsForTurn(turn.id)[0] ?? activeOperationId;
66631
66741
  if (!turnControllers.has(turn.id)) turnControllers.set(turn.id, new AbortController());
@@ -66723,29 +66833,44 @@ async function createPartner(config, credentials, dependencies = {}) {
66723
66833
  } : {}
66724
66834
  }
66725
66835
  };
66836
+ const connectedAppServer = appServer;
66837
+ if (!connectedAppServer) throw new Error("Codex app-server connection was not initialized");
66726
66838
  let response;
66727
- if (marker?.threadId) {
66728
- const resume = {
66729
- ...shared,
66730
- threadId: marker.threadId,
66731
- excludeTurns: true
66732
- };
66733
- response = await appServer.call("thread/resume", resume);
66734
- threadId = response.thread.id;
66735
- } else {
66839
+ let resumed = false;
66840
+ const startThread = async () => {
66736
66841
  const start = {
66737
66842
  ...shared,
66738
66843
  historyMode: "paginated",
66739
66844
  sessionStartSource: "startup"
66740
66845
  };
66741
- response = await appServer.call("thread/start", start);
66742
- threadId = response.thread.id;
66846
+ const started = await connectedAppServer.call("thread/start", start);
66847
+ threadId = started.thread.id;
66743
66848
  if (!threadId) throw new Error("Codex app-server did not return a thread id");
66744
66849
  await writeFile(markerPath, JSON.stringify({
66745
66850
  threadId,
66746
66851
  model: config.codex.model
66747
66852
  }), { mode: 384 });
66748
- }
66853
+ return started;
66854
+ };
66855
+ if (marker?.threadId) {
66856
+ const resume = {
66857
+ ...shared,
66858
+ threadId: marker.threadId,
66859
+ excludeTurns: true
66860
+ };
66861
+ try {
66862
+ response = await connectedAppServer.call("thread/resume", resume);
66863
+ threadId = response.thread.id;
66864
+ resumed = true;
66865
+ } catch (error) {
66866
+ if (!noRolloutForThread(error) || messageIds.size) throw error;
66867
+ await unlink(markerPath);
66868
+ marker = void 0;
66869
+ startupPending = true;
66870
+ await refreshBootstrap();
66871
+ response = await startThread();
66872
+ }
66873
+ } else response = await startThread();
66749
66874
  const readinessDeadline = Date.now() + 1e4;
66750
66875
  for (;;) {
66751
66876
  const mcpStatuses = [];
@@ -66765,7 +66890,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66765
66890
  await new Promise((resolve) => setTimeout(resolve, 100));
66766
66891
  }
66767
66892
  if (response.model !== void 0 && response.model !== config.codex.model) throw new Error(`Codex selected ${String(response.model)} instead of configured ${config.codex.model}`);
66768
- if (marker?.threadId && marker.model !== config.codex.model) await writeFile(markerPath, JSON.stringify({
66893
+ if (resumed && marker?.threadId && marker.model !== config.codex.model) await writeFile(markerPath, JSON.stringify({
66769
66894
  threadId,
66770
66895
  model: config.codex.model
66771
66896
  }), { mode: 384 });
@@ -66778,7 +66903,6 @@ async function createPartner(config, credentials, dependencies = {}) {
66778
66903
  closing = true;
66779
66904
  for (const unsubscribe of unsubscribeAppServer.splice(0)) unsubscribe();
66780
66905
  for (const controller of turnControllers.values()) controller.abort();
66781
- compactWaiter?.reject(error instanceof Error ? error : new Error(String(error)));
66782
66906
  await appServer?.close().catch(() => void 0);
66783
66907
  await store.close().catch(() => void 0);
66784
66908
  throw error;
@@ -66841,6 +66965,34 @@ async function createPartner(config, credentials, dependencies = {}) {
66841
66965
  return;
66842
66966
  }
66843
66967
  },
66968
+ async conversationImages(options = {}) {
66969
+ await eventChain;
66970
+ const page = pageConversationImages(await withAvailability([...catalogue.values()]), options.limit ?? 5, options.cursor);
66971
+ return {
66972
+ images: page.images.map(({ id, filename, mediaType, created, origin, available }) => ({
66973
+ id,
66974
+ filename,
66975
+ mediaType,
66976
+ created,
66977
+ origin,
66978
+ available,
66979
+ url: `/api/conversation-images/${id}`
66980
+ })),
66981
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {}
66982
+ };
66983
+ },
66984
+ async conversationImage(id) {
66985
+ const image = catalogue.get(id);
66986
+ if (!image) return void 0;
66987
+ try {
66988
+ return {
66989
+ media_type: image.mediaType,
66990
+ data: await readFile(image.path)
66991
+ };
66992
+ } catch {
66993
+ return;
66994
+ }
66995
+ },
66844
66996
  avatar: (kind) => avatars[kind],
66845
66997
  async petAsset(activity) {
66846
66998
  return pet ? localPetClip(config.state, activity) : void 0;
@@ -66968,7 +67120,6 @@ async function createPartner(config, credentials, dependencies = {}) {
66968
67120
  if (replaces.length) await setRestoredDraft([...unresolvedInputIds]);
66969
67121
  await eventChain;
66970
67122
  syncActiveTurn();
66971
- if (rejectedSteers.length) await (drainRejectedSteers() ?? Promise.resolve());
66972
67123
  const intent = {
66973
67124
  ids: [id],
66974
67125
  input,
@@ -66976,6 +67127,12 @@ async function createPartner(config, credentials, dependencies = {}) {
66976
67127
  transportId: id,
66977
67128
  source: "owner"
66978
67129
  };
67130
+ if (compacting && !activeTurnId) {
67131
+ rejectedSteers.push(intent);
67132
+ notify();
67133
+ return;
67134
+ }
67135
+ if (rejectedSteers.length) await (drainRejectedSteers() ?? Promise.resolve());
66979
67136
  if (activeTurnId) await steerIntent(intent, activeTurnId);
66980
67137
  else await startIntent(intent);
66981
67138
  notify();
@@ -67009,27 +67166,15 @@ async function createPartner(config, credentials, dependencies = {}) {
67009
67166
  startedAt: now()
67010
67167
  };
67011
67168
  compacting = true;
67012
- const wait = new Promise((resolveWait, rejectWait) => {
67013
- compactWaiter = {
67014
- resolve: resolveWait,
67015
- reject: rejectWait
67016
- };
67017
- });
67018
- let timeout;
67169
+ notify();
67019
67170
  try {
67020
67171
  await appServer.call("thread/compact/start", { threadId });
67021
- await Promise.race([wait, new Promise((_, reject) => {
67022
- timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("Codex compaction timed out")), 6e4);
67023
- })]);
67024
67172
  } catch (error) {
67025
67173
  compacting = false;
67026
- failCompaction(error);
67174
+ failCompaction();
67027
67175
  processError("compaction.failed", error);
67028
67176
  notify();
67029
67177
  throw error;
67030
- } finally {
67031
- if (timeout) clearTimeout(timeout);
67032
- compactWaiter = void 0;
67033
67178
  }
67034
67179
  notify();
67035
67180
  });
@@ -67052,8 +67197,6 @@ async function createPartner(config, credentials, dependencies = {}) {
67052
67197
  keetFeed?.close();
67053
67198
  for (const unsubscribe of unsubscribeAppServer.splice(0)) unsubscribe();
67054
67199
  for (const controller of turnControllers.values()) controller.abort();
67055
- compactWaiter?.reject(/* @__PURE__ */ new Error("Partner is closing"));
67056
- compactWaiter = void 0;
67057
67200
  compacting = false;
67058
67201
  const serverClose = appServer?.close() ?? Promise.resolve();
67059
67202
  closePromise = (async () => {
@@ -67823,6 +67966,25 @@ function createWebServer(partner, assets, options = {}) {
67823
67966
  response.end(image.data);
67824
67967
  return;
67825
67968
  }
67969
+ if (path === "/api/conversation-images" && request.method === "GET") {
67970
+ const params = Object.fromEntries(new URL(request.url, "http://localhost").searchParams);
67971
+ const options = object({
67972
+ limit: number().int().min(1).max(50).optional(),
67973
+ cursor: string().min(1).max(500).optional()
67974
+ }).strict().parse(params);
67975
+ return json(response, await partner.conversationImages(options));
67976
+ }
67977
+ if (path.startsWith("/api/conversation-images/") && request.method === "GET") {
67978
+ const id = string().regex(/^[a-f0-9]{64}$/u).parse(path.slice(25));
67979
+ const image = await partner.conversationImage(id);
67980
+ if (!image) return json(response, { error: "Image not found" }, 404);
67981
+ response.writeHead(200, {
67982
+ "content-type": image.media_type,
67983
+ "cache-control": "private, max-age=31536000, immutable"
67984
+ });
67985
+ response.end(image.data);
67986
+ return;
67987
+ }
67826
67988
  if (path.startsWith("/api/audio/") && request.method === "GET") {
67827
67989
  const id = string().regex(/^[a-f0-9]{64}\.mp3$/u).parse(path.slice(11));
67828
67990
  const audio = await partner.audio(id.slice(0, -4));
@@ -67878,7 +68040,7 @@ function createWebServer(partner, assets, options = {}) {
67878
68040
  }
67879
68041
  if (path === "/api/compact" && request.method === "POST") {
67880
68042
  await partner.compact();
67881
- return json(response, { ok: true });
68043
+ return json(response, { ok: true }, 202);
67882
68044
  }
67883
68045
  if (path === "/api/cancel" && request.method === "POST") {
67884
68046
  const { id } = object({ id: string().min(1).max(300) }).strict().parse(await body(request));
@@ -68037,7 +68199,7 @@ if (command === "credential") {
68037
68199
  await partner.close();
68038
68200
  process.exitCode = 1;
68039
68201
  });
68040
- app.server.listen(config.port, "127.0.0.1", () => console.log(`Companion: http://127.0.0.1:${config.port}`));
68202
+ app.server.listen(config.port, config.listen_host, () => console.log(`Companion: http://${config.listen_host}:${config.port}`));
68041
68203
  let stopping = false;
68042
68204
  const stop = async () => {
68043
68205
  if (stopping) return;