@lamplitisles/codex-for-love 0.4.2 → 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.
@@ -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,13 +13,12 @@ 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 * as fs from "node:fs";
17
- import { constants, createReadStream } from "node:fs";
16
+ import { createConnection, isIP as isIP$1 } from "net";
18
17
  import { DatabaseSync } from "node:sqlite";
19
18
  import { execFile } from "node:child_process";
20
19
  import { promisify } from "node:util";
21
20
  import { createServer } from "node:http";
21
+ import * as fs from "node:fs";
22
22
  import * as qs from "node:querystring";
23
23
  //#region \0rolldown/runtime.js
24
24
  var __create = Object.create;
@@ -6197,12 +6197,12 @@ const schema = object({
6197
6197
  user: string().min(1)
6198
6198
  }).strict().optional(),
6199
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"),
6200
6201
  codex: object({
6201
6202
  command: string().min(1).default("codex-app-server"),
6202
6203
  model: string().min(1).default(DEFAULT_CODEX_MODEL),
6203
6204
  version: literal(SUPPORTED_CODEX_VERSION).default(SUPPORTED_CODEX_VERSION),
6204
6205
  home: string().min(1).optional(),
6205
- provenance: string().min(1).optional(),
6206
6206
  local_compaction: boolean().default(false),
6207
6207
  context_round_limit: number$1().int().min(0).default(10)
6208
6208
  }).strict().default({
@@ -6237,8 +6237,7 @@ async function loadConfig(path) {
6237
6237
  } : void 0,
6238
6238
  codex: {
6239
6239
  ...config.codex,
6240
- home: config.codex.home ? resolve(base, config.codex.home) : void 0,
6241
- provenance: config.codex.provenance ? resolve(base, config.codex.provenance) : void 0
6240
+ home: config.codex.home ? resolve(base, config.codex.home) : void 0
6242
6241
  },
6243
6242
  keet: config.keet ? {
6244
6243
  ...config.keet.endpoint ? { endpoint: keetEndpoint(config.keet.endpoint) } : {},
@@ -61940,7 +61939,7 @@ function validateBearerToken(value) {
61940
61939
  function isLoopbackHostname(hostname) {
61941
61940
  const normalized = hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
61942
61941
  if (normalized === "localhost") return true;
61943
- const family = isIP(normalized);
61942
+ const family = isIP$1(normalized);
61944
61943
  if (family === 4) return normalized.startsWith("127.");
61945
61944
  if (family === 6) return normalized === "::1" || normalized.startsWith("::ffff:127.");
61946
61945
  return false;
@@ -62600,7 +62599,8 @@ function partnerPaths(workspace) {
62600
62599
  database: join(managedRoot, "session.sqlite"),
62601
62600
  relationshipJournal: join(managedRoot, "relationship.jsonl"),
62602
62601
  attachments: join(managedRoot, "attachments"),
62603
- audio: join(managedRoot, "audio")
62602
+ audio: join(managedRoot, "audio"),
62603
+ conversationImages: join(managedRoot, "conversation-images.json")
62604
62604
  };
62605
62605
  }
62606
62606
  //#endregion
@@ -62890,72 +62890,6 @@ const compactionPrompt = [
62890
62890
  "- Output checkpoint text only. Do not call Tools, take actions, or mention compaction."
62891
62891
  ].join("\n");
62892
62892
  //#endregion
62893
- //#region apps/partner/runtime/provenance.ts
62894
- const CFL_FORK_REPOSITORY = "https://github.com/lamplitisles/codex";
62895
- const CFL_FORK_SOURCE_REVISION = "445477b6a83514611ac206d2ab04b79374555a4c";
62896
- const provenanceSchema = object({
62897
- schemaVersion: literal(1),
62898
- forkRepository: literal(CFL_FORK_REPOSITORY),
62899
- sourceRevision: string(),
62900
- releaseTag: string(),
62901
- codexVersion: literal(SUPPORTED_CODEX_VERSION),
62902
- target: _enum(["x86_64-unknown-linux-musl", "aarch64-apple-darwin"]),
62903
- executables: object({
62904
- "bin/codex-app-server": string().regex(/^[a-f0-9]{64}$/u),
62905
- "bin/codex-code-mode-host": string().regex(/^[a-f0-9]{64}$/u)
62906
- }).strict()
62907
- }).strict().superRefine((value, context) => {
62908
- const mac = value.target === "aarch64-apple-darwin";
62909
- const revision = mac ? "c1139f7b2793e94c14243689d756b09c0186708d" : CFL_FORK_SOURCE_REVISION;
62910
- const tag = mac ? /^cfl\/v0\.154\.0-app-server-darwin\.[0-9]+$/u : /^cfl\/v0\.154\.0-app-server-musl\.[0-9]+$/u;
62911
- if (value.sourceRevision !== revision || !tag.test(value.releaseTag)) context.addIssue({
62912
- code: "custom",
62913
- message: "Source revision/release identity does not match the selected platform"
62914
- });
62915
- });
62916
- async function sha256(path) {
62917
- const hash = createHash("sha256");
62918
- for await (const chunk of createReadStream(path)) hash.update(chunk);
62919
- return hash.digest("hex");
62920
- }
62921
- function provenanceError(path, message) {
62922
- return /* @__PURE__ */ new Error(`Invalid Codex provenance at ${path}: ${message}`);
62923
- }
62924
- /**
62925
- * Verifies the exact executable selected for local compaction before the SDK
62926
- * starts it. The app-server handshake still verifies the runtime version;
62927
- * this sidecar binds the selected path to the pinned source and patch set.
62928
- */
62929
- async function verifyCodexArtifact(command, provenancePath) {
62930
- if (!isAbsolute(command)) throw new Error("Local compaction requires an absolute codex.command so the selected build can be verified");
62931
- const selectedPath = resolve(command);
62932
- let raw;
62933
- try {
62934
- raw = await readFile(provenancePath, "utf8");
62935
- } catch (error) {
62936
- throw new Error(`Codex provenance is required and could not be read at ${provenancePath}: ${String(error)}`);
62937
- }
62938
- let provenance;
62939
- try {
62940
- provenance = provenanceSchema.parse(JSON.parse(raw));
62941
- } catch (error) {
62942
- throw provenanceError(provenancePath, error instanceof Error ? error.message : String(error));
62943
- }
62944
- await verifyExecutableHashes(selectedPath, provenancePath, provenance.executables["bin/codex-app-server"], provenance.executables["bin/codex-code-mode-host"]);
62945
- }
62946
- async function verifyExecutableHashes(selectedPath, provenancePath, expectedBinaryHash, expectedHelperHash) {
62947
- try {
62948
- if (!(await stat(selectedPath)).isFile()) throw new Error("selected path is not a regular file");
62949
- await access(selectedPath, constants.X_OK);
62950
- } catch (error) {
62951
- throw new Error(`Configured Codex executable cannot be verified at ${selectedPath}: ${String(error)}`);
62952
- }
62953
- if (await sha256(selectedPath) !== expectedBinaryHash) throw provenanceError(provenancePath, `binary hash mismatch for ${selectedPath}`);
62954
- const helper = join(dirname(selectedPath), "codex-code-mode-host");
62955
- await access(helper, constants.X_OK);
62956
- if (await sha256(helper) !== expectedHelperHash) throw provenanceError(provenancePath, "code-mode host hash does not match");
62957
- }
62958
- //#endregion
62959
62893
  //#region apps/partner/runtime/store.ts
62960
62894
  function identityConflict() {
62961
62895
  return Object.assign(/* @__PURE__ */ new Error("Message ID already belongs to different content"), { code: "MESSAGE_IDENTITY_CONFLICT" });
@@ -64723,8 +64657,6 @@ async function convertDshSession(config, inputPath, companionStatePath, attachme
64723
64657
  });
64724
64658
  const injected = dependencies.appServer ?? {};
64725
64659
  const selectedCodexPath = injected.codexPath ?? config.codex.command;
64726
- if (config.codex.local_compaction && !config.codex.provenance) throw new Error("Local compaction requires codex.provenance for the selected custom Codex build");
64727
- if (config.codex.provenance) await verifyCodexArtifact(selectedCodexPath, config.codex.provenance);
64728
64660
  const environment = {
64729
64661
  ...config.codex.home ? { CODEX_HOME: config.codex.home } : {},
64730
64662
  ...injected.env ?? {}
@@ -65549,6 +65481,80 @@ async function readClip(root, name, frameCount) {
65549
65481
  return;
65550
65482
  }
65551
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
+ }
65552
65558
  const HISTORY_PAGE_MESSAGES = 50;
65553
65559
  /** Match DSH's 50-message history window against CFL's rendered projection. */
65554
65560
  function visibleHistoryPage(messages, results, before) {
@@ -65725,6 +65731,9 @@ function historyImage(path, operationId, index) {
65725
65731
  function noHistoryYet(error) {
65726
65732
  return error instanceof AppServerInvalidRequestError && error.rpcMessage.includes("thread/turns/list is unavailable before first user message");
65727
65733
  }
65734
+ function noRolloutForThread(error) {
65735
+ return error instanceof AppServerInvalidRequestError && error.rpcMessage.startsWith("no rollout found for thread id ");
65736
+ }
65728
65737
  function rpcMessage(error) {
65729
65738
  return error instanceof AppServerInvalidRequestError ? error.rpcMessage : void 0;
65730
65739
  }
@@ -65841,6 +65850,24 @@ async function createPartner(config, credentials, dependencies = {}) {
65841
65850
  recursive: true,
65842
65851
  mode: 448
65843
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
+ }
65844
65871
  const store = new Store(paths.database);
65845
65872
  const now = dependencies.now ?? Date.now;
65846
65873
  const listeners = /* @__PURE__ */ new Set();
@@ -65872,7 +65899,6 @@ async function createPartner(config, credentials, dependencies = {}) {
65872
65899
  let unsubscribeAppServer = [];
65873
65900
  let startupPending = false;
65874
65901
  let compacting = false;
65875
- let compactWaiter;
65876
65902
  let eventChain = Promise.resolve();
65877
65903
  let admission = Promise.resolve();
65878
65904
  let closePromise;
@@ -66048,7 +66074,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66048
66074
  if (previous?.status === "replaced") return;
66049
66075
  if (previous?.status !== "attachment-error" || previous.error !== error) await store.markOutcome(owner, "attachment-error", error);
66050
66076
  }
66051
- async function projectGeneratedImage(operationId, sourceIds, item) {
66077
+ async function projectGeneratedImage(operationId, sourceIds, item, created) {
66052
66078
  if (item.type !== "imageGeneration") return void 0;
66053
66079
  if (item.status === "failed") {
66054
66080
  processError("image.generation_failed", new Error(imageFailure("failed")), {
@@ -66063,6 +66089,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66063
66089
  const existing = await store.generatedImageForItem(operationId, itemId);
66064
66090
  if (existing) try {
66065
66091
  await access(existing.path);
66092
+ await catalogueImage("partner", existing, created);
66066
66093
  return existing.id;
66067
66094
  } catch {
66068
66095
  storageError = true;
@@ -66085,6 +66112,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66085
66112
  return;
66086
66113
  }
66087
66114
  await store.saveGeneratedImage(generated, itemId);
66115
+ await catalogueImage("partner", generated, created);
66088
66116
  return generated.id;
66089
66117
  } catch (error) {
66090
66118
  storageError = true;
@@ -66175,6 +66203,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66175
66203
  await store.ensureMessage(sourceId, turnTime(turn.startedAt, now()), source.input, source.images);
66176
66204
  messageIds.add(sourceId);
66177
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()));
66178
66207
  await store.markObserved(sourceId, segment);
66179
66208
  }
66180
66209
  acknowledgeInputIds(officialIds);
@@ -66187,7 +66216,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66187
66216
  const completedAt = status === "completed" ? completedTurnTime(turn.completedAt) : void 0;
66188
66217
  for (const item of turn.items ?? []) {
66189
66218
  if (item.type !== "imageGeneration") continue;
66190
- 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()));
66191
66220
  if (generatedId) generatedIds.push(generatedId);
66192
66221
  else if (item.status === "failed") {
66193
66222
  resultStatus = "failed";
@@ -66217,15 +66246,23 @@ async function createPartner(config, credentials, dependencies = {}) {
66217
66246
  }
66218
66247
  /** Full history is hydrated once at startup/resume; events update one turn thereafter. */
66219
66248
  async function hydrateHistory() {
66249
+ reconcilingCatalogue = true;
66250
+ reconciledCatalogueIds = /* @__PURE__ */ new Set();
66220
66251
  const nextTurns = await listTurns();
66221
66252
  turns.clear();
66222
66253
  turnInputs.clear();
66223
66254
  turnResults.clear();
66224
66255
  presentationFingerprints.clear();
66225
- for (const turn of nextTurns) {
66226
- registerTurn(turn);
66227
- const complete = await reconcileTurn(turn.id, turn);
66228
- 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()]));
66229
66266
  }
66230
66267
  const unresolved = await unresolvedMessages();
66231
66268
  for (const message of unresolved) unresolvedInputIds.add(message.id);
@@ -66378,7 +66415,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66378
66415
  }
66379
66416
  }
66380
66417
  function drainRejectedSteers() {
66381
- if (closing || activeTurnId || recoveryPromise || !rejectedSteers.length) return recoveryPromise;
66418
+ if (closing || compacting || activeTurnId || recoveryPromise || !rejectedSteers.length) return recoveryPromise;
66382
66419
  const intents = rejectedSteers.splice(0);
66383
66420
  const ids = intents.flatMap((intent) => intent.ids);
66384
66421
  const merged = {
@@ -66402,7 +66439,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66402
66439
  }
66403
66440
  function drainKeet() {
66404
66441
  admission = admission.then(async () => {
66405
- if (closing || activeTurnId || recoveryPromise || !keetEnabled) return;
66442
+ if (closing || compacting || activeTurnId || recoveryPromise || !keetEnabled) return;
66406
66443
  const pending = (await store.pendingMessages()).filter((item) => item.id.startsWith("keet:") && !observedInputIds.has(item.id));
66407
66444
  if (!pending.length || activeTurnId || closing) return;
66408
66445
  const first = pending[0];
@@ -66504,11 +66541,11 @@ async function createPartner(config, credentials, dependencies = {}) {
66504
66541
  scheduleKeetReconnect();
66505
66542
  });
66506
66543
  }
66507
- async function restoreUnconsumed(turnId) {
66544
+ async function restoreUnconsumed(turnId, preserveQueued = false) {
66508
66545
  const ids = turnId ? [...sourceIdsForTurn(turnId)] : [];
66509
66546
  const selectedSteers = pendingSteers.filter((intent) => turnId === void 0 || intent.turnId === turnId);
66510
66547
  const selectedStarts = [...pendingStarts.values()].filter((intent) => turnId === void 0 || intent.turnId === turnId);
66511
- const selectedRejected = rejectedSteers.filter((intent) => turnId === void 0 || intent.turnId === turnId);
66548
+ const selectedRejected = preserveQueued ? [] : rejectedSteers.filter((intent) => turnId === void 0 || intent.turnId === turnId);
66512
66549
  for (const intent of selectedSteers) ids.push(...intent.ids);
66513
66550
  for (const intent of selectedStarts) ids.push(...intent.ids);
66514
66551
  for (const intent of selectedRejected) ids.push(...intent.ids);
@@ -66576,18 +66613,14 @@ async function createPartner(config, credentials, dependencies = {}) {
66576
66613
  endedAt: boundary.time
66577
66614
  };
66578
66615
  await refreshBootstrap();
66579
- compactWaiter?.resolve();
66580
- compactWaiter = void 0;
66581
66616
  }
66582
- function failCompaction(error) {
66617
+ function failCompaction() {
66583
66618
  if (lifecycle?.status === "running") lifecycle = {
66584
66619
  ...lifecycle,
66585
66620
  status: "failed",
66586
66621
  endSeq: eventSequence,
66587
66622
  endedAt: now()
66588
66623
  };
66589
- compactWaiter?.reject(error instanceof Error ? error : new Error(String(error)));
66590
- compactWaiter = void 0;
66591
66624
  }
66592
66625
  async function handleServerEvent(notification) {
66593
66626
  if (closing) return;
@@ -66669,11 +66702,14 @@ async function createPartner(config, credentials, dependencies = {}) {
66669
66702
  turnControllers.delete(turn.id);
66670
66703
  }
66671
66704
  if (reconciled && (turnStatus(reconciled) === "interrupted" || turnStatus(reconciled) === "cancelled")) await restoreUnconsumed(completedTurnId);
66672
- else if (reconciled && !userItems(reconciled).length && completedTurnId) await restoreUnconsumed(completedTurnId);
66705
+ else if (reconciled && !userItems(reconciled).length && completedTurnId) await restoreUnconsumed(completedTurnId, turnStatus(reconciled) === "completed");
66673
66706
  syncActiveTurn();
66674
66707
  if (compacting) {
66675
66708
  compacting = false;
66676
- 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
+ }
66677
66713
  }
66678
66714
  await refreshBootstrap();
66679
66715
  if (!activeTurnId) {
@@ -66697,6 +66733,9 @@ async function createPartner(config, credentials, dependencies = {}) {
66697
66733
  if (notification.method === "turn/started") {
66698
66734
  const turn = registerTurn(record(notification.params).turn);
66699
66735
  if (turn) {
66736
+ if (compacting) {
66737
+ for (const intent of rejectedSteers) if (!intent.turnId) intent.turnId = turn.id;
66738
+ }
66700
66739
  activeTurnId = turn.id;
66701
66740
  activeOperationId = sourceIdsForTurn(turn.id)[0] ?? activeOperationId;
66702
66741
  if (!turnControllers.has(turn.id)) turnControllers.set(turn.id, new AbortController());
@@ -66727,8 +66766,6 @@ async function createPartner(config, credentials, dependencies = {}) {
66727
66766
  await refreshBootstrap();
66728
66767
  const injected = dependencies.appServer ?? {};
66729
66768
  const selectedCodexPath = injected.codexPath ?? config.codex.command;
66730
- if (config.codex.local_compaction && !config.codex.provenance) throw new Error("Local compaction requires codex.provenance for the selected custom Codex build");
66731
- if (config.codex.provenance) await verifyCodexArtifact(selectedCodexPath, config.codex.provenance);
66732
66769
  const environment = {
66733
66770
  ...config.codex.home ? { CODEX_HOME: config.codex.home } : {},
66734
66771
  ...keetEnabled ? { CFL_KEET_TOKEN: credentials.keet } : {},
@@ -66796,29 +66833,44 @@ async function createPartner(config, credentials, dependencies = {}) {
66796
66833
  } : {}
66797
66834
  }
66798
66835
  };
66836
+ const connectedAppServer = appServer;
66837
+ if (!connectedAppServer) throw new Error("Codex app-server connection was not initialized");
66799
66838
  let response;
66800
- if (marker?.threadId) {
66801
- const resume = {
66802
- ...shared,
66803
- threadId: marker.threadId,
66804
- excludeTurns: true
66805
- };
66806
- response = await appServer.call("thread/resume", resume);
66807
- threadId = response.thread.id;
66808
- } else {
66839
+ let resumed = false;
66840
+ const startThread = async () => {
66809
66841
  const start = {
66810
66842
  ...shared,
66811
66843
  historyMode: "paginated",
66812
66844
  sessionStartSource: "startup"
66813
66845
  };
66814
- response = await appServer.call("thread/start", start);
66815
- threadId = response.thread.id;
66846
+ const started = await connectedAppServer.call("thread/start", start);
66847
+ threadId = started.thread.id;
66816
66848
  if (!threadId) throw new Error("Codex app-server did not return a thread id");
66817
66849
  await writeFile(markerPath, JSON.stringify({
66818
66850
  threadId,
66819
66851
  model: config.codex.model
66820
66852
  }), { mode: 384 });
66821
- }
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();
66822
66874
  const readinessDeadline = Date.now() + 1e4;
66823
66875
  for (;;) {
66824
66876
  const mcpStatuses = [];
@@ -66838,7 +66890,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66838
66890
  await new Promise((resolve) => setTimeout(resolve, 100));
66839
66891
  }
66840
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}`);
66841
- 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({
66842
66894
  threadId,
66843
66895
  model: config.codex.model
66844
66896
  }), { mode: 384 });
@@ -66851,7 +66903,6 @@ async function createPartner(config, credentials, dependencies = {}) {
66851
66903
  closing = true;
66852
66904
  for (const unsubscribe of unsubscribeAppServer.splice(0)) unsubscribe();
66853
66905
  for (const controller of turnControllers.values()) controller.abort();
66854
- compactWaiter?.reject(error instanceof Error ? error : new Error(String(error)));
66855
66906
  await appServer?.close().catch(() => void 0);
66856
66907
  await store.close().catch(() => void 0);
66857
66908
  throw error;
@@ -66914,6 +66965,34 @@ async function createPartner(config, credentials, dependencies = {}) {
66914
66965
  return;
66915
66966
  }
66916
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
+ },
66917
66996
  avatar: (kind) => avatars[kind],
66918
66997
  async petAsset(activity) {
66919
66998
  return pet ? localPetClip(config.state, activity) : void 0;
@@ -67041,7 +67120,6 @@ async function createPartner(config, credentials, dependencies = {}) {
67041
67120
  if (replaces.length) await setRestoredDraft([...unresolvedInputIds]);
67042
67121
  await eventChain;
67043
67122
  syncActiveTurn();
67044
- if (rejectedSteers.length) await (drainRejectedSteers() ?? Promise.resolve());
67045
67123
  const intent = {
67046
67124
  ids: [id],
67047
67125
  input,
@@ -67049,6 +67127,12 @@ async function createPartner(config, credentials, dependencies = {}) {
67049
67127
  transportId: id,
67050
67128
  source: "owner"
67051
67129
  };
67130
+ if (compacting && !activeTurnId) {
67131
+ rejectedSteers.push(intent);
67132
+ notify();
67133
+ return;
67134
+ }
67135
+ if (rejectedSteers.length) await (drainRejectedSteers() ?? Promise.resolve());
67052
67136
  if (activeTurnId) await steerIntent(intent, activeTurnId);
67053
67137
  else await startIntent(intent);
67054
67138
  notify();
@@ -67082,27 +67166,15 @@ async function createPartner(config, credentials, dependencies = {}) {
67082
67166
  startedAt: now()
67083
67167
  };
67084
67168
  compacting = true;
67085
- const wait = new Promise((resolveWait, rejectWait) => {
67086
- compactWaiter = {
67087
- resolve: resolveWait,
67088
- reject: rejectWait
67089
- };
67090
- });
67091
- let timeout;
67169
+ notify();
67092
67170
  try {
67093
67171
  await appServer.call("thread/compact/start", { threadId });
67094
- await Promise.race([wait, new Promise((_, reject) => {
67095
- timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("Codex compaction timed out")), 6e4);
67096
- })]);
67097
67172
  } catch (error) {
67098
67173
  compacting = false;
67099
- failCompaction(error);
67174
+ failCompaction();
67100
67175
  processError("compaction.failed", error);
67101
67176
  notify();
67102
67177
  throw error;
67103
- } finally {
67104
- if (timeout) clearTimeout(timeout);
67105
- compactWaiter = void 0;
67106
67178
  }
67107
67179
  notify();
67108
67180
  });
@@ -67125,8 +67197,6 @@ async function createPartner(config, credentials, dependencies = {}) {
67125
67197
  keetFeed?.close();
67126
67198
  for (const unsubscribe of unsubscribeAppServer.splice(0)) unsubscribe();
67127
67199
  for (const controller of turnControllers.values()) controller.abort();
67128
- compactWaiter?.reject(/* @__PURE__ */ new Error("Partner is closing"));
67129
- compactWaiter = void 0;
67130
67200
  compacting = false;
67131
67201
  const serverClose = appServer?.close() ?? Promise.resolve();
67132
67202
  closePromise = (async () => {
@@ -67896,6 +67966,25 @@ function createWebServer(partner, assets, options = {}) {
67896
67966
  response.end(image.data);
67897
67967
  return;
67898
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
+ }
67899
67988
  if (path.startsWith("/api/audio/") && request.method === "GET") {
67900
67989
  const id = string().regex(/^[a-f0-9]{64}\.mp3$/u).parse(path.slice(11));
67901
67990
  const audio = await partner.audio(id.slice(0, -4));
@@ -67951,7 +68040,7 @@ function createWebServer(partner, assets, options = {}) {
67951
68040
  }
67952
68041
  if (path === "/api/compact" && request.method === "POST") {
67953
68042
  await partner.compact();
67954
- return json(response, { ok: true });
68043
+ return json(response, { ok: true }, 202);
67955
68044
  }
67956
68045
  if (path === "/api/cancel" && request.method === "POST") {
67957
68046
  const { id } = object({ id: string().min(1).max(300) }).strict().parse(await body(request));
@@ -68052,8 +68141,7 @@ if (nativePackageRoot) {
68052
68141
  ...config,
68053
68142
  codex: {
68054
68143
  ...config.codex,
68055
- command: join(root, "bin", "codex-app-server"),
68056
- provenance: join(root, "provenance.json")
68144
+ command: join(root, "bin", "codex-app-server")
68057
68145
  }
68058
68146
  };
68059
68147
  }
@@ -68111,7 +68199,7 @@ if (command === "credential") {
68111
68199
  await partner.close();
68112
68200
  process.exitCode = 1;
68113
68201
  });
68114
- 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}`));
68115
68203
  let stopping = false;
68116
68204
  const stop = async () => {
68117
68205
  if (stopping) return;