Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
23424e2
fix(translate): hoist leading system prefix to top-level, keep non-le…
yyyr-p Jun 26, 2026
7190138
fix(translate): preserve part boundaries when hoisting system; reject…
Menci Jun 28, 2026
217053e
test(translate): cover applyLastSystemCacheBreakpoint + leading empty…
Menci Jun 28, 2026
5e65cb2
chore(translate): trim redundant comments on system hoist + cache helper
Menci Jun 28, 2026
3f07c60
chore(translate): drop redundant dev/system normalization comment line
Menci Jun 28, 2026
43dc268
chore(translate): further trim hoist + inline-branch comment narration
Menci Jun 28, 2026
1109014
chore(translate): trim hoist comment + tighten test name
Menci Jun 28, 2026
c877294
refactor(translate): unify system-content helpers to always return Me…
Menci Jun 28, 2026
3574d8a
refactor(translate): preserve system block boundaries on Messages→Cha…
Menci Jun 28, 2026
35f712f
fix(translate): exhaustiveness guard for unknown Responses system con…
Menci Jun 28, 2026
5b631a9
chore(translate): drop stale ResponsesInputText cast + cover empty-sy…
Menci Jun 28, 2026
f9ebb7f
feat(provider-cursor): Cursor provider with multi-turn native MCP too…
yyyr-p Jun 30, 2026
eafd7f8
feat(provider-cursor): cross-instance session reuse via D1 + DurableH…
yyyr-p Jun 30, 2026
7fb4fe6
fix(provider-cursor): fix cross-instance resume frame loss + Phase 4 …
yyyr-p Jun 30, 2026
1f09b8a
fix(platform-cloudflare): credit-based DurableHttpSessionDO body channel
yyyr-p Jul 1, 2026
b42ee58
feat(provider-cursor): record TTFT as upstream_success
yyyr-p Jul 1, 2026
bb8b493
feat(provider-cursor): fold tool-call history into cold-open transcript
yyyr-p Jul 1, 2026
3672d81
feat(provider-cursor): add privacyMode to CursorUpstreamConfig schema
yyyr-p Jul 1, 2026
c714462
feat(provider-cursor): thread privacyMode config through to transport…
yyyr-p Jul 1, 2026
f5db14f
feat(provider-cursor): encode conversation_state root_prompt blob ref
yyyr-p Jul 1, 2026
785899d
feat(provider-cursor): seed root-prompt blob and reference it from ru…
yyyr-p Jul 1, 2026
656d5fc
feat(provider-cursor): route system prompt to native root_prompt blob…
yyyr-p Jul 1, 2026
029805b
test(provider-cursor): cover privacyMode header/config + root_prompt …
yyyr-p Jul 1, 2026
5bede6a
feat(gateway): allow cursor privacyMode config PATCH (reject only cre…
yyyr-p Jul 1, 2026
6bcdbf5
test(gateway): cursor privacyMode PATCH accepted, credential edits st…
yyyr-p Jul 1, 2026
f13cef8
feat(web): cursor privacy-mode toggle in config panel, persisted via …
yyyr-p Jul 1, 2026
f5c7377
fix(provider-cursor): wrap get_blob_result as GetBlobResult{blob_data=1}
yyyr-p Jul 1, 2026
be0541a
revert(provider-cursor): drop native root_prompt system-prompt injection
yyyr-p Jul 1, 2026
99062f4
feat(cursor): account-level usage sync with proportional per-key attr…
yyyr-p Jul 1, 2026
4c1332c
feat(cursor): real context window from AvailableModels tooltip prose
yyyr-p Jul 1, 2026
21148dd
feat(cursor): collapse variants to base models + Max Mode toggle
yyyr-p Jul 1, 2026
2884b8f
feat(cursor): model display names + modalities + reasoning metadata
yyyr-p Jul 1, 2026
934526b
fix(cursor): drop image modality metadata
yyyr-p Jul 1, 2026
1c041c0
feat(cursor): route reasoning_effort/thinking to the wire variant
yyyr-p Jul 1, 2026
bcade79
feat(cursor): image input via inline SelectedImage bytes
yyyr-p Jul 1, 2026
a5f845f
feat(cursor): advertise image input on every model
yyyr-p Jul 2, 2026
aa4f494
refactor(cursor): use clientDisplayName for model display names
yyyr-p Jul 2, 2026
79ed7a2
feat(cursor): StreamCpp (Tab) transport + proto (Phase 1)
yyyr-p Jul 2, 2026
e37c0d4
feat(cursor): Tab completions bridge — FIM/insertion path (Phase 2)
yyyr-p Jul 2, 2026
36ae8bc
feat(cursor): Zeta 2.1 (V0318) marker path for Tab completions
yyyr-p Jul 2, 2026
31095e6
feat(cursor): Zeta V0615 hashed-regions Tab path (custom clients)
yyyr-p Jul 2, 2026
a297b7e
feat(web): Cursor Tab completion toggle in CursorConfigPanel
yyyr-p Jul 2, 2026
afdb2de
fix(cursor): preserve trailing newline in Tab edit-region splice
yyyr-p Jul 2, 2026
8cbd238
docs(cursor): confirm range_to_replace is a 2-field LineRange
yyyr-p Jul 2, 2026
0245408
fix(cursor): balance newlines when splicing Tab edits (no swallowed/d…
yyyr-p Jul 2, 2026
485c25a
fix(cursor): range-bounded Tab splice + newline-insensitive cursor
yyyr-p Jul 2, 2026
11692fb
feat(cursor): real per-request token usage from the RunSSE stream
yyyr-p Jul 2, 2026
10132df
fix(cursor): only account a turn when its checkpoint arrives
yyyr-p Jul 2, 2026
286b179
refactor(cursor): drop the hourly account-level usage sync
yyyr-p Jul 2, 2026
7af6c61
feat(cursor): notional cost from Cursor's own published per-token rates
yyyr-p Jul 2, 2026
02b59ae
feat(cursor): cache observed context windows, prefer over tooltip heu…
yyyr-p Jul 2, 2026
33aaa37
feat(cursor): count discarded KV-blob mirrors on streamed-text turns
yyyr-p Jul 2, 2026
5a8fec6
refactor(cursor): KV-blob guard warns only when blob exceeds the stream
yyyr-p Jul 2, 2026
2682af9
merge(cursor): fold in privacyMode / ghost-mode config
yyyr-p Jul 2, 2026
8a1c013
merge(main): fold upstream refactors into cursor-provider
yyyr-p Jul 3, 2026
9ca5080
feat(cursor): fetch subscription usage from the cursor.com dashboard …
yyyr-p Jul 3, 2026
509b8c0
feat(cursor): control-plane GET /upstreams/:id/cursor/quota route
yyyr-p Jul 3, 2026
cf57d6b
feat(web): CursorDashboardUsage type mirroring provider-cursor
yyyr-p Jul 3, 2026
21d5385
fix(cursor): return 502 for session-expired, not 401
yyyr-p Jul 3, 2026
3ee1764
feat(web): CursorAccountCard renders subscription usage
yyyr-p Jul 3, 2026
ce49893
feat(web): wire Cursor dashboard usage into upstream-edit page
yyyr-p Jul 3, 2026
a1a399e
chore(cursor): apply eslint --fix cleanups (import order, optional ch…
yyyr-p Jul 3, 2026
a47f624
chore(vitest): include provider-cursor in the root test project list
yyyr-p Jul 3, 2026
bf2aae2
feat(cursor): estimate token usage for Cursor Tab (StreamCpp) complet…
yyyr-p Jul 3, 2026
cda90d4
merge(cursor): fold in dashboard usage display
yyyr-p Jul 3, 2026
432ea4a
refactor(web): regroup CursorConfigPanel around three semantic sections
yyyr-p Jul 3, 2026
b316b3a
docs(web): note Cursor Tab token usage is estimated
yyyr-p Jul 3, 2026
c8c81bb
docs(web): correct Cursor Privacy mode description
yyyr-p Jul 4, 2026
074eca2
chore(cursor): denoise cursor-provider session diff
yyyr-p Jul 4, 2026
93018b4
chore(cursor): branch-wide behavior-preserving cleanup pass
yyyr-p Jul 4, 2026
59a6e66
chore(cursor): drop dead scaffolding and duplicate declarations
yyyr-p Jul 4, 2026
8e9473f
chore(cursor): round-3 cleanup cascade — orphan proto helpers
yyyr-p Jul 4, 2026
162b0d8
perf(cursor): hoist TextEncoder/TextDecoder + proto slice→subarray + …
yyyr-p Jul 4, 2026
49b78f4
refactor(cursor): dedup exec builders/parsers, synthetic errors, tool…
yyyr-p Jul 4, 2026
b4c1271
refactor(cursor): unify Tab-splice helpers + drop message-list clone
yyyr-p Jul 4, 2026
7952361
perf: fuse buffer concat/tail-slice + head-index DO chunk queues
yyyr-p Jul 4, 2026
29803ef
docs: trim cursor-specific reveals from shared contract/impl comments
yyyr-p Jul 4, 2026
7654bb0
style(cursor): eslint import-order autofix over the simplify diff
yyyr-p Jul 4, 2026
afeec52
merge(cursor): fold in simplify/cursor-provider
yyyr-p Jul 4, 2026
439b07c
fix(cursor): apply cursor-scoped review fixes
yyyr-p Jul 4, 2026
77b74a9
fix(cursor): restore CURSOR_BIDI_APPEND_PATH + CURSOR_GRPC_WEB_CONTEN…
yyyr-p Jul 4, 2026
37b7ac2
fix(cursor): correct parseToolCall LF-guard length check
yyyr-p Jul 4, 2026
218911d
fix(cursor): migration 0050 strips legacy quotaSnapshot from state_json
yyyr-p Jul 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/platform-cloudflare/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
// exported name on the Worker module. The wrangler `migrations.new_sqlite_classes`
// entry must match this export.
export { BroadcastDO } from './src/broadcast-do.ts';
export { DurableHttpSessionDO } from './src/durable-http-session-do.ts';

initBackgroundSchedulerResolver(c => promise => c.executionCtx.waitUntil(promise));

Expand Down
1 change: 1 addition & 0 deletions apps/platform-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@floway-dev/gateway": "workspace:*",
"@floway-dev/http": "workspace:*",
"@floway-dev/platform": "workspace:*",
"@floway-dev/proxy": "workspace:*",
"@floway-dev/protocols": "workspace:*",
"hono": "^4"
},
Expand Down
26 changes: 26 additions & 0 deletions apps/platform-cloudflare/src/_held-socket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Internal helper (underscore prefix = not a platform export): adapts a
// DialedSocket into the DuplexStream shape `@floway-dev/http` consumes, plus
// an idempotent close.
//
// Why this exists as its own ~tiny class: the DurableHttpSessionDO is the
// first piece of code in the workspace that OWNS a live outbound socket across
// inbound requests. If a future need arises for a raw persistent TCP duplex
// (not HTTP), HeldSocket is the seam to promote into a platform `DurableSocketDial`
// contract + a `DurableSocketDO` — a mechanical refactor rather than a rewrite.

import type { DuplexStream } from '@floway-dev/http';
import type { DialedSocket } from '@floway-dev/platform';

export class HeldSocket {
constructor(private readonly dialed: DialedSocket) {}

/** The duplex view `fetchOnStream` reads/writes. */
asDuplex(): DuplexStream {
return { readable: this.dialed.readable, writable: this.dialed.writable };
}

/** Idempotent — safe to call on an already-closed/errored socket. */
close(): Promise<void> {
return this.dialed.close();
}
}
13 changes: 9 additions & 4 deletions apps/platform-cloudflare/src/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DurableObjectChannelBroker, type BroadcastNamespace } from './do-channel-broker.ts';
import { DurableObjectDurableHttpSession, type DurableHttpSessionNamespace } from './do-durable-http-session.ts';
import { createCloudflareImageProcessor, type ImagesBinding } from './image-processor.ts';
import { KvImageCache, type KvNamespace } from './kv-image-cache.ts';
import { R2FileProvider, type R2BucketLike } from './r2-file-provider.ts';
Expand All @@ -10,6 +11,7 @@ import type { DumpMetadata } from '@floway-dev/gateway/dump-types';
import { addTrustedRootCAs } from '@floway-dev/http';
import {
IMAGE_CACHE_POLICY,
initDurableHttpSession,
initEnv,
initFileProvider,
initImageCacheStore,
Expand All @@ -25,15 +27,17 @@ export interface CloudflareEnv {
IMAGES: ImagesBinding;
KV: KvNamespace;
BROADCAST_DO: BroadcastNamespace;
DURABLE_HTTP_SESSION: DurableHttpSessionNamespace;
[key: string]: unknown;
}

// Every binding declared on `CloudflareEnv` is load-bearing — D1 holds all
// config and telemetry, R2 holds spilled payloads, Images compresses inline
// images, KV memoises compressed image results. A missing binding means
// wrangler.jsonc drifted from the code, so we refuse to initialise rather
// than 503 on first use of the absent binding.
const REQUIRED_BINDINGS = ['DB', 'FILES', 'IMAGES', 'KV', 'BROADCAST_DO'] as const;
// images, KV memoises compressed image results, BROADCAST_DO fans out dump
// events, DURABLE_HTTP_SESSION holds cross-request upstream response streams
// (cursor RunSSE). A missing binding means wrangler.jsonc drifted from the
// code, so we refuse to initialise rather than 503 on first use.
const REQUIRED_BINDINGS = ['DB', 'FILES', 'IMAGES', 'KV', 'BROADCAST_DO', 'DURABLE_HTTP_SESSION'] as const;

export const bootstrapCloudflarePlatform = (env: CloudflareEnv): { db: SqlDatabase } => {
const missing = REQUIRED_BINDINGS.filter(name => env[name] === undefined);
Expand All @@ -58,5 +62,6 @@ export const bootstrapCloudflarePlatform = (env: CloudflareEnv): { db: SqlDataba
addTrustedRootCAs(cloudflareRuntimeRootCAs);
initDumpStore(new FileDumpStore(env.DB, files));
initDumpBroker(new DurableObjectChannelBroker<DumpMetadata>(env.BROADCAST_DO, dumpCodec));
initDurableHttpSession(new DurableObjectDurableHttpSession(env.DURABLE_HTTP_SESSION));
return { db: env.DB };
};
6 changes: 6 additions & 0 deletions apps/platform-cloudflare/src/broadcast-do_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ class FakeState {
push(ws: FakeWebSocket): void {
this.sockets.push(ws);
}
// BroadcastDO never touches storage; present only to satisfy the shared
// DurableObjectState type (DurableHttpSessionDO uses setAlarm).
storage = {
async setAlarm(): Promise<void> {},
async deleteAlarm(): Promise<void> {},
};
}

test('BroadcastDO extends DurableObject so the runtime gates RPC dispatch on it', () => {
Expand Down
9 changes: 7 additions & 2 deletions apps/platform-cloudflare/src/cloudflare-workers.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ declare module 'cloudflare:workers' {
}
}

// The runtime's `DurableObjectState` surface the actor touches — just the
// WebSocket Hibernation entry points.
// The runtime's `DurableObjectState` surface the actor touches — the
// WebSocket Hibernation entry points and the alarm scheduler (used by
// DurableHttpSessionDO for idle eviction).
interface DurableObjectState {
acceptWebSocket(server: WebSocket): void;
getWebSockets(): WebSocket[];
storage: {
setAlarm(scheduledTime: number): Promise<void>;
deleteAlarm(): Promise<void>;
};
}
173 changes: 173 additions & 0 deletions apps/platform-cloudflare/src/do-durable-http-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import type {
DurableHttpSessionStartInit,
DurableHttpSessionStartResult,
} from './durable-http-session-do.ts';
import type {
DurableHttpSession,
DurableHttpSessionAcquireOptions,
DurableHttpSessionHandle,
DurableHttpSessionInit,
} from '@floway-dev/platform';

// CF implementation of the DurableHttpSession contract, mirroring the
// DurableObjectChannelBroker shape: resolve a per-sessionKey DO stub, then
// drive it over RPC + a WebSocket body channel. The DO holds the live outbound
// RunSSE response; this broker exposes it as the contract's ReadableStream.

const DEFAULT_IDLE_TIMEOUT_MS = 5 * 60 * 1000;

// One credit = "send me the next chunk". The broker sends exactly one per
// ReadableStream pull; the DO treats any inbound message as a credit and ignores
// the content, so a 1-byte sentinel is enough.
const CREDIT_BYTE = new Uint8Array([1]);

// Minimal namespace/stub surface — declared locally so this file stays off
// `@cloudflare/workers-types` (same convention as do-channel-broker.ts).
export interface DurableHttpSessionNamespace {
idFromName(name: string): unknown;
get(id: unknown): DurableHttpSessionStub;
}

interface DurableHttpSessionStub {
queryOrStart(
init: DurableHttpSessionStartInit | null,
idleTimeoutMs: number,
): Promise<DurableHttpSessionStartResult | null>;
release(): Promise<void>;
discard(reason: string): Promise<void>;
fetch(request: Request): Promise<Response>;
}

export class DurableObjectDurableHttpSession implements DurableHttpSession {
constructor(private readonly namespace: DurableHttpSessionNamespace) {}

private stub(sessionKey: string): DurableHttpSessionStub {
return this.namespace.get(this.namespace.idFromName(sessionKey));
}

async acquire(
sessionKey: string,
init: DurableHttpSessionInit | null,
opts?: DurableHttpSessionAcquireOptions,
): Promise<DurableHttpSessionHandle | null> {
const stub = this.stub(sessionKey);
const idleTimeoutMs = opts?.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;

const meta = await stub.queryOrStart(init, idleTimeoutMs);
if (!meta) return null; // miss + init=null → caller degrades

const wsResponse = await stub.fetch(new Request('https://durable-http.do/body', {
headers: { Upgrade: 'websocket' },
}));
if (wsResponse.status !== 101) {
throw new Error(`DurableHttpSessionDO body upgrade returned HTTP ${wsResponse.status} instead of 101`);
}
const socket = (wsResponse as Response & { webSocket?: WebSocket }).webSocket;
if (!socket) throw new Error('DurableHttpSessionDO returned 101 without a webSocket');
socket.accept();

const body = wsToByteStream(socket, opts?.signal);

return {
status: meta.status,
headers: new Headers(meta.headers),
body,
async release(): Promise<void> {
try { socket.close(1000, 'released'); } catch { /* already closed */ }
await stub.release();
},
async discard(reason: string): Promise<void> {
try { socket.close(1000, 'discarded'); } catch { /* already closed */ }
await stub.discard(reason);
},
};
}
}

// Wrap the body WebSocket as a ReadableStream<Uint8Array>. Binary frames are
// the body bytes; a close ends the stream; an error rejects the pending read.
// Aborting the caller signal closes the socket and ends the stream — it does
// NOT discard the DO session (sibling acquires keep working), matching the
// contract's signal semantics.
const wsToByteStream = (
socket: WebSocket,
signal: AbortSignal | undefined,
): ReadableStream<Uint8Array> => {
// Head-index cursor over a push-only array so a burst that queues dozens of
// chunks before the consumer resumes doesn't pay O(n) per delivered chunk.
const queue: Uint8Array[] = [];
let queueHead = 0;
let pull: ((v: { value?: Uint8Array; done: boolean }) => void) | null = null;
let pendingError: unknown = null;
let closed = false;

const deliver = (chunk: Uint8Array | null): void => {
if (pull) {
const r = pull;
pull = null;
r(chunk ? { value: chunk, done: false } : { done: true });
} else if (chunk) {
queue.push(chunk);
}
};

const end = (): void => {
if (closed) return;
closed = true;
deliver(null);
try { socket.close(1000, 'consumer done'); } catch { /* already closed */ }
};

socket.addEventListener('message', event => {
if (closed) return;
const raw = (event as MessageEvent).data as string | ArrayBuffer | Uint8Array;
let bytes: Uint8Array;
if (typeof raw === 'string') bytes = new TextEncoder().encode(raw);
else if (raw instanceof Uint8Array) bytes = raw;
else bytes = new Uint8Array(raw);
deliver(bytes);
});
socket.addEventListener('close', () => { closed = true; deliver(null); });
socket.addEventListener('error', () => {
if (pendingError === null) pendingError = new Error('DurableHttpSession body socket error');
closed = true;
deliver(null);
});

if (signal) {
if (signal.aborted) end();
else signal.addEventListener('abort', end, { once: true });
}

return new ReadableStream<Uint8Array>({
async pull(controller): Promise<void> {
if (queueHead < queue.length) {
controller.enqueue(queue[queueHead]!);
queueHead += 1;
if (queueHead >= queue.length) {
queue.length = 0;
queueHead = 0;
}
return;
}
if (closed) {
if (pendingError) { controller.error(pendingError); return; }
controller.close();
return;
}
// Credit-based backpressure: request exactly one chunk, then wait for it.
// With highWaterMark 0 the stream only pulls on an active read, so a
// consumer that pauses (transport parked at exec_mcp) sends no credit and
// the DO holds subsequent chunks in its buffer for the next acquire.
try { socket.send(CREDIT_BYTE); } catch { controller.close(); return; }
const result = await new Promise<{ value?: Uint8Array; done: boolean }>(resolve => { pull = resolve; });
if (result.done) {
if (pendingError) controller.error(pendingError);
else controller.close();
return;
}
if (result.value) controller.enqueue(result.value);
},
cancel(): void { end(); },
}, { highWaterMark: 0 });
};
Loading