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
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ test('keeps a failed Close across connection replacement and acknowledges Stop w
client: domainClient({
listRuntimeResources: async () => [],
getRuntimeResource: async () => { throw new Error('must not reread after Stop'); },
stopRuntimeResource: async () => { attempts += 1; return { resource: shellRunUpdate().result as never }; },
stopRuntimeResource: async () => { attempts += 1; return {}; },
}),
}, second);
const recovering = await second.invoke('shell-runs:recover', identity.sessionId) as TerminalRecovery;
Expand Down Expand Up @@ -656,7 +656,7 @@ test('adapts interactive terminal ownership to one Host controller lease', async
client: domainClient({
startRuntimeResource: async (input) => {
calls.push({ operation: 'start', input });
return { resource: update.result as never };
return { resource: update.result };
},
getRuntimeResource: async (sessionId, ref) => {
calls.push({ operation: 'get', input: { sessionId, ref } });
Expand All @@ -668,15 +668,15 @@ test('adapts interactive terminal ownership to one Host controller lease', async
},
controlRuntimeResource: async (input) => {
calls.push({ operation: 'control', input });
return { controllerId: input.controllerId, sequence: input.sequence, resource: update.result as never };
return { controllerId: input.controllerId, sequence: input.sequence };
},
releaseRuntimeResourceController: async (input) => {
calls.push({ operation: 'release', input });
return { controllerId: input.controllerId, released: true };
},
stopRuntimeResource: async (input) => {
calls.push({ operation: 'stop', input });
return { resource: update.result as never };
return {};
},
}),
sessionObserver: {
Expand Down Expand Up @@ -853,7 +853,6 @@ test('reacquires a missing terminal controller with protocol-exact identity fiel
controlRuntimeResource: async (input) => ({
controllerId: input.controllerId,
sequence: input.sequence,
resource: shellRunUpdate().result as never,
}),
getRuntimeResource: async () => shellRunUpdate(),
}),
Expand Down
22 changes: 11 additions & 11 deletions apps/desktop/src/main/runtime-host-shell-runs-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { randomUUID } from 'node:crypto';
import { DESKTOP_TERMINAL_LAUNCH_PREFIX } from '../shared/runtime-host-identity.js';
import { DESKTOP_TERMINAL_LAUNCH_PREFIX } from '@maka/core/shell-run';
import type { ShellRunUpdate } from '@maka/core/events';
import type { ShellRunPtySnapshot } from '@maka/runtime/shell-run-contract';
import type { SessionDomainChange } from '@maka/runtime-host/protocol';
Expand All @@ -35,7 +35,6 @@ export type RuntimeHostShellRunsClient = Pick<
DesktopRuntimeHostClient,
| 'acquireRuntimeResourceController'
| 'controlRuntimeResource'
| 'getRuntimeResource'
| 'listRuntimeResources'
| 'releaseRuntimeResourceController'
| 'startRuntimeResource'
Expand Down Expand Up @@ -101,13 +100,19 @@ export function registerRuntimeHostShellRunsIpc(
);
ipcMain.handle('shell-runs:start', async (_event, sessionId: unknown) => {
const normalizedSessionId = requiredId(sessionId, 'Session');
const launchId = `${DESKTOP_TERMINAL_LAUNCH_PREFIX}${newId()}`;
const started = await deps.client.startRuntimeResource({
sessionId: normalizedSessionId,
launchId: `${DESKTOP_TERMINAL_LAUNCH_PREFIX}${newId()}`,
launchId,
});
return requiredRuntimeResource(
await deps.client.getRuntimeResource(normalizedSessionId, started.resource.ref),
);
const update: ShellRunUpdate = {
sessionId: normalizedSessionId,
ownership: { kind: 'local' },
sourceTurnId: launchId,
sourceToolCallId: launchId,
result: started.resource,
};
return update;
});
ipcMain.handle('shell-runs:attach', (event, value: unknown) =>
controllers.attach(
Expand Down Expand Up @@ -421,11 +426,6 @@ function parseResourceIdentity(identity: string): [sessionId: string, ref: strin
return [identity.slice(0, separator), identity.slice(separator + 1)];
}

function requiredRuntimeResource(resource: ShellRunUpdate | null): ShellRunUpdate {
if (!resource) throw new Error('Terminal started without a Runtime Resource projection');
return resource;
}

function requiredId(value: unknown, name: string, maxLength = 512): string {
if (
typeof value !== 'string'
Expand Down
30 changes: 17 additions & 13 deletions apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,21 +456,25 @@ async function runtimeHostSessionRef(sessionId: string): Promise<{
readonly sessionId: string;
}> {
const ref = parseDesktopSessionKey(sessionId);
await runtimeHostScopeList();
const recordedProfileId = runtimeHostSessionProfiles.get(sessionId);
let scope: DesktopTargetScope | undefined;
if (recordedProfileId) {
const scopeKey = runtimeHostProfiles.get(recordedProfileId);
scope = scopeKey ? runtimeHostScopes.get(scopeKey) : undefined;
if (!scope || scope.hostId !== ref.hostId) {
throw new Error('The Runtime Host for this task is unavailable');
// The profile maps are already kept current by the identities push channel;
// only pull on a miss so routine calls (every terminal keystroke) stay local.
for (let attempt = 0; attempt < 2; attempt++) {
if (attempt > 0) await runtimeHostScopeList();
const recordedProfileId = runtimeHostSessionProfiles.get(sessionId);
let scope: DesktopTargetScope | undefined;
if (recordedProfileId) {
const scopeKey = runtimeHostProfiles.get(recordedProfileId);
const recorded = scopeKey ? runtimeHostScopes.get(scopeKey) : undefined;
if (recorded?.hostId === ref.hostId) scope = recorded;
} else {
const candidates = [...runtimeHostScopes.values()].filter(
({ hostId }) => hostId === ref.hostId,
);
if (candidates.length === 1) scope = candidates[0];
}
} else {
const candidates = [...runtimeHostScopes.values()].filter(({ hostId }) => hostId === ref.hostId);
if (candidates.length === 1) scope = candidates[0];
if (scope) return { scope, sessionId: ref.sessionId };
}
if (!scope) throw new Error('The Runtime Host for this task is unavailable');
return { scope, sessionId: ref.sessionId };
throw new Error('The Runtime Host for this task is unavailable');
}

function hostAttachmentRefs(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ export class SessionTerminalRenderQueue {
if (!this.#writing) this.sink.resync();
return;
}
this.#pending.push({ data, reset: false });
const tail = this.#pending.at(-1);
if (tail && !tail.reset) tail.data += data;
else this.#pending.push({ data, reset: false });
this.#bytes += data.length * 2;
this.#pump();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@

import type { MakaBridge } from '../../../preload/bridge-contract.js';
import type { ShellRunUpdate } from '@maka/core/events';
import { isTerminalShellRunStatus } from '@maka/core/shell-run';
import { DESKTOP_TERMINAL_LAUNCH_PREFIX } from '../../../shared/runtime-host-identity.js';
import {
isDesktopTerminalShellRun,
isTerminalShellRunStatus,
} from '@maka/core/shell-run';
import type { WorkbarServices } from '../../features/workbar';
import { readSettledMessagesFrom } from './session-message-settlement.js';
import { expectSessionUpdate } from './create-session-settings-services.js';
Expand Down Expand Up @@ -49,10 +51,10 @@ const DEFAULT_DEPENDENCIES: DesktopWorkbarServiceDependencies = {
};

function isDesktopTerminal(update: ShellRunUpdate): boolean {
return update.ownership.kind === 'local' &&
update.sourceTurnId.startsWith(DESKTOP_TERMINAL_LAUNCH_PREFIX) &&
update.sourceTurnId === update.sourceToolCallId &&
update.result.mode === 'pty';
return (
update.ownership.kind === 'local' &&
isDesktopTerminalShellRun({ ...update, mode: update.result.mode })
);
}

/** The only Desktop-to-Workbar adapter. It narrows the preload bridge by tool. */
Expand Down
3 changes: 0 additions & 3 deletions apps/desktop/src/shared/runtime-host-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ export interface DesktopHostRef {
readonly hostId: string;
}

/** Desktop launch identity persisted by Host as sourceTurnId/sourceToolCallId. */
export const DESKTOP_TERMINAL_LAUNCH_PREFIX = 'desktop-terminal-';

export interface DesktopTargetScope extends DesktopHostRef {
readonly targetEpoch: string;
}
Expand Down
10 changes: 1 addition & 9 deletions packages/cli/src/__tests__/runtime-host-session-driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2973,15 +2973,7 @@ class FakeConnection {
}
if (operation === 'runtime.resource.stop') {
if (this.runtimeResourceStopFailure) throw this.runtimeResourceStopFailure;
return {
resource: {
...this.userCommandResource,
status: 'cancelled',
updatedAt: 2,
completedAt: 2,
revision: 2,
},
} as OperationOutput<K>;
return {} as OperationOutput<K>;
}
if (operation === 'runtime.resource.query') {
if (this.runtimeResourceQuery === undefined) {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/pi-transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type {
SandboxBoundaryRequestEvent,
UserQuestionRequestEvent,
SessionEvent,
ShellRunSnapshotResult,
ShellRunStateResult,
ToolOutputStream,
ToolResultContent,
} from '@maka/core/events';
Expand Down Expand Up @@ -392,7 +392,7 @@ export function applyShellRunUpdateToTranscript(
/** Adds a local-only card for a `!<command>` resource without creating a model turn. */
export function appendUserCommandToTranscript(
state: MakaPiTranscriptState,
input: { commandId: string; command: string; result: ShellRunSnapshotResult },
input: { commandId: string; command: string; result: ShellRunStateResult },
): void {
state.entries.push({
kind: 'tool',
Expand Down
15 changes: 5 additions & 10 deletions packages/cli/src/runtime-host-session-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { markPersisted } from '@maka/core/persisted-value';
import {
type ActiveInteractionRequestEvent,
type SessionEvent,
type ShellRunSnapshotResult,
type ShellRunStateResult,
type ShellRunUpdate,
} from '@maka/core/events';
import { isSideConversationSession } from '@maka/core/side-conversation';
Expand Down Expand Up @@ -391,7 +391,7 @@ class RuntimeHostMakaSessionDriverImpl implements RuntimeHostMakaSessionDriver {

async runUserCommand(command: string): Promise<{
commandId: string;
result: ShellRunSnapshotResult;
result: ShellRunStateResult;
takeRacedUpdate(): ShellRunUpdate['result'] | undefined;
}> {
const stopGeneration = this.#userCommandStopGeneration;
Expand Down Expand Up @@ -1716,17 +1716,12 @@ class RuntimeHostMakaSessionDriverImpl implements RuntimeHostMakaSessionDriver {
owner: { readonly sessionId: string; readonly commandId: string },
): Promise<void> {
if (this.#activeUserCommands.get(ref) !== owner) return;
const stopped = await this.#request('runtime.resource.stop', {
await this.#request('runtime.resource.stop', {
sessionId: owner.sessionId,
ref,
});
this.#publishShellRunUpdate({
sessionId: owner.sessionId,
ownership: { kind: 'local' },
sourceTurnId: owner.commandId,
sourceToolCallId: owner.commandId,
result: stopped.resource,
});
this.#activeUserCommands.delete(ref);
this.#publishRuntimeResource(owner.sessionId, ref);
}

#publishShellRunUpdate(update: ShellRunUpdate): void {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/session-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { realpath } from 'node:fs/promises';
import type { SessionEvent, ShellRunSnapshotResult, ShellRunUpdate } from '@maka/core/events';
import type { SessionEvent, ShellRunStateResult, ShellRunUpdate } from '@maka/core/events';
import type { OrchestrationMode } from '@maka/core/orchestration';
import type { PermissionMode } from '@maka/core/permission';
import type { SandboxBoundaryResponse } from '@maka/core/sandbox-boundary';
Expand Down Expand Up @@ -138,7 +138,7 @@ export function skillInvocationBlockedMessage(skillInvocation: SkillInvocationRe

export interface MakaUserCommand {
readonly commandId: string;
readonly result: ShellRunSnapshotResult;
readonly result: ShellRunStateResult;
/** Returns the newest update that raced the initial card into the transcript. */
takeRacedUpdate(): ShellRunUpdate['result'] | undefined;
}
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/shell-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,25 @@ export function isActiveShellRunStatus(value: ShellRunStatus): value is ShellRun
return (SHELL_RUN_ACTIVE_STATUSES as readonly string[]).includes(value);
}

/** Desktop interactive-terminal launch identity, persisted as the run's source ids. */
export const DESKTOP_TERMINAL_LAUNCH_PREFIX = 'desktop-terminal-';

/**
* A Desktop-owned interactive terminal carries no transcript tool call, so
* nothing consumes the output on its update and wire projections.
*/
export function isDesktopTerminalShellRun(source: {
readonly sourceTurnId: string;
readonly sourceToolCallId: string;
readonly mode: string;
}): boolean {
return (
source.mode === 'pty' &&
source.sourceTurnId === source.sourceToolCallId &&
source.sourceTurnId.startsWith(DESKTOP_TERMINAL_LAUNCH_PREFIX)
);
}

export function isValidShellRunStatusTransition(
current: ShellRunStatus,
next: ShellRunStatus,
Expand Down
Loading