Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/upgrade-dsh-rc1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@orbisapp/remote-dsh": minor
---

Support DeepSeek Harness 0.1.5-rc.1, including its new session format and streaming API. Preserve streamed responses across retries and reconnects, restore system messages from session history, and update event replay for the new format.

Requires DeepSeek Harness 0.1.5-rc.1 or newer within the supported 0.1 release line. Earlier DSH versions are no longer supported by this plugin release.
2 changes: 2 additions & 0 deletions packages/orbis-agent-backend/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ export interface AgentSessionStateChangedEvent extends AgentTransientEventBase {

export interface AgentEntryDeltaEvent extends AgentTransientEventBase {
readonly payload: {
/** This delta closes its content block; an empty delta can carry completion alone. */
readonly blockComplete?: true;
readonly blockIndex: number;
readonly chunkSeq: number;
readonly delta: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ function toolInputDeltaEvent(): TransportEvent {
const occurredAt = agentTimestamp("2026-08-11T00:00:04.000Z");
const event = {
blockIndex: 0,
blockComplete: true,
channel: "transient",
chunkSeq: 1,
delta: '{"path":"/workspace/demo.ts"}',
Expand Down Expand Up @@ -466,6 +467,7 @@ test("v2 connection decodes tool state and tool input events", async () => {
},
{
event: {
blockComplete: true,
entryId: "tool-call-a",
part: "tool_input",
type: "entry.delta",
Expand Down
2 changes: 2 additions & 0 deletions packages/orbis-remote-agent-protocol/src/v2-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ function parseStreaming(value: unknown): NonNullable<RemoteAgentV2Overlay["strea
entryId: agentEntryId(input.entryId),
blocks: input.blocks.map((block) => ({
blockIndex: block.blockIndex,
...(block.blockComplete === undefined ? {} : { blockComplete: block.blockComplete }),
content: parseStreamingContent(block.content),
})),
chunkSeq: input.chunkSeq,
Expand Down Expand Up @@ -687,6 +688,7 @@ function parseEvent(value: unknown): RemoteAgentV2SessionEvent | RemoteAgentV2De
part: parsed.part,
blockIndex: parsed.blockIndex,
chunkSeq: parsed.chunkSeq,
...(parsed.blockComplete === undefined ? {} : { blockComplete: parsed.blockComplete }),
delta: parsed.delta,
};
}
Expand Down
108 changes: 106 additions & 2 deletions packages/orbis-remote-agent-protocol/src/v2-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,95 @@ test("v2 host replays native entries from its cursor index without ACK state", a
}
});

test("v2 host preserves appended metadata when a state refresh reads ahead", async () => {
const runtimeListeners = new Set<(event: RemoteAgentV2SessionEvent) => void>();
const appended = entry("entry-race");
let current = snapshot([]);
const backendBase = presenceBackend(runtimeListeners);
const backend: RemoteAgentV2Backend = {
...backendBase,
readSession: async () => current,
};
const delivered: RemoteAgentV2SessionEvent[] = [];
const host = new OrbisRemoteAgentV2Host({
backend,
backendId: "remote:host-a",
store: new MemoryStore(),
transport: {
send: async (_target, frame) => {
const event = sessionEventFromTransport(frame);
if (event !== undefined) delivered.push(event);
},
},
});
try {
await host.handleRequest(
ORBIS_REMOTE_AGENT_V2_METHODS.hello,
{ device: { name: "Test", platform: "node" }, supportedVersions: [2] },
context(),
);
await host.handleRequest(
ORBIS_REMOTE_AGENT_V2_METHODS.sessionsSync,
params({ mode: "live", ref: publicRef }),
context(),
);

const source = {
backendId: nativeRef.backendId,
driverId: nativeRef.driverId,
nativeType: "agent/assistant-stream",
version: "0.1.5-rc.1",
};
const appendedEvent: RemoteAgentV2SessionEvent = {
channel: "replayable",
cursor: agentDeliveryCursor(0),
entry: appended,
eventId: agentEventId("native-entry-race"),
occurredAt: agentTimestamp("2026-08-11T00:00:01.000Z"),
sessionId: nativeRef.sessionId,
settlesEntryId: agentEntryId("stream-placeholder"),
source,
type: "entry.appended",
};
const stateEvent: RemoteAgentV2SessionEvent = {
channel: "state",
eventId: agentEventId("native-state-race"),
occurredAt: agentTimestamp("2026-08-11T00:00:01.000Z"),
patch: { runState: "running" },
revision: 1,
sessionId: nativeRef.sessionId,
source,
type: "session.state.changed",
};

// The snapshot is already ahead when the runtime emits the state hint.
// Emit both notifications synchronously so the state refresh queues before
// the append callback can reconcile the entry.
current = snapshot([appended], 1, { runState: "running" });
for (const listener of runtimeListeners) {
listener(stateEvent);
listener(appendedEvent);
}
await new Promise((resolve) => setTimeout(resolve, 0));

expect(delivered.filter((event) => event.type === "entry.appended")).toHaveLength(1);
const deliveredAppend = delivered.find((event) => event.type === "entry.appended");
expect(deliveredAppend).toMatchObject({
eventId: appendedEvent.eventId,
occurredAt: appendedEvent.occurredAt,
settlesEntryId: appendedEvent.settlesEntryId,
source: {
backendId: publicRef.backendId,
driverId: publicRef.driverId,
nativeType: source.nativeType,
version: source.version,
},
});
} finally {
await host.close();
}
});

test("v2 sync continuation with no pending work is empty, and a stale afterEntryId falls back to baseline", async () => {
const store = new MemoryStore();
const runtimeListeners = new Set<(event: RemoteAgentV2SessionEvent) => void>();
Expand Down Expand Up @@ -1156,11 +1245,16 @@ test("v2 host does not replay transient backlog to a peer joining live sync", as
},
},
});
const emit = (chunkSeq: number, sessionId = nativeRef.sessionId): void => {
const emit = (
chunkSeq: number,
sessionId = nativeRef.sessionId,
blockComplete = false,
): void => {
const event: RemoteAgentV2SessionEvent = {
blockIndex: 0,
channel: "transient",
chunkSeq,
...(blockComplete ? { blockComplete: true as const } : {}),
delta: String(chunkSeq),
entryId: streamEntryId,
eventId: agentEventId(`delta-${chunkSeq}`),
Expand Down Expand Up @@ -1199,6 +1293,7 @@ test("v2 host does not replay transient backlog to a peer joining live sync", as
blocks: [
{
blockIndex: 0,
blockComplete: true as const,
content: { text: String(preSyncChunkCount), type: "text" as const },
},
],
Expand Down Expand Up @@ -1235,6 +1330,7 @@ test("v2 host does not replay transient backlog to a peer joining live sync", as
blocks: [
{
blockIndex: 0,
blockComplete: true as const,
content: { text: String(queuedAfterSyncChunkSeq), type: "text" as const },
},
],
Expand Down Expand Up @@ -1276,7 +1372,7 @@ test("v2 host does not replay transient backlog to a peer joining live sync", as
});
expect(readSessionCalls).toBe(1);

emit(postSyncChunkSeq);
emit(postSyncChunkSeq, nativeRef.sessionId, true);
await Promise.all([ownerPostSyncDelivered, postSyncDelivered]);

const eventsFor = (transportId: string) =>
Expand All @@ -1286,6 +1382,14 @@ test("v2 host does not replay transient backlog to a peer joining live sync", as
expect(eventsFor(peer.transportId)).toEqual([1, 2, 3, 4, 5]);
expect(eventsFor(peerB.transportId)).toEqual([]);
expect(eventsFor(peerBReconnect.transportId)).toEqual([postSyncChunkSeq]);
expect(
delivered.find(
({ transportId, event }) =>
transportId === peerBReconnect.transportId &&
event.type === "entry.delta" &&
event.chunkSeq === postSyncChunkSeq,
)?.event,
).toMatchObject({ blockComplete: true });
expect(readSessionCalls).toBe(1);
expect(firstTransient).toBe(false);

Expand Down
55 changes: 40 additions & 15 deletions packages/orbis-remote-agent-protocol/src/v2-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,16 @@ interface RemoteAgentV2Subscriber {
readonly since: AgentTimestamp;
}

type PendingEntryMetadata = Pick<
Extract<RemoteAgentV2SessionEvent, { readonly type: "entry.appended" }>,
"eventId" | "occurredAt" | "source" | "settlesEntryId"
>;

interface Owner {
readonly nativeRef: AgentSessionRef;
readonly ref: AgentSessionRef;
readonly subscribers: Map<string, RemoteAgentV2Subscriber>;
readonly pendingEntries: Map<string, PendingEntryMetadata>;
initializing: Promise<void>;
index?: RemoteAgentV2StoredSessionIndex;
runtime?: RemoteAgentV2Runtime;
Expand Down Expand Up @@ -2032,6 +2038,7 @@ export class OrbisRemoteAgentV2Host {
nativeRef: nativeRef(this.backend.hostId, ref),
ref,
subscribers: new Map(),
pendingEntries: new Map(),
tail: Promise.resolve(),
transientTail: Promise.resolve(),
presenceSeq: 0,
Expand All @@ -2056,10 +2063,32 @@ export class OrbisRemoteAgentV2Host {
const runtime = await this.backend.connectRuntime(owner.nativeRef);
owner.runtime = runtime;
owner.unsubscribe = runtime.subscribe((event) => {
try {
this.assertNativeEventSession(owner, event);
} catch (error) {
this.report(error);
return;
}
// A queued refresh can observe entries from later native events. Capture
// their delivery metadata before any asynchronous reconciliation begins.
if (event.type === "entry.appended" && event.channel === "replayable") {
owner.pendingEntries.set(event.entry.id, {
eventId: event.eventId,
occurredAt: event.occurredAt,
source: event.source,
...(event.settlesEntryId === undefined ? {} : { settlesEntryId: event.settlesEntryId }),
});
}
const task =
event.channel === "transient"
? this.enqueueTransientEvent(owner, event)
: this.enqueue(owner, () => this.receiveNativeEvent(owner, event));
: this.enqueue(owner, async () => {
try {
await this.receiveNativeEvent(owner, event);
} finally {
if (event.type === "entry.appended") owner.pendingEntries.delete(event.entry.id);
}
});
if (task !== undefined) void task.catch((error) => this.report(error));
});
await this.enqueue(owner, async () => {
Expand Down Expand Up @@ -2120,25 +2149,20 @@ export class OrbisRemoteAgentV2Host {
const previousEntryIds = new Set(previous?.entries.map((candidate) => candidate.id) ?? []);
const newEntries = current.entries.filter((candidate) => !previousEntryIds.has(candidate.id));
for (const entry of newEntries) {
const isTriggeredEntry =
event.type === "entry.appended" &&
event.channel === "replayable" &&
event.entry.id === entry.id;
const metadata = owner.pendingEntries.get(entry.id);
await this.deliverLive(
owner,
entryEvent(
owner.ref,
entry,
isTriggeredEntry ? event.eventId : `entry:${entry.id}`,
isTriggeredEntry ? event.occurredAt : entry.createdAt,
isTriggeredEntry
? event.source
: {
backendId: owner.ref.backendId,
driverId: owner.ref.driverId,
nativeType: "reconciled",
},
isTriggeredEntry ? event.settlesEntryId : undefined,
metadata?.eventId ?? `entry:${entry.id}`,
metadata?.occurredAt ?? entry.createdAt,
metadata?.source ?? {
backendId: owner.ref.backendId,
driverId: owner.ref.driverId,
nativeType: "reconciled",
},
metadata?.settlesEntryId,
),
[...owner.subscribers.values()],
);
Expand Down Expand Up @@ -2209,6 +2233,7 @@ export class OrbisRemoteAgentV2Host {
...(event.type === "entry.delta"
? {
blockIndex: event.blockIndex,
...(event.blockComplete === undefined ? {} : { blockComplete: event.blockComplete }),
chunkSeq: event.chunkSeq,
delta: event.delta,
entryId: event.entryId,
Expand Down
2 changes: 2 additions & 0 deletions packages/orbis-remote-agent-protocol/src/v2-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ export const v2OverlaySchema = z
blocks: z.array(
z
.object({
blockComplete: z.literal(true).optional(),
blockIndex: nonNegativeInteger,
content: v2StreamingContentBlockSchema,
})
Expand Down Expand Up @@ -990,6 +991,7 @@ export const v2SessionEventSchema = z.union([
.passthrough(),
z
.object({
blockComplete: z.literal(true).optional(),
blockIndex: nonNegativeInteger,
channel: z.literal("transient"),
chunkSeq: positiveInteger,
Expand Down
3 changes: 3 additions & 0 deletions packages/orbis-remote-agent-protocol/src/v2-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export interface RemoteAgentV2Overlay {
/** Indexed blocks preserve thinking/text interleaving across snapshots. */
readonly blocks: readonly {
readonly blockIndex: number;
readonly blockComplete?: true;
readonly content: RemoteAgentV2StreamingContentBlock;
}[];
readonly chunkSeq: number;
Expand Down Expand Up @@ -450,6 +451,8 @@ export type RemoteAgentV2SessionEvent =
readonly part: "text" | "thinking" | "tool_input" | "tool_output";
readonly blockIndex: number;
readonly chunkSeq: number;
/** Explicitly closes this content block; delta may be empty. */
readonly blockComplete?: true;
readonly delta: string;
})
| (RemoteAgentV2EventBase & {
Expand Down
4 changes: 2 additions & 2 deletions packages/orbis-remote-dsh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ npm dist-tag/exact version:

```sh
pnpm run serve:dsh --dsh local:/path/to/deepseek-harness
pnpm run serve:dsh --dsh github:tag:dsh-v0.1.2-rc.1
pnpm run serve:dsh --dsh github:tag:dsh-v0.1.5-rc.1
pnpm run serve:dsh --dsh github:commit:a66e4702047846cdaa10c66c9d3df3951f5ea70d
pnpm run serve:dsh --dsh npm:latest
pnpm run serve:dsh --dsh-bin /path/to/dsh
Expand Down Expand Up @@ -301,7 +301,7 @@ the ambient Orbis identity environment variable. The disposable runner
discovers the local LAN endpoint. The runner never touches
the mobile app or its integration tests.

The compatibility gate checks `dsh --version` (expected `0.1.2-rc.1`), the
The compatibility gate checks `dsh --version` (expected `0.1.5-rc.1`), the
launcher `--patch` flag, and Web's `--host`/`--port` flags before creating a fixture. Set
`ORBIS_DSH_EXPECTED_VERSION` only for another explicitly reviewed DSH
profile; an unreviewed or missing CLI is a clear skip by default and a
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{"type":"session","version":0,"id":"orbis-keyless-e2e","createdAt":0}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"ORBIS_KEYLESS_E2E_OK"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ORBIS_KEYLESS_E2E_OK"}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":7,"outputTokens":4,"totalTokens":11}}}}
{"type":"assistant/chunk","data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"session","version":3,"id":"orbis-keyless-e2e","createdAt":0,"isSeeded":false,"delegationDepth":0}
{"type":"turn/start","data":{"turn":1}}
{"type":"step/start","data":{"turn":1,"step":1}}
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"id":"orbis-keyless-system"}},"surfaceOp":"append"}
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"ORBIS_KEYLESS_E2E_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-flash"},"id":"orbis-keyless-message"},"usage":{"inputTokens":7,"outputTokens":1},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":0,"chunk":{"type":"text-delta","index":0,"text":"ORBIS_KEYLESS_E2E_OK"}},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ORBIS_KEYLESS_E2E_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":7,"outputTokens":1}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":1}}
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
Loading
Loading