Skip to content

feat: engine-native image references via kimi-file:// media resolver - #2593

Open
7Sageer wants to merge 61 commits into
mainfrom
feat/image-file-ref
Open

feat: engine-native image references via kimi-file:// media resolver#2593
7Sageer wants to merge 61 commits into
mainfrom
feat/image-file-ref

Conversation

@7Sageer

@7Sageer 7Sageer commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — the problem is explained below. Opening as a draft for early discussion per CONTRIBUTING.

Problem

Pasted/uploaded images are inlined into prompts as base64 data URLs with no on-disk file. As a result the model cannot re-read an image after the context projector drops old image parts (the "re-read the file" hint is a dead end for pasted images), cannot process it with tools (Bash/Python), and cannot feed it to file-based workflows. Video already has engine-native file references (kimi-file:// + a resolver); images don't.

What changed

Images now follow the same engine-native reference model as video, unified under one media-ref abstraction:

  • media-ref contract (agent-core-v2's media domain; packages/kosong itself is untouched): new mediaRef.tskimi-file:// URL helpers, MediaKind classification, MIME/extension tables, and <image|video|audio path> tag build/match helpers, with a transcript-side contract copy at packages/transcript/src/contract/mediaRef.ts. kimiFileUrl.ts and the file-type suffix table become aliases/re-exports.
  • Resolver generalization: AgentVideoResolverServiceAgentMediaResolverService (old names kept as deprecated aliases). Image strategy is inline-only: references resolve to base64 data URLs at LLM request time; when the referenced file is gone, the part is dropped if an adjacent <image path> tag already conveys the path (the model can re-read it with ReadMediaFile), otherwise an [image omitted] notice is emitted. Video semantics are byte-identical.
  • kap-server edge: image uploads persist bytes to the session file store and write [caption?, <image path> text, image_url kimi-file ref]; compression-altered bytes are saved as a new upload. Transcript projection emits {kind:'file'} without leaking internal URLs. The generic-file flow, the ATTACHED_FILE_NOTICE wording, and the REST inline-base64 path are untouched.
  • klient: global.files save/get/delete over both memory and ipc transports (base64 wire format).
  • node-sdk: harness.uploadFile() plus re-exported daemon file-reference helpers (changeset: @moonshot-ai/kimi-code-sdk minor).
  • TUI (engine v2 only): pasting an image uploads it immediately and submit produces the dual form synchronously; v1 behavior is unchanged; an upload failure silently falls back to inline base64.
  • transcript: pins current cold-rebuild behavior with tests (media references — image and video alike — are dropped on cold rebuild; a pre-existing limitation to be fixed separately).

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update. (behind the experimental v2 engine; no user-facing doc surface yet)

@changeset-bot

changeset-bot Bot commented Aug 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 533df90

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@moonshot-ai/kimi-code Patch
@moonshot-ai/kimi-code-sdk Minor
@moonshot-ai/acp-adapter Patch
kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@7Sageer
7Sageer marked this pull request as ready for review August 5, 2026 05:31
@pkg-pr-new

pkg-pr-new Bot commented Aug 5, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@cb345be
npx https://pkg.pr.new/@moonshot-ai/kimi-code@cb345be

commit: cb345be

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

❌ Nix build failed

    kimi-code> [UNLOADABLE_DEPENDENCY] Could not load ../../packages/agent-core-v2/src/agent/tools/task/task-output/task-output.md?raw
    kimi-code>     ╭─[ ../../packages/agent-core-v2/src/agent/tools/task/task-output/taskOutputTool.ts:33:37 ]
    kimi-code>     │
    kimi-code>  33 │ import TASK_OUTPUT_DESCRIPTION from './task-output.md?raw';
    kimi-code>     │                                     ───────────┬──────────
    kimi-code>     │                                                ╰──────────── No such file or directory (os error 2)
    kimi-code> ────╯
    kimi-code> 
    kimi-code> [UNLOADABLE_DEPENDENCY] Could not load ../../packages/agent-core-v2/src/session/agentLifecycle/profile/explore-overlay.md?raw
    kimi-code>     ╭─[ ../../packages/agent-core-v2/src/session/agentLifecycle/profile/profiles.ts:18:26 ]
    kimi-code>     │
    kimi-code>  18 │ import EXPLORE_ROLE from './explore-overlay.md?raw';
    kimi-code>     │                          ─────────────┬────────────
    kimi-code>     │                                       ╰────────────── No such file or directory (os error 2)
    kimi-code> ────╯
    kimi-code> 
    kimi-code> [UNLOADABLE_DEPENDENCY] Could not load ../../packages/agent-core-v2/src/session/agentLifecycle/profile/summary-continuation.md?raw
    kimi-code>     ╭─[ ../../packages/agent-core-v2/src/session/agentLifecycle/profile/profiles.ts:19:41 ]
    kimi-code>     │
    kimi-code>  19 │ import SUMMARY_CONTINUATION_PROMPT from './summary-continuation.md?raw';
    kimi-code>     │                                         ───────────────┬───────────────
    kimi-code>     │                                                        ╰───────────────── No such file or directory (os error 2)
    kimi-code> ────╯
    kimi-code> 
    kimi-code> [UNLOADABLE_DEPENDENCY] Could not load ../../packages/agent-core-v2/src/agent/tools/todo-list/todo-list.md?raw
    kimi-code>     ╭─[ ../../packages/agent-core-v2/src/agent/tools/todo-list/todoListTool.ts:33:25 ]
    kimi-code>     │
    kimi-code>  33 │ import DESCRIPTION from './todo-list.md?raw';
    kimi-code>     │                         ──────────┬─────────
    kimi-code>     │                                   ╰─────────── No such file or directory (os error 2)
    kimi-code> ────╯
    kimi-code> 
    kimi-code> ...
    kimi-code>     at aggregateBindingErrorsIntoJsError (file:///build/source/node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/error-CkdMJ9ps.mjs:48:18)
    kimi-code>     at unwrapBindingResult (file:///build/source/node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/error-CkdMJ9ps.mjs:18:128)
    kimi-code>     at #build (file:///build/source/node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/rolldown-build-BVD3dIdE.mjs:3275:34)
    kimi-code>     at async build (file:///build/source/node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/index.mjs:42:22)
    kimi-code>     at async Promise.all (index 0)
    kimi-code>     at async buildSingle (file:///build/source/node_modules/.pnpm/tsdown@0.22.0_@arethetypeswrong+core@0.18.2_publint@0.3.18_tsx@4.21.0_typescript@6.0.2__e3c4b77bdbb7c42f8dbfce92959948a8/node_modules/tsdown/dist/build-5FURNVr0.mjs:765:19)
    kimi-code>     at async Promise.all (index 1)
    kimi-code>     at async buildWithConfigs (file:///build/source/node_modules/.pnpm/tsdown@0.22.0_@arethetypeswrong+core@0.18.2_publint@0.3.18_tsx@4.21.0_typescript@6.0.2__e3c4b77bdbb7c42f8dbfce92959948a8/node_modules/tsdown/dist/build-5FURNVr0.mjs:721:18)
    kimi-code>     at async CAC.<anonymous> (file:///build/source/node_modules/.pnpm/tsdown@0.22.0_@arethetypeswrong+core@0.18.2_publint@0.3.18_tsx@4.21.0_typescript@6.0.2__e3c4b77bdbb7c42f8dbfce92959948a8/node_modules/tsdown/dist/run.mjs:22:2)
    kimi-code>     at async runCLI (file:///build/source/node_modules/.pnpm/tsdown@0.22.0_@arethetypeswrong+core@0.18.2_publint@0.3.18_tsx@4.21.0_typescript@6.0.2__e3c4b77bdbb7c42f8dbfce92959948a8/node_modules/tsdown/dist/run.mjs:46:3)
    kimi-code> 
    kimi-code> 
    kimi-code> /build/source/apps/kimi-code:
    kimi-code>  ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL  @moonshot-ai/kimi-code@0.34.0 build:native:sea: `node scripts/native/build.mjs --profile=local`
    kimi-code> Exit status 1
    error: Cannot build '/nix/store/lsa2f8yl57gr54dglgpnjl8nhcqchhyg-kimi-code-0.34.0.drv'.
           Reason: builder failed with exit code 1.
           Output paths:
             /nix/store/4rig7m6l4fw7h4nncsmkxgw3l5hs8qqa-kimi-code-0.34.0
           Last 25 log lines:
           >
           > [UNLOADABLE_DEPENDENCY] Could not load ../../packages/agent-core-v2/src/agent/tools/todo-list/todo-list.md?raw
           >     ╭─[ ../../packages/agent-core-v2/src/agent/tools/todo-list/todoListTool.ts:33:25 ]
           >     │
           >  33 │ import DESCRIPTION from './todo-list.md?raw';
           >     │                         ──────────┬─────────
           >     │                                   ╰─────────── No such file or directory (os error 2)
           > ────╯
           >
           > ...
           >     at aggregateBindingErrorsIntoJsError (file:///build/source/node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/error-CkdMJ9ps.mjs:48:18)
           >     at unwrapBindingResult (file:///build/source/node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/error-CkdMJ9ps.mjs:18:128)
           >     at #build (file:///build/source/node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/rolldown-build-BVD3dIdE.mjs:3275:34)
           >     at async build (file:///build/source/node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/index.mjs:42:22)
           >     at async Promise.all (index 0)
           >     at async buildSingle (file:///build/source/node_modules/.pnpm/tsdown@0.22.0_@arethetypeswrong+core@0.18.2_publint@0.3.18_tsx@4.21.0_typescript@6.0.2__e3c4b77bdbb7c42f8dbfce92959948a8/node_modules/tsdown/dist/build-5FURNVr0.mjs:765:19)
           >     at async Promise.all (index 1)
           >     at async buildWithConfigs (file:///build/source/node_modules/.pnpm/tsdown@0.22.0_@arethetypeswrong+core@0.18.2_publint@0.3.18_tsx@4.21.0_typescript@6.0.2__e3c4b77bdbb7c42f8dbfce92959948a8/node_modules/tsdown/dist/build-5FURNVr0.mjs:721:18)
           >     at async CAC.<anonymous> (file:///build/source/node_modules/.pnpm/tsdown@0.22.0_@arethetypeswrong+core@0.18.2_publint@0.3.18_tsx@4.21.0_typescript@6.0.2__e3c4b77bdbb7c42f8dbfce92959948a8/node_modules/tsdown/dist/run.mjs:22:2)
           >     at async runCLI (file:///build/source/node_modules/.pnpm/tsdown@0.22.0_@arethetypeswrong+core@0.18.2_publint@0.3.18_tsx@4.21.0_typescript@6.0.2__e3c4b77bdbb7c42f8dbfce92959948a8/node_modules/tsdown/dist/run.mjs:46:3)
           >
           >
           > /build/source/apps/kimi-code:
           >  ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL  @moonshot-ai/kimi-code@0.34.0 build:native:sea: `node scripts/native/build.mjs --profile=local`
           > Exit status 1
           For full logs, run:
             nix log /nix/store/lsa2f8yl57gr54dglgpnjl8nhcqchhyg-kimi-code-0.34.0.drv

@7Hanrui

7Hanrui commented Aug 12, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb31813cc9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +88 to +89
const file = await store.open(part.source.file_id);
if (file === undefined) throw fileNotFoundError(part.source.file_id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate session media against the requested kind

When a client resubmits a stored attachment using the new session_media source, this path only checks that the file exists. Unlike transient file sources above, it never verifies file.mediaType against the requested image/video kind, so {type:'image', source:{kind:'session_media', file_id:<video>}} is accepted and persisted as an image daemon ref; the resolver can then degrade/mislabel the video as an <image path=...> instead of rejecting the bad prompt before controls/metadata mutate. Apply the same assertMediaFile check to the opened session media.

Useful? React with 👍 / 👎.

const tail = text.slice(cursor);
pushText(parts, tail);

store.retainFileIds(imageAttachmentIds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Release retained image uploads when cache hint restores input

This retain happens during extraction, before a submission is actually leased. If the idle cache-hint dialog intercepts a prompt with a pasted image and the user dismisses/restores the input, sendNormalUserInput returns before creating a StagingLease, so this retain is never balanced by takeFileIds; submitting the restored prompt later retains the same daemon file a second time, and turn-end cleanup only decrements one use, leaving the upload staged until transcript/session clear. Retain only after dispatch is leased, or release the retained ids when the cache-hint path restores the stashed input.

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Aug 13, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb31813cc9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

readonly origin: PromptOrigin;
readonly prompt?: string;
/** The prompt record id when the turn was opened by a prompt submission. */
readonly promptId?: string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include promptId in the protocol event schema

This adds promptId to the public TurnStartedEvent type, but the matching turnStartedEventSchema in this file still only accepts type, turnId, origin, and prompt. Because Zod object parsing strips unknown keys by default, any client that validates WS events through sessionEventMessageSchema/turnStartedEventSchema will drop the server's promptId, so prompt-to-turn correlation for client-chosen prompt IDs (for example staged media cleanup keyed off the echoed prompt id) stops working after validation.

Useful? React with 👍 / 👎.

7Sageer added 22 commits August 13, 2026 15:14
…nal projection

A projector that attached after turn.started built the terminal turn.upsert
with an empty header, and the whole-header replace downstream wiped the
backfilled origin / prompt / attachmentIds — only the debounced best-effort
heal could restore them. Fall back to the producer store's seeded header
(via a new optional ProjectorLookups.turn) when currentTurn misses, and
cover the mid-turn attach path with a service-level regression test.
…gent/media

The kimi-file:// daemon reference grammar, media path tags, and the tag/ref
fold are engine-internal conventions, not provider-wire contract; keep
src/kosong untouched. Root exports and SDK re-exports are unchanged.
Pasted and uploaded media now materialize under the session's own media/
dir instead of the shared cache, so the copies follow the session's
lifecycle: fork carries them along, session deletion cleans them up.

A new Session-scope ISessionMediaStore owns the dir: atomic tmp+rename
materialization with a unified extension policy, and canonical-vs-hint
display-path resolution. The persisted ?path= is a write-time snapshot —
readers prefer the session-canonical location, so fork and home relocation
never hand the model a dead path. Prompt intake normalizes every daemon
reference through the single enqueue funnel (REST edge, SDK prompt/steer,
gateway), serialized in arrival order to keep the FIFO across the async
file I/O. The kap-server edge materializes through the same store with a
shared-cache fallback, and the request-time resolver refreshes stale
persisted and memoized path tags; a claimed video reference degrades to
its tag alone instead of duplicating it.
… path

The record now joins the FIFO synchronously and its daemon-ref intake runs
as a per-record promise, awaited by the launch and steer paths before the
message is consumed — queue order, list/abort visibility, and prompt
submission latency no longer wait on file I/O, and a slow intake no longer
head-of-line blocks later prompts. The launching record is tracked so abort
and clear stay reachable inside the launch window; startNext re-checks
cancellation after every await (intake race, hook, turn admission), a
cancelled record is never re-queued, and a compaction requeue waits for
onDidFinishCompaction instead of busy-looping the scheduler.
…ring

pairMediaPathTagRefs now exposes claimingRefByTagIndex, and claimingRefIndex
reads it instead of recovering the claimer by path equality — which
mis-attributed a tag when two different fileIds carried the same path in an
interleaved sequence, breaking the pair and leaking the tag as user text.
Also covers the memoized-video-tag claimed-drop branch.
…g parity

The cold rebuild's user-slash branch now folds the turn-opening input like
any user turn (claimed tag out of the prompt text, one attachment entity),
matching the live projection. The ref extraction is consolidated into the
contract module (daemonFileRefFromPairingPart, the mirror of the engine's
daemonFileRefFromPart) and the mirror carries the new claimingRefByTagIndex
map. A new kap-server parity test imports both implementations and asserts
identical pairings over shared fixtures, so the engine/mirror pair can no
longer drift silently.
The global search indexer concatenated every text part of a persisted user
message, so the upload pair's <image path> tag made pure-image prompts
searchable and wrote the materialization path into the index — breaking the
module's documented pure-image invariant and diverging from the live route.
textOfContent now folds the pair like every other read model (with a
fold-safe coercion for malformed wire parts). Also pins the prompt-media
cache-dir fallback with a read-only session media dir test (skipped as root).
…oadFile rejection

foldMediaPathTagRefs and matchSingleMediaPathTag join the daemon
file-reference helper re-exports so hosts can fold the upload tag+ref pair
without importing agent-core-v2; the v1 harness's uploadFile not_implemented
rejection is pinned by a test.
…s atomic in steer input

Resumed-session replay rendered the upload pair raw — the <image path> tag
as user text and the kimi-file:// url as an XML-ish reference — and the
markdown export leaked the tag into both the turn body and the overview
topic. contentPartsToText and the exporter now fold the pair, and daemon
references render as a bare [image]/[video] placeholder. combineSteerInput
moves to tui/utils/steer-input and no longer merges a standalone media tag
into adjacent text, which would have broken the engine-side pairing for
steered image messages.
A queued pure-image message opens with a standalone `<media path>` tag,
which combineSteerInput keeps atomic. With the previous item ending in a
media part, the '\n\n' separator landed as a stranded whitespace-only text
part between the media part and the tag, normalizePromptInput rejected the
steer, and the already-cleared queue lost the messages. Treat a leading
standalone tag as media so the separator is dropped there.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aae53f0ff5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const cacheKey = `${ref.fileId}\0${providerKey}`;

const memoed = this.resolved.get(cacheKey);
if (memoed !== undefined) return this.memoedOutcome(ref, memoed);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-check video capability before using cached uploads

When a video is first resolved with a video-capable model and the same session later switches to a same-provider model whose capabilities have video_in: false, this memo hit returns the prior ms:// video_url before the uncached path's capability check runs. That makes subsequent history requests send a video part to a model that cannot accept video instead of degrading to the path tag, so conversations can start failing after a model switch; include the capability/model in the cache key or check video_in before honoring cached upload parts.

Useful? React with 👍 / 👎.

…oads

The video memo hit path returned a cached ms:// part before the current
model's capability check, so switching to a same-provider model with
video_in:false sent a video part the model cannot accept instead of
degrading to the path tag. Gate on capability first, mirroring the image
strategy.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1e2db64b9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/kimi-code/src/tui/kimi-tui.ts Outdated
if (this.state.queuedMessages.length === 0) return undefined;
const last = this.state.queuedMessages.at(-1)!;
this.state.queuedMessages = this.state.queuedMessages.slice(0, -1);
this.staging.releaseQueued([last]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep recalled queued media staged

When the user recalls a queued media prompt with Up-arrow while a turn is still running, this releases the queued item's staged files even though the item is not being discarded—it is moved back into the editor. For queued videos, releaseQueued deletes the cached stagingPaths, and the restored editor text only contains the placeholder; if the original video source path has already disappeared, the next submit cannot recreate the cache copy and the media that was safely queued is lost. Transfer ownership to the editor instead of releasing here, and only release if the restored draft is actually abandoned.

Useful? React with 👍 / 👎.

- error-codes 40923 编号冲突:保留 main 的 SESSION_TITLE_UNAVAILABLE,
  PROMPT_ID_CONFLICT 顺延为 40924(同步 kap-server / protocol /
  klient dispatcher 及相关测试断言)
- prompts.test.ts / sdk-rpc-client-v2.test.ts 合并两侧新增测试
- promptMetadataText.test.ts 合并两侧头部注释
…g it

Recalling a queued media prompt into the editor is not a discard, but
the recall path released the staged files: image attachments lost their
daemon upload (resubmit silently downgraded to inline), and a recalled
video's cache copy was deleted even though re-materialization needs a
source that may already be gone. Recall now consumes only the retain
(the next submit re-retains), retires the cache copy to session
lifetime, and rebases the video attachment onto that copy.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

this.eventBus.publish({ type: 'prompt.queued', promptId: record.id, content: record.message.content, queueLength: this.pending.length });

P3 Badge Count launching media prompts in queued events

When an idle submission contains a kimi-file:// media part, enqueueAccepted() calls startNext() before publishing prompt.queued; startNext() synchronously shifts the record out of pending into launchingItem, so this event reports queueLength: 0 even though the prompt is accepted, abortable, and list() now exposes it as queued. Consumers of the prompt.queued event/external hook will see an impossible empty queue for that media prompt; compute the count from the same snapshot used by list() or publish before shifting.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

…eueLength

startNext shifts the record into launchingItem before publishQueued
computes the count, so a media prompt's prompt.queued reported
queueLength 0 even though the prompt is accepted, abortable, and listed
as queued. Compute the count from the same snapshot list() exposes.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c78cbf88c1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const stagingPaths = input.flatMap((item) => item.stagingPaths ?? []);
const stagingLease = this.staging.create(imageAttachmentIds, stagingPaths, 'user');
const currentTurnId = this.streamingUI.getTurnContext().turnId;
if (currentTurnId !== undefined) this.staging.bindToTurn(stagingLease, currentTurnId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind staged steer media after the consuming turn is known

When a Ctrl-S/user steer with staged media races the current turn ending, submitSteer can fall back to launching a new turn when no active prompt remains. This pre-binds the lease to the old UI turn before session.steer(...) resolves, so the new turn's turn.started cannot claim it; the old turn end can delete the daemon upload before intake, or if that turn already ended the lease is never released. Wait for the steer result/next turn.started before binding, or rebind when the steer launches a new turn.

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: cb345bec94

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Intake keeps the upload-backed reference when the canonical write fails
instead of double-writing into an unowned global cache scope; the session
media store's reads collapse to the canonical scope, and non-filesystem
deployments no longer write every media blob twice.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f2b575d814

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

};
return { meta: wire.meta, data: Buffer.from(wire.data, 'base64') };
},
delete: (fileId) => call('fileService', 'delete', [fileId]) as Promise<void>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize file deletion errors before exposing delete

When callers delete a stale or expired upload through the new global.files.delete facade, this goes through the generic dispatcher path even though the file-service special cases only normalize save and get. In the memory transport that rejects with the raw engine Error2, while the IPC host serializes the same non-RPCError as 50001, so callers cannot reliably detect file-not-found and the two transports diverge; add the same rethrowFileErrorAsRpc handling for fileService.delete.

AGENTS.md reference: packages/klient/AGENTS.md:L21-L25

Useful? React with 👍 / 👎.

@7Sageer 7Sageer closed this Aug 14, 2026
@7Sageer 7Sageer reopened this Aug 14, 2026
…mon file references

The kimi-file:// reference persisted in context memory bundled a durable
identity (fileId) with a perishable machine-local absolute path (?path=),
which forked sessions and home relocations would stale. The reference now
carries only the file id; the display path is derived from the session
media store by file id at read time. Parsers tolerate and strip the legacy
?path= query so old records keep resolving.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0ecda4695

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


private async findKey(fileId: string): Promise<string | undefined> {
const keys = await this.storage.list(this.scope, fileId);
return keys.find((key) => key === fileId || key.startsWith(`${fileId}.`));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore atomic temp files when resolving session media

When a media upload is still being materialized on the filesystem backend, atomicWriteStream creates a sibling temp file named like <fileId>.<ext>.tmp.<pid>.<hex>. This fallback matches that temp name because it accepts any key starting with ${fileId}., so an immediate /sessions/{id}/media/{file_id} fetch (or any display-path lookup before metadata is written) can expose or stream the in-progress temp file instead of waiting for the canonical <fileId>.<ext> copy. Filter out the atomic temp suffix or resolve through validated metadata/canonical extensions only.

Useful? React with 👍 / 👎.

…by-id resolution

The fs backend stages atomic writes at <key>.tmp.<pid>.<hex> next to the
target key, and the media store's prefix-listing predicate matched them, so
a lookup racing an unfinished materialize could return the partial copy as
the canonical file.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3fb96fb3ef

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const stagingLease = this.staging.create(imageAttachmentIds, stagingPaths, 'user');
const currentTurnId = this.streamingUI.getTurnContext().turnId;
if (currentTurnId !== undefined) this.staging.bindToTurn(stagingLease, currentTurnId);
this.staging.trackDispatch(stagingLease, session.steer(combineSteerInput(input)), (error) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh queued media refs before Ctrl-S steer

When Ctrl-S steers queued media after the paste-time daemon upload is near/over its TTL, this path sends the queued parts directly through combineSteerInput; unlike sendQueuedMessage, it never calls refreshExpiringImageFileRefs, so a queued image can still contain an expired kimi-file:// URL even though the attachment bytes are available for inline fallback. Refresh each item's parts before combining them for session.steer.

Useful? React with 👍 / 👎.

Comment on lines +301 to +304
void Promise.race([handle.launched, handle.completion]).then(
() => staging?.discard(),
() => staging?.discard(),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Keep staging uploads until resolver fallback is safe

When an uploaded image is compressed, the prompt content references the new owned daemon upload, but prompt intake treats session-media materialization as best effort. If that canonical write is skipped or fails, handle.launched can still resolve before the first LLM request reads media; this cleanup then deletes the only daemon copy, so the resolver has neither the upload nor a session copy and emits an omitted-media placeholder. Keep the owned upload until turn end, or only discard it after materialization is known to have succeeded.

Useful? React with 👍 / 👎.

…spatch

Create the staging lease right after extraction so every pre-dispatch exit
releases through the tracker: validation/session failures release it,
queueing defers it to the queue item's raw ids/paths, and the cache-hint
stash takes over ownership. A forgotten exit now degrades to an unclaimed
lease swept at session close instead of a permanently retained upload.

The cache-hint restore exits (dismiss, chained restore, session switch
during fetch, failed compact/new-session) previously returned only the
text to the editor, leaking the extraction's retains and staged cache
copies. They now go through queue-recall semantics: retains are consumed,
staged copies retire, and recalled videos rebase onto them.
…udgeted LRU

A memoized inline image part pins a multi-MB base64 string, and the agent
state registry's snapshot/inspect path serializes every registered state
in full — so the memo no longer lives in agentState. It is now a private
per-file-id LRU with the existing 8MB per-entry cap plus a 64MB total
budget; eviction simply re-reads the bytes on the next request. The video
memo stays in agentState.
…route

Prompt intake materializes bytes into the session media store
asynchronously and best-effort, but a session_media ref is projected to
clients as soon as the prompt is queued — so the download route could 404
during the intake window, and forever after an intake failure. The route
now reads the canonical session store first and falls back to the App-scope
staged upload, adapting it to the same served shape; only a double miss is
a 404. The header note also records that resolving the store resumes cold
sessions, an accepted short-term semantic with a TODO for a cold-read
channel.
@7Hanrui

7Hanrui commented Aug 14, 2026

Copy link
Copy Markdown

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 533df902e4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +190 to +193
const fileId = attachment?.kind === 'image' ? attachment.fileId : undefined;
this.byId.delete(id);
this.stagingUses.delete(id);
return fileId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve retained uploads while trimming thumbnails

When a user reuses a pasted image from an older transcript turn and the new submit pushes that older turn out of the TUI window, trimTranscriptWindow() calls removeMany() while the new extraction has already retained the same attachment id. This path ignores the retain count, removes the attachment, and returns its daemon fileId for deletion, so the prompt still contains a kimi-file://... part but the upload can be deleted before intake; refreshExpiringImageFileRefs() also cannot inline it because the attachment was removed. Keep retained attachments/uploads alive, or drop only thumbnail bytes until stagingUses reaches zero.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants