@lamplitisles/codex-for-love 0.4.3 → 0.5.1

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,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
@@ -63237,6 +63240,21 @@ var Store = class {
63237
63240
  };
63238
63241
  }));
63239
63242
  }
63243
+ async messageMeta(id) {
63244
+ return this.transaction(() => {
63245
+ const row = this.db.prepare(`
63246
+ SELECT m.sequence,m.id,m.created,r.revision
63247
+ FROM message_meta m JOIN message_revisions r ON r.message_id=m.id
63248
+ WHERE m.id=?
63249
+ `).get(id);
63250
+ return row ? {
63251
+ id: row.id,
63252
+ created: Number(row.created),
63253
+ sequence: Number(row.sequence),
63254
+ revision: Number(row.revision)
63255
+ } : void 0;
63256
+ });
63257
+ }
63240
63258
  async pendingMessages() {
63241
63259
  return this.transaction(() => this.db.prepare(`
63242
63260
  SELECT m.sequence,m.id,m.created,r.revision,p.input
@@ -64439,6 +64457,7 @@ function buildRollout(extraction, media, threadId, destination, persona, relatio
64439
64457
  lastByTurn.set(record.turn, index);
64440
64458
  }
64441
64459
  });
64460
+ const startedAtByTurn = new Map([...firstByTurn].map(([turn, index]) => [turn, Math.floor(extraction.records[index].time / 1e3)]));
64442
64461
  for (const [index, record] of extraction.records.entries()) {
64443
64462
  const timestamp = new Date(record.time).toISOString();
64444
64463
  if (record.type === "compact") {
@@ -64477,6 +64496,7 @@ function buildRollout(extraction, media, threadId, destination, persona, relatio
64477
64496
  type: "task_complete",
64478
64497
  turn_id: turnId,
64479
64498
  last_agent_message: null,
64499
+ started_at: Math.floor(record.time / 1e3),
64480
64500
  completed_at: Math.floor(record.time / 1e3)
64481
64501
  }, timestamp);
64482
64502
  boundaries.push({
@@ -64489,10 +64509,11 @@ function buildRollout(extraction, media, threadId, destination, persona, relatio
64489
64509
  continue;
64490
64510
  }
64491
64511
  const turnId = `turn-${record.turn}`;
64512
+ const startedAt = startedAtByTurn.get(record.turn);
64492
64513
  if (firstByTurn.get(record.turn) === index) add("event_msg", {
64493
64514
  type: "task_started",
64494
64515
  turn_id: turnId,
64495
- started_at: Math.floor(record.time / 1e3),
64516
+ started_at: startedAt,
64496
64517
  model_context_window: null,
64497
64518
  collaboration_mode_kind: "default"
64498
64519
  }, timestamp);
@@ -64548,7 +64569,7 @@ function buildRollout(extraction, media, threadId, destination, persona, relatio
64548
64569
  type: "task_complete",
64549
64570
  turn_id: turnId,
64550
64571
  last_agent_message: record.type === "assistant" ? record.text : null,
64551
- started_at: null,
64572
+ started_at: startedAt,
64552
64573
  completed_at: Math.floor(record.time / 1e3),
64553
64574
  duration_ms: null
64554
64575
  }, timestamp);
@@ -65478,17 +65499,92 @@ async function readClip(root, name, frameCount) {
65478
65499
  return;
65479
65500
  }
65480
65501
  }
65502
+ //#endregion
65503
+ //#region apps/partner/runtime/conversation-images.ts
65504
+ const CURSOR_BYTES = 40;
65505
+ const CURSOR_TOKEN = /^[A-Za-z0-9_-]{54}$/u;
65506
+ const IMAGE_ID = /^[a-f0-9]{64}$/u;
65507
+ function cursorFor(image) {
65508
+ if (!Number.isSafeInteger(image.created) || image.created < 0 || !IMAGE_ID.test(image.id)) throw new Error("Invalid conversation image cursor data");
65509
+ const value = Buffer.alloc(CURSOR_BYTES);
65510
+ value.writeBigUInt64BE(BigInt(image.created));
65511
+ Buffer.from(image.id, "hex").copy(value, 8);
65512
+ return value.toString("base64url");
65513
+ }
65514
+ function readCursor(cursor) {
65515
+ if (!cursor) return void 0;
65516
+ if (!CURSOR_TOKEN.test(cursor)) return void 0;
65517
+ try {
65518
+ const value = Buffer.from(cursor, "base64url");
65519
+ if (value.length !== CURSOR_BYTES) return void 0;
65520
+ const created = Number(value.readBigUInt64BE());
65521
+ if (!Number.isSafeInteger(created)) return void 0;
65522
+ return [created, value.subarray(8).toString("hex")];
65523
+ } catch {
65524
+ return;
65525
+ }
65526
+ }
65527
+ function conversationImageId(origin, source) {
65528
+ return createHash("sha256").update(`${origin}:${source}`).digest("hex");
65529
+ }
65530
+ function pageConversationImages(images, limit = 5, cursor) {
65531
+ const after = readCursor(cursor);
65532
+ if (cursor && !after) throw new Error("Invalid conversation image cursor");
65533
+ 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));
65534
+ const page = ordered.slice(0, limit);
65535
+ return {
65536
+ images: page,
65537
+ ...ordered.length > page.length ? { nextCursor: cursorFor(page.at(-1)) } : {}
65538
+ };
65539
+ }
65540
+ async function readConversationImages(path) {
65541
+ try {
65542
+ const value = JSON.parse(await readFile(path, "utf8"));
65543
+ return Array.isArray(value) ? value : [];
65544
+ } catch (error) {
65545
+ if (error.code === "ENOENT") return [];
65546
+ throw error;
65547
+ }
65548
+ }
65549
+ async function withAvailability(images) {
65550
+ return Promise.all(images.map(async (image) => {
65551
+ try {
65552
+ await access(image.path);
65553
+ return {
65554
+ ...image,
65555
+ available: true
65556
+ };
65557
+ } catch {
65558
+ return {
65559
+ ...image,
65560
+ available: false
65561
+ };
65562
+ }
65563
+ }));
65564
+ }
65565
+ /** Atomic metadata projection shared by the HTTP host and the standalone MCP. */
65566
+ async function writeConversationImages(path, images) {
65567
+ await mkdir(dirname(path), {
65568
+ recursive: true,
65569
+ mode: 448
65570
+ });
65571
+ const target = resolve(path);
65572
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
65573
+ await writeFile(temporary, `${JSON.stringify(images)}\n`, { mode: 384 });
65574
+ await rename(temporary, target);
65575
+ }
65481
65576
  const HISTORY_PAGE_MESSAGES = 50;
65577
+ const HISTORICAL_FALLBACK_START = new Date(2026, 8, 1).getTime();
65482
65578
  /** Match DSH's 50-message history window against CFL's rendered projection. */
65483
65579
  function visibleHistoryPage(messages, results, before) {
65484
65580
  const candidates = messages.filter((message) => before === void 0 || message.sequence < before);
65485
- const resultByOwner = new Map(results.map((result) => [result.sourceIds.at(-1), result]));
65581
+ const resultBySource = new Map(results.map((result) => [result.sourceIds.at(-1), result]));
65486
65582
  const selected = [];
65487
65583
  let visible = 0;
65488
65584
  let index = candidates.length - 1;
65489
65585
  for (; index >= 0; index -= 1) {
65490
65586
  const message = candidates[index];
65491
- const result = resultByOwner.get(message.id);
65587
+ const result = resultBySource.get(message.id);
65492
65588
  const units = 1 + (result ? result.answers.length + result.generatedIds.length + result.voiceIds.length + (result.error || [
65493
65589
  "failed",
65494
65590
  "interrupted",
@@ -65531,11 +65627,17 @@ function turnStatus(turn) {
65531
65627
  function isActiveTurn(turn) {
65532
65628
  return turnStatus(turn) === "inProgress" || turnStatus(turn) === "in_progress" || turnStatus(turn) === "active";
65533
65629
  }
65534
- function turnTime(value, fallback) {
65630
+ function recordedTurnTime(value) {
65535
65631
  const number = numberOrNull(value);
65536
- if (number === null) return fallback;
65632
+ if (number === null) return void 0;
65537
65633
  return number > 1e10 ? Math.round(number) : Math.round(number * 1e3);
65538
65634
  }
65635
+ function turnTime(value, fallback) {
65636
+ return recordedTurnTime(value) ?? fallback;
65637
+ }
65638
+ function historicalFallbackTime(sequence) {
65639
+ return HISTORICAL_FALLBACK_START + Math.max(0, sequence - 1) * 1e3;
65640
+ }
65539
65641
  function completedTurnTime(value) {
65540
65642
  const raw = numberOrNull(value);
65541
65643
  if (raw === null) return void 0;
@@ -65546,7 +65648,7 @@ function inputTimeContext(source, now) {
65546
65648
  const time = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
65547
65649
  return {
65548
65650
  kind: "application",
65549
- value: `${source === "owner" ? "Current owner input" : "Qualifying Keet input"} received around local ${time}. Trusted delivery metadata; not user-authored text or an instruction.`
65651
+ value: `${source === "human" ? "Current human input" : "Qualifying Keet input"} received around local ${time}. Trusted delivery metadata; not user-authored text or an instruction.`
65550
65652
  };
65551
65653
  }
65552
65654
  function textFromUserItem(item) {
@@ -65654,6 +65756,9 @@ function historyImage(path, operationId, index) {
65654
65756
  function noHistoryYet(error) {
65655
65757
  return error instanceof AppServerInvalidRequestError && error.rpcMessage.includes("thread/turns/list is unavailable before first user message");
65656
65758
  }
65759
+ function noRolloutForThread(error) {
65760
+ return error instanceof AppServerInvalidRequestError && error.rpcMessage.startsWith("no rollout found for thread id ");
65761
+ }
65657
65762
  function rpcMessage(error) {
65658
65763
  return error instanceof AppServerInvalidRequestError ? error.rpcMessage : void 0;
65659
65764
  }
@@ -65770,10 +65875,33 @@ async function createPartner(config, credentials, dependencies = {}) {
65770
65875
  recursive: true,
65771
65876
  mode: 448
65772
65877
  });
65878
+ const catalogue = new Map((await readConversationImages(paths.conversationImages)).map((image) => [image.id, image]));
65879
+ let reconcilingCatalogue = false;
65880
+ let reconciledCatalogueIds = /* @__PURE__ */ new Set();
65881
+ async function catalogueImage(origin, image, created) {
65882
+ const id = conversationImageId(origin, `${image.operation_id}:${image.id}`);
65883
+ const previous = catalogue.get(id);
65884
+ catalogue.set(id, {
65885
+ id,
65886
+ filename: image.name,
65887
+ path: resolve(image.path),
65888
+ mediaType: image.media_type,
65889
+ created: reconcilingCatalogue ? created : previous?.created ?? created,
65890
+ origin,
65891
+ available: true
65892
+ });
65893
+ reconciledCatalogueIds.add(id);
65894
+ if (!reconcilingCatalogue) await writeConversationImages(paths.conversationImages, await withAvailability([...catalogue.values()]));
65895
+ }
65773
65896
  const store = new Store(paths.database);
65774
65897
  const now = dependencies.now ?? Date.now;
65775
65898
  const listeners = /* @__PURE__ */ new Set();
65776
- const messageIds = new Set((await store.allMessages()).map((message) => message.id));
65899
+ const storedMessages = await store.allMessages();
65900
+ const messageIds = new Set(storedMessages.map((message) => message.id));
65901
+ const messagesById = new Map(storedMessages.map((message) => [message.id, {
65902
+ created: message.created,
65903
+ sequence: message.sequence
65904
+ }]));
65777
65905
  const turns = /* @__PURE__ */ new Map();
65778
65906
  /** One turn owns an ordered list of application input identities. */
65779
65907
  const turnInputs = /* @__PURE__ */ new Map();
@@ -65801,7 +65929,6 @@ async function createPartner(config, credentials, dependencies = {}) {
65801
65929
  let unsubscribeAppServer = [];
65802
65930
  let startupPending = false;
65803
65931
  let compacting = false;
65804
- let compactWaiter;
65805
65932
  let eventChain = Promise.resolve();
65806
65933
  let admission = Promise.resolve();
65807
65934
  let closePromise;
@@ -65971,13 +66098,13 @@ async function createPartner(config, credentials, dependencies = {}) {
65971
66098
  return status === "failed" ? "Native image generation failed" : "Native image artifact unavailable";
65972
66099
  }
65973
66100
  async function markAttachmentError(sourceIds, error) {
65974
- const owner = sourceIds.at(-1);
65975
- if (!owner) return;
65976
- const previous = await store.outcome(owner);
66101
+ const sourceId = sourceIds.at(-1);
66102
+ if (!sourceId) return;
66103
+ const previous = await store.outcome(sourceId);
65977
66104
  if (previous?.status === "replaced") return;
65978
- if (previous?.status !== "attachment-error" || previous.error !== error) await store.markOutcome(owner, "attachment-error", error);
66105
+ if (previous?.status !== "attachment-error" || previous.error !== error) await store.markOutcome(sourceId, "attachment-error", error);
65979
66106
  }
65980
- async function projectGeneratedImage(operationId, sourceIds, item) {
66107
+ async function projectGeneratedImage(operationId, sourceIds, item, created) {
65981
66108
  if (item.type !== "imageGeneration") return void 0;
65982
66109
  if (item.status === "failed") {
65983
66110
  processError("image.generation_failed", new Error(imageFailure("failed")), {
@@ -65992,6 +66119,7 @@ async function createPartner(config, credentials, dependencies = {}) {
65992
66119
  const existing = await store.generatedImageForItem(operationId, itemId);
65993
66120
  if (existing) try {
65994
66121
  await access(existing.path);
66122
+ await catalogueImage("agent", existing, created);
65995
66123
  return existing.id;
65996
66124
  } catch {
65997
66125
  storageError = true;
@@ -66014,6 +66142,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66014
66142
  return;
66015
66143
  }
66016
66144
  await store.saveGeneratedImage(generated, itemId);
66145
+ await catalogueImage("agent", generated, created);
66017
66146
  return generated.id;
66018
66147
  } catch (error) {
66019
66148
  storageError = true;
@@ -66100,9 +66229,20 @@ async function createPartner(config, credentials, dependencies = {}) {
66100
66229
  text_offset: 0,
66101
66230
  text_length: source.input.length
66102
66231
  };
66103
- if (!messageIds.has(sourceId)) {
66104
- await store.ensureMessage(sourceId, turnTime(turn.startedAt, now()), source.input, source.images);
66232
+ let message = messagesById.get(sourceId);
66233
+ if (!message) {
66234
+ const created = turnTime(turn.startedAt, now());
66235
+ const stored = await store.ensureMessage(sourceId, created, source.input, source.images);
66105
66236
  messageIds.add(sourceId);
66237
+ message = {
66238
+ created: stored.created,
66239
+ sequence: stored.sequence
66240
+ };
66241
+ messagesById.set(sourceId, message);
66242
+ }
66243
+ if (!sourceId.startsWith("keet:")) for (const image of source.images) {
66244
+ const origin = isUnder(join(paths.managedRoot, "historical-media"), image.path) ? "historical" : "human";
66245
+ await catalogueImage(origin, image, origin === "historical" ? recordedTurnTime(turn.startedAt) ?? historicalFallbackTime(message.sequence) : message.created);
66106
66246
  }
66107
66247
  await store.markObserved(sourceId, segment);
66108
66248
  }
@@ -66116,7 +66256,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66116
66256
  const completedAt = status === "completed" ? completedTurnTime(turn.completedAt) : void 0;
66117
66257
  for (const item of turn.items ?? []) {
66118
66258
  if (item.type !== "imageGeneration") continue;
66119
- const generatedId = await projectGeneratedImage(resultOperationId(turn.id), sourceIds, item);
66259
+ const generatedId = await projectGeneratedImage(resultOperationId(turn.id), sourceIds, item, completedTurnTime(turn.completedAt) ?? turnTime(turn.startedAt, now()));
66120
66260
  if (generatedId) generatedIds.push(generatedId);
66121
66261
  else if (item.status === "failed") {
66122
66262
  resultStatus = "failed";
@@ -66146,15 +66286,23 @@ async function createPartner(config, credentials, dependencies = {}) {
66146
66286
  }
66147
66287
  /** Full history is hydrated once at startup/resume; events update one turn thereafter. */
66148
66288
  async function hydrateHistory() {
66289
+ reconcilingCatalogue = true;
66290
+ reconciledCatalogueIds = /* @__PURE__ */ new Set();
66149
66291
  const nextTurns = await listTurns();
66150
66292
  turns.clear();
66151
66293
  turnInputs.clear();
66152
66294
  turnResults.clear();
66153
66295
  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);
66296
+ try {
66297
+ for (const turn of nextTurns) {
66298
+ registerTurn(turn);
66299
+ const complete = await reconcileTurn(turn.id, turn);
66300
+ if (complete) await projectTurn(complete, false);
66301
+ }
66302
+ } finally {
66303
+ reconcilingCatalogue = false;
66304
+ for (const id of catalogue.keys()) if (!reconciledCatalogueIds.has(id)) catalogue.delete(id);
66305
+ await writeConversationImages(paths.conversationImages, await withAvailability([...catalogue.values()]));
66158
66306
  }
66159
66307
  const unresolved = await unresolvedMessages();
66160
66308
  for (const message of unresolved) unresolvedInputIds.add(message.id);
@@ -66307,7 +66455,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66307
66455
  }
66308
66456
  }
66309
66457
  function drainRejectedSteers() {
66310
- if (closing || activeTurnId || recoveryPromise || !rejectedSteers.length) return recoveryPromise;
66458
+ if (closing || compacting || activeTurnId || recoveryPromise || !rejectedSteers.length) return recoveryPromise;
66311
66459
  const intents = rejectedSteers.splice(0);
66312
66460
  const ids = intents.flatMap((intent) => intent.ids);
66313
66461
  const merged = {
@@ -66315,7 +66463,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66315
66463
  input: intents.map((intent) => intent.input).join("\n"),
66316
66464
  images: intents.flatMap((intent) => intent.images),
66317
66465
  transportId: clientIdForInputs(ids),
66318
- source: intents.every((intent) => intent.source === "owner") ? "owner" : "none"
66466
+ source: intents.every((intent) => intent.source === "human") ? "human" : "none"
66319
66467
  };
66320
66468
  recoveryPromise = (async () => {
66321
66469
  try {
@@ -66331,7 +66479,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66331
66479
  }
66332
66480
  function drainKeet() {
66333
66481
  admission = admission.then(async () => {
66334
- if (closing || activeTurnId || recoveryPromise || !keetEnabled) return;
66482
+ if (closing || compacting || activeTurnId || recoveryPromise || !keetEnabled) return;
66335
66483
  const pending = (await store.pendingMessages()).filter((item) => item.id.startsWith("keet:") && !observedInputIds.has(item.id));
66336
66484
  if (!pending.length || activeTurnId || closing) return;
66337
66485
  const first = pending[0];
@@ -66433,11 +66581,11 @@ async function createPartner(config, credentials, dependencies = {}) {
66433
66581
  scheduleKeetReconnect();
66434
66582
  });
66435
66583
  }
66436
- async function restoreUnconsumed(turnId) {
66584
+ async function restoreUnconsumed(turnId, preserveQueued = false) {
66437
66585
  const ids = turnId ? [...sourceIdsForTurn(turnId)] : [];
66438
66586
  const selectedSteers = pendingSteers.filter((intent) => turnId === void 0 || intent.turnId === turnId);
66439
66587
  const selectedStarts = [...pendingStarts.values()].filter((intent) => turnId === void 0 || intent.turnId === turnId);
66440
- const selectedRejected = rejectedSteers.filter((intent) => turnId === void 0 || intent.turnId === turnId);
66588
+ const selectedRejected = preserveQueued ? [] : rejectedSteers.filter((intent) => turnId === void 0 || intent.turnId === turnId);
66441
66589
  for (const intent of selectedSteers) ids.push(...intent.ids);
66442
66590
  for (const intent of selectedStarts) ids.push(...intent.ids);
66443
66591
  for (const intent of selectedRejected) ids.push(...intent.ids);
@@ -66505,18 +66653,14 @@ async function createPartner(config, credentials, dependencies = {}) {
66505
66653
  endedAt: boundary.time
66506
66654
  };
66507
66655
  await refreshBootstrap();
66508
- compactWaiter?.resolve();
66509
- compactWaiter = void 0;
66510
66656
  }
66511
- function failCompaction(error) {
66657
+ function failCompaction() {
66512
66658
  if (lifecycle?.status === "running") lifecycle = {
66513
66659
  ...lifecycle,
66514
66660
  status: "failed",
66515
66661
  endSeq: eventSequence,
66516
66662
  endedAt: now()
66517
66663
  };
66518
- compactWaiter?.reject(error instanceof Error ? error : new Error(String(error)));
66519
- compactWaiter = void 0;
66520
66664
  }
66521
66665
  async function handleServerEvent(notification) {
66522
66666
  if (closing) return;
@@ -66598,11 +66742,14 @@ async function createPartner(config, credentials, dependencies = {}) {
66598
66742
  turnControllers.delete(turn.id);
66599
66743
  }
66600
66744
  if (reconciled && (turnStatus(reconciled) === "interrupted" || turnStatus(reconciled) === "cancelled")) await restoreUnconsumed(completedTurnId);
66601
- else if (reconciled && !userItems(reconciled).length && completedTurnId) await restoreUnconsumed(completedTurnId);
66745
+ else if (reconciled && !userItems(reconciled).length && completedTurnId) await restoreUnconsumed(completedTurnId, turnStatus(reconciled) === "completed");
66602
66746
  syncActiveTurn();
66603
66747
  if (compacting) {
66604
66748
  compacting = false;
66605
- if (lifecycle?.status === "running") await finishCompaction(reconciled?.id);
66749
+ if (lifecycle?.status === "running") {
66750
+ if (turnStatus(reconciled) === "completed") await finishCompaction(reconciled?.id);
66751
+ else failCompaction();
66752
+ }
66606
66753
  }
66607
66754
  await refreshBootstrap();
66608
66755
  if (!activeTurnId) {
@@ -66626,6 +66773,9 @@ async function createPartner(config, credentials, dependencies = {}) {
66626
66773
  if (notification.method === "turn/started") {
66627
66774
  const turn = registerTurn(record(notification.params).turn);
66628
66775
  if (turn) {
66776
+ if (compacting) {
66777
+ for (const intent of rejectedSteers) if (!intent.turnId) intent.turnId = turn.id;
66778
+ }
66629
66779
  activeTurnId = turn.id;
66630
66780
  activeOperationId = sourceIdsForTurn(turn.id)[0] ?? activeOperationId;
66631
66781
  if (!turnControllers.has(turn.id)) turnControllers.set(turn.id, new AbortController());
@@ -66723,29 +66873,44 @@ async function createPartner(config, credentials, dependencies = {}) {
66723
66873
  } : {}
66724
66874
  }
66725
66875
  };
66876
+ const connectedAppServer = appServer;
66877
+ if (!connectedAppServer) throw new Error("Codex app-server connection was not initialized");
66726
66878
  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 {
66879
+ let resumed = false;
66880
+ const startThread = async () => {
66736
66881
  const start = {
66737
66882
  ...shared,
66738
66883
  historyMode: "paginated",
66739
66884
  sessionStartSource: "startup"
66740
66885
  };
66741
- response = await appServer.call("thread/start", start);
66742
- threadId = response.thread.id;
66886
+ const started = await connectedAppServer.call("thread/start", start);
66887
+ threadId = started.thread.id;
66743
66888
  if (!threadId) throw new Error("Codex app-server did not return a thread id");
66744
66889
  await writeFile(markerPath, JSON.stringify({
66745
66890
  threadId,
66746
66891
  model: config.codex.model
66747
66892
  }), { mode: 384 });
66748
- }
66893
+ return started;
66894
+ };
66895
+ if (marker?.threadId) {
66896
+ const resume = {
66897
+ ...shared,
66898
+ threadId: marker.threadId,
66899
+ excludeTurns: true
66900
+ };
66901
+ try {
66902
+ response = await connectedAppServer.call("thread/resume", resume);
66903
+ threadId = response.thread.id;
66904
+ resumed = true;
66905
+ } catch (error) {
66906
+ if (!noRolloutForThread(error) || messageIds.size) throw error;
66907
+ await unlink(markerPath);
66908
+ marker = void 0;
66909
+ startupPending = true;
66910
+ await refreshBootstrap();
66911
+ response = await startThread();
66912
+ }
66913
+ } else response = await startThread();
66749
66914
  const readinessDeadline = Date.now() + 1e4;
66750
66915
  for (;;) {
66751
66916
  const mcpStatuses = [];
@@ -66765,7 +66930,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66765
66930
  await new Promise((resolve) => setTimeout(resolve, 100));
66766
66931
  }
66767
66932
  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({
66933
+ if (resumed && marker?.threadId && marker.model !== config.codex.model) await writeFile(markerPath, JSON.stringify({
66769
66934
  threadId,
66770
66935
  model: config.codex.model
66771
66936
  }), { mode: 384 });
@@ -66778,7 +66943,6 @@ async function createPartner(config, credentials, dependencies = {}) {
66778
66943
  closing = true;
66779
66944
  for (const unsubscribe of unsubscribeAppServer.splice(0)) unsubscribe();
66780
66945
  for (const controller of turnControllers.values()) controller.abort();
66781
- compactWaiter?.reject(error instanceof Error ? error : new Error(String(error)));
66782
66946
  await appServer?.close().catch(() => void 0);
66783
66947
  await store.close().catch(() => void 0);
66784
66948
  throw error;
@@ -66841,6 +67005,34 @@ async function createPartner(config, credentials, dependencies = {}) {
66841
67005
  return;
66842
67006
  }
66843
67007
  },
67008
+ async conversationImages(options = {}) {
67009
+ await eventChain;
67010
+ const page = pageConversationImages(await withAvailability([...catalogue.values()]), options.limit ?? 5, options.cursor);
67011
+ return {
67012
+ images: page.images.map(({ id, filename, mediaType, created, origin, available }) => ({
67013
+ id,
67014
+ filename,
67015
+ mediaType,
67016
+ created,
67017
+ origin,
67018
+ available,
67019
+ url: `/api/conversation-images/${id}`
67020
+ })),
67021
+ ...page.nextCursor ? { nextCursor: page.nextCursor } : {}
67022
+ };
67023
+ },
67024
+ async conversationImage(id) {
67025
+ const image = catalogue.get(id);
67026
+ if (!image) return void 0;
67027
+ try {
67028
+ return {
67029
+ media_type: image.mediaType,
67030
+ data: await readFile(image.path)
67031
+ };
67032
+ } catch {
67033
+ return;
67034
+ }
67035
+ },
66844
67036
  avatar: (kind) => avatars[kind],
66845
67037
  async petAsset(activity) {
66846
67038
  return pet ? localPetClip(config.state, activity) : void 0;
@@ -66956,6 +67148,11 @@ async function createPartner(config, credentials, dependencies = {}) {
66956
67148
  const materialized = await materializeImages(paths.workspaceRoot, images);
66957
67149
  await store.admit(id, input, materialized);
66958
67150
  messageIds.add(id);
67151
+ const message = await store.messageMeta(id);
67152
+ if (message) messagesById.set(id, {
67153
+ created: message.created,
67154
+ sequence: message.sequence
67155
+ });
66959
67156
  if (operationTurn(id) || pendingStarts.has(id) || pendingSteers.some((intent) => intent.ids.includes(id)) || await store.outcome(id)) return;
66960
67157
  if (unresolvedInputIds.has(id) && !replaces.includes(id)) throw new Error("Message delivery is unresolved; inspect the conversation before retrying");
66961
67158
  for (const replacedId of replaces) {
@@ -66968,14 +67165,19 @@ async function createPartner(config, credentials, dependencies = {}) {
66968
67165
  if (replaces.length) await setRestoredDraft([...unresolvedInputIds]);
66969
67166
  await eventChain;
66970
67167
  syncActiveTurn();
66971
- if (rejectedSteers.length) await (drainRejectedSteers() ?? Promise.resolve());
66972
67168
  const intent = {
66973
67169
  ids: [id],
66974
67170
  input,
66975
67171
  images: materialized,
66976
67172
  transportId: id,
66977
- source: "owner"
67173
+ source: "human"
66978
67174
  };
67175
+ if (compacting && !activeTurnId) {
67176
+ rejectedSteers.push(intent);
67177
+ notify();
67178
+ return;
67179
+ }
67180
+ if (rejectedSteers.length) await (drainRejectedSteers() ?? Promise.resolve());
66979
67181
  if (activeTurnId) await steerIntent(intent, activeTurnId);
66980
67182
  else await startIntent(intent);
66981
67183
  notify();
@@ -67009,27 +67211,15 @@ async function createPartner(config, credentials, dependencies = {}) {
67009
67211
  startedAt: now()
67010
67212
  };
67011
67213
  compacting = true;
67012
- const wait = new Promise((resolveWait, rejectWait) => {
67013
- compactWaiter = {
67014
- resolve: resolveWait,
67015
- reject: rejectWait
67016
- };
67017
- });
67018
- let timeout;
67214
+ notify();
67019
67215
  try {
67020
67216
  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
67217
  } catch (error) {
67025
67218
  compacting = false;
67026
- failCompaction(error);
67219
+ failCompaction();
67027
67220
  processError("compaction.failed", error);
67028
67221
  notify();
67029
67222
  throw error;
67030
- } finally {
67031
- if (timeout) clearTimeout(timeout);
67032
- compactWaiter = void 0;
67033
67223
  }
67034
67224
  notify();
67035
67225
  });
@@ -67052,8 +67242,6 @@ async function createPartner(config, credentials, dependencies = {}) {
67052
67242
  keetFeed?.close();
67053
67243
  for (const unsubscribe of unsubscribeAppServer.splice(0)) unsubscribe();
67054
67244
  for (const controller of turnControllers.values()) controller.abort();
67055
- compactWaiter?.reject(/* @__PURE__ */ new Error("Partner is closing"));
67056
- compactWaiter = void 0;
67057
67245
  compacting = false;
67058
67246
  const serverClose = appServer?.close() ?? Promise.resolve();
67059
67247
  closePromise = (async () => {
@@ -67823,6 +68011,25 @@ function createWebServer(partner, assets, options = {}) {
67823
68011
  response.end(image.data);
67824
68012
  return;
67825
68013
  }
68014
+ if (path === "/api/conversation-images" && request.method === "GET") {
68015
+ const params = Object.fromEntries(new URL(request.url, "http://localhost").searchParams);
68016
+ const options = object({
68017
+ limit: number().int().min(1).max(50).optional(),
68018
+ cursor: string().min(1).max(500).optional()
68019
+ }).strict().parse(params);
68020
+ return json(response, await partner.conversationImages(options));
68021
+ }
68022
+ if (path.startsWith("/api/conversation-images/") && request.method === "GET") {
68023
+ const id = string().regex(/^[a-f0-9]{64}$/u).parse(path.slice(25));
68024
+ const image = await partner.conversationImage(id);
68025
+ if (!image) return json(response, { error: "Image not found" }, 404);
68026
+ response.writeHead(200, {
68027
+ "content-type": image.media_type,
68028
+ "cache-control": "private, max-age=31536000, immutable"
68029
+ });
68030
+ response.end(image.data);
68031
+ return;
68032
+ }
67826
68033
  if (path.startsWith("/api/audio/") && request.method === "GET") {
67827
68034
  const id = string().regex(/^[a-f0-9]{64}\.mp3$/u).parse(path.slice(11));
67828
68035
  const audio = await partner.audio(id.slice(0, -4));
@@ -67878,7 +68085,7 @@ function createWebServer(partner, assets, options = {}) {
67878
68085
  }
67879
68086
  if (path === "/api/compact" && request.method === "POST") {
67880
68087
  await partner.compact();
67881
- return json(response, { ok: true });
68088
+ return json(response, { ok: true }, 202);
67882
68089
  }
67883
68090
  if (path === "/api/cancel" && request.method === "POST") {
67884
68091
  const { id } = object({ id: string().min(1).max(300) }).strict().parse(await body(request));
@@ -68037,7 +68244,7 @@ if (command === "credential") {
68037
68244
  await partner.close();
68038
68245
  process.exitCode = 1;
68039
68246
  });
68040
- app.server.listen(config.port, "127.0.0.1", () => console.log(`Companion: http://127.0.0.1:${config.port}`));
68247
+ app.server.listen(config.port, config.listen_host, () => console.log(`Companion: http://${config.listen_host}:${config.port}`));
68041
68248
  let stopping = false;
68042
68249
  const stop = async () => {
68043
68250
  if (stopping) return;