@rho-workspace/rho 0.5.5 → 0.5.6
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.
- package/dist/agent-connect.d.ts +8 -0
- package/dist/agent-connect.js +15 -0
- package/dist/browser-access.js +1 -1
- package/dist/cli.js +38 -8
- package/dist/contract.generated.d.ts +148 -5
- package/dist/contract.generated.js +1 -1
- package/dist/contracts/owners.generated.js +107 -12
- package/dist/conversations.d.ts +157 -4
- package/dist/conversations.js +69 -6
- package/dist/discovery.js +1 -1
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.js +12 -5
- package/dist/server.d.ts +2 -10
- package/dist/server.js +71 -8
- package/dist/web/.vite/manifest.json +61 -61
- package/dist/web/assets/{choices-b0XhrGQV.js → choices-D-YfnFP6.js} +1 -1
- package/dist/web/assets/{console-BSjLGe0T.js → console-Trqc0swD.js} +1 -1
- package/dist/web/assets/{dist-Buk0lwzI.js → dist-DZmszrjG.js} +1 -1
- package/dist/web/assets/{dist-Cw8hRIlT.js → dist-DpIHRk3B.js} +1 -1
- package/dist/web/assets/{dist-foH9IKc6.js → dist-eMx6b8N1.js} +1 -1
- package/dist/web/assets/{dist-Tzoe_zm-.js → dist-pF902jHA.js} +1 -1
- package/dist/web/assets/{explore-in-BVUy3ZW_.js → explore-in-BCHqjJ7B.js} +1 -1
- package/dist/web/assets/{graph-1OdIJ9l7.js → graph-lENgfQNH.js} +1 -1
- package/dist/web/assets/index-B3_OVy91.css +1 -0
- package/dist/web/assets/{index-CxmItXqa.js → index-BpL_Kl_s.js} +84 -83
- package/dist/web/assets/{notes-meaning-pVbbQUGP.js → notes-meaning-DcVGUBBY.js} +1 -1
- package/dist/web/assets/{overview-B9UtVJ32.js → overview-CDnVKlOt.js} +3 -3
- package/dist/web/assets/{preview-CksD_5KS.js → preview-BEC1E9MK.js} +1 -1
- package/dist/web/assets/{review-CMxQlQ-C.js → review-DYP0aCXb.js} +1 -1
- package/dist/web/assets/{skills-BFNSyrnD.js → skills-1ec0Z-jx.js} +1 -1
- package/dist/web/assets/{skills-work-BGPSnF_I.js → skills-work-DNexV4OJ.js} +1 -1
- package/dist/web/assets/{studio-process-CsRoTHOh.js → studio-process-EYQgikxq.js} +1 -1
- package/dist/web/assets/{timeline-events-BbuuFWHV.js → timeline-events-Chj2Gbx7.js} +1 -1
- package/dist/web/assets/{work-turns-BvJlIMmA.js → work-turns-CT9RVcWY.js} +1 -1
- package/dist/web/assets/{workspace-CWlkjLNx.js → workspace-COmCbGV3.js} +1 -1
- package/dist/web/composition.json +58 -58
- package/dist/web/index.html +2 -2
- package/dist/web/renderer.json +1 -1
- package/docs/FRONTEND.md +26 -18
- package/docs/HEADLESS.md +37 -27
- package/node_modules/@rho-workspace/plugin-agent/README.md +29 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +9 -1
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +10 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +10 -5
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +12 -1
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
- package/node_modules/@rho-workspace/plugin-agent/package.json +2 -1
- package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
- package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
- package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
- package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
- package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
- package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
- package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
- package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
- package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
- package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
- package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
- package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
- package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
- package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
- package/node_modules/hono/dist/cjs/utils/url.js +1 -1
- package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
- package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
- package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
- package/node_modules/hono/dist/jsx/base.js +49 -32
- package/node_modules/hono/dist/jsx/components.js +2 -2
- package/node_modules/hono/dist/jsx/constants.js +2 -1
- package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
- package/node_modules/hono/dist/middleware/etag/index.js +3 -1
- package/node_modules/hono/dist/middleware/language/language.js +1 -1
- package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
- package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
- package/node_modules/hono/dist/utils/cookie.js +2 -2
- package/node_modules/hono/dist/utils/mime.js +1 -1
- package/node_modules/hono/dist/utils/url.js +1 -1
- package/node_modules/hono/package.json +1 -1
- package/package.json +2 -2
- package/protocol/index.json +39 -33
- package/protocol/operations/agent.sessions.list.json +49 -3
- package/protocol/operations/agent.sessions.read.json +53 -4
- package/protocol/operations/conversation.configure.json +29 -0
- package/protocol/operations/conversation.decide.json +29 -0
- package/protocol/operations/conversation.list.json +29 -0
- package/protocol/operations/conversation.permission.json +29 -0
- package/protocol/operations/conversation.read.json +29 -0
- package/protocol/operations/conversation.restore.json +29 -0
- package/protocol/operations/conversation.send.json +29 -0
- package/protocol/operations/conversation.settings.check.json +92 -0
- package/protocol/operations/conversation.settings.read.json +92 -0
- package/protocol/operations/conversation.settings.update.json +92 -0
- package/protocol/operations/conversation.start.json +46 -1
- package/protocol/operations/conversation.stop.json +29 -0
- package/protocol/operations/conversation.trash.json +29 -0
- package/protocol/operations/observation.snapshot.json +29 -0
- package/protocol/operations/session.instructions.json +43 -0
- package/vendor/composition.json +74 -74
- package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
- package/web/theme.css +12 -0
- package/dist/web/assets/index-CsC3sTRq.css +0 -1
- package/vendor/rho-workspace-plugin-agent-0.4.29.tgz +0 -0
package/dist/conversations.js
CHANGED
|
@@ -8,6 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { atomicJson, canonical, digest, Fault, now, readJson, requireThat } from '@rho-workspace/plugin-contracts';
|
|
11
|
+
import { acpEnvironment, missingClient } from '@rho-workspace/plugin-agent/acp-command';
|
|
11
12
|
// Rho holds user messages and their original identities. The selected Agent plugin
|
|
12
13
|
// owns the connection, native session and protocol. Reads never start a driver.
|
|
13
14
|
// A new Claude Code session opens in Manual mode unless the person's Claude settings name another.
|
|
@@ -234,7 +235,8 @@ export class Conversations {
|
|
|
234
235
|
return { conversationId: c.id, changeId: c.changeId, requestDigest: c.requestDigest, title: c.title, client: c.client, principal: c.principal, createdAt: c.createdAt, updatedAt: c.updatedAt, version: c.version, turns: c.turns.length, state: last?.state ?? 'idle', lastAt: last?.endedAt ?? last?.sentAt ?? c.createdAt,
|
|
235
236
|
listed: 2, turnLimit: !!last && agentTurnLimit(last), waitingPermission: last?.state === 'running' && !!last.permissions?.some(p => p.state === 'pending'), backend: c.backend ?? 'claude-cli', agentTitle: c.observation?.info?.title ?? null, adapter: c.adapter ?? null, model: [...c.turns].reverse().find(t => t.model)?.model ?? null, config: c.config ?? {},
|
|
236
237
|
usage: usage ? { used: usage.used, size: usage.size, cost: usage.cost ?? null } : null, costUsd: costs.length ? costs.reduce((a, b) => a + b, 0) : null,
|
|
237
|
-
sessionId: c.acpSessionId ?? c.nativeSessionId ?? null, trashedAt: c.trashedAt ?? null
|
|
238
|
+
sessionId: c.acpSessionId ?? c.nativeSessionId ?? null, trashedAt: c.trashedAt ?? null,
|
|
239
|
+
origin: c.origin ? { producer: c.origin.producer, where: c.origin.where, ...(c.origin.copy ? { copyOf: c.origin.sessionId } : {}) } : null };
|
|
238
240
|
}
|
|
239
241
|
async write(project, c) {
|
|
240
242
|
const directory = await this.folder(project);
|
|
@@ -394,7 +396,9 @@ export class Conversations {
|
|
|
394
396
|
requireThat(!input.backend || backends.includes(input.backend), 'invalid', 'Choose Claude Code or Codex.');
|
|
395
397
|
if (input.config)
|
|
396
398
|
this.choices(input.config);
|
|
397
|
-
const
|
|
399
|
+
const from = input.continue;
|
|
400
|
+
const text = this.message(input.text), requestDigest = digest(canonical({ text, ...(input.contexts?.length ? { contexts: input.contexts } : {}), ...(input.backend ? { backend: input.backend } : {}), ...(input.config ? { config: input.config } : {}),
|
|
401
|
+
...(from ? { continue: { producer: from.producer, sessionId: from.sessionId, copy: from.copy } } : {}) }));
|
|
398
402
|
return this.serial(project.id, async () => {
|
|
399
403
|
const index = await this.index(project), existing = index.conversations.find(x => x.changeId === input.changeId);
|
|
400
404
|
requireThat(!index.deleted?.some(x => x.changeId === input.changeId), 'conflict', 'That request started a conversation that was later deleted. It is not started again.');
|
|
@@ -404,13 +408,26 @@ export class Conversations {
|
|
|
404
408
|
return this.reply(project, c, c.turns[0]);
|
|
405
409
|
}
|
|
406
410
|
requireThat(index.conversations.filter(x => !x.trashedAt).length < this.limits.maxConversations, 'limit', `This project keeps at most ${this.limits.maxConversations} conversations started in Rho. Move one to the trash to start another.`, { limit: 'maxConversations', maximum: this.limits.maxConversations });
|
|
411
|
+
if (from && !from.copy) {
|
|
412
|
+
const already = index.conversations.find(x => x.sessionId === from.sessionId);
|
|
413
|
+
requireThat(!already, 'conflict', already?.trashedAt ? 'This session is a conversation in the trash. Restore it to continue.' : 'This session already continues as a conversation here. Open it to send your message.');
|
|
414
|
+
await this.notHeld(project, from.producer, from.sessionId, false);
|
|
415
|
+
}
|
|
407
416
|
this.capacity();
|
|
408
417
|
const saved = await this.readSettings();
|
|
409
|
-
const backend = input.backend ?? saved.backend ?? this.config.backend ?? 'claude-acp';
|
|
418
|
+
const backend = from?.backend ?? input.backend ?? saved.backend ?? this.config.backend ?? 'claude-acp';
|
|
410
419
|
const config = backend === 'claude-cli' ? undefined : input.config ?? saved.choices[backend];
|
|
411
420
|
const at = now(), c = { format: 2, backend, id: 'c-' + randomBytes(8).toString('hex'), changeId: input.changeId, requestDigest, createdAt: at, createdBy: by, updatedAt: at, version: 0, client: backend === 'codex-acp' ? 'codex' : backend === 'acp' ? 'acp' : 'claude-code', nativeSessionId: backend === 'claude-cli' ? randomUUID() : null, confirmed: false, principal: null, title: titleOf(text), turns: [], ...(config && Object.keys(config).length ? { config } : {}) };
|
|
412
421
|
for (const id of input.contexts ?? [])
|
|
413
422
|
await this.ports.context?.(project, id);
|
|
423
|
+
if (from) {
|
|
424
|
+
c.origin = { producer: from.producer, sessionId: from.sessionId, where: from.where, copy: from.copy };
|
|
425
|
+
if (from.copy)
|
|
426
|
+
c.forkFrom = from.sessionId;
|
|
427
|
+
else
|
|
428
|
+
c.acpSessionId = from.sessionId;
|
|
429
|
+
c.title = from.title;
|
|
430
|
+
}
|
|
414
431
|
const turn = this.turn(input.changeId, requestDigest, by, text, 1);
|
|
415
432
|
if (input.contexts?.length)
|
|
416
433
|
turn.contexts = [...input.contexts];
|
|
@@ -421,6 +438,29 @@ export class Conversations {
|
|
|
421
438
|
return this.reply(project, c, turn);
|
|
422
439
|
});
|
|
423
440
|
}
|
|
441
|
+
/** Whether a process holding a session open is Rho's own connection to this conversation: the
|
|
442
|
+
* Agent its adapter started. */
|
|
443
|
+
async owns(project, conversationId, pid) {
|
|
444
|
+
const adapter = this.drivers.get(project.id + '/' + conversationId)?.driver?.pid;
|
|
445
|
+
if (!adapter || !pid || process.platform === 'win32')
|
|
446
|
+
return false;
|
|
447
|
+
const { execFile } = await import('node:child_process');
|
|
448
|
+
let current = pid;
|
|
449
|
+
for (let depth = 0; depth < 4 && current > 1; depth++) {
|
|
450
|
+
const parent = await new Promise(resolve => execFile('ps', ['-o', 'ppid=', '-p', String(current)], { timeout: 2000 }, (error, out) => resolve(error ? 0 : Number(String(out).trim()))));
|
|
451
|
+
if (parent === adapter)
|
|
452
|
+
return true;
|
|
453
|
+
current = parent;
|
|
454
|
+
}
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
async notHeld(project, producer, sessionId, existing, conversationId) {
|
|
458
|
+
if (producer !== 'claude-code' && producer !== 'codex')
|
|
459
|
+
return;
|
|
460
|
+
const found = await this.ports.nativeSession?.(project, producer, sessionId) ?? null;
|
|
461
|
+
const holder = found?.open && !(conversationId && await this.owns(project, conversationId, found.open.pid)) ? found.open : null;
|
|
462
|
+
requireThat(!holder, 'condition', `This session is open in ${holder?.where ?? 'another window'}. Close it there to continue here${existing ? '' : ', or continue a copy'}. Nothing was sent.`, { heldBy: holder?.where ?? null });
|
|
463
|
+
}
|
|
424
464
|
async send(project, by, input) {
|
|
425
465
|
const text = this.message(input.text), requestDigest = digest(canonical({ text, ...(input.contexts?.length ? { contexts: input.contexts } : {}), ...(input.continuationOf ? { continuationOf: input.continuationOf } : {}) }));
|
|
426
466
|
return this.serial(project.id, async () => {
|
|
@@ -444,6 +484,9 @@ export class Conversations {
|
|
|
444
484
|
requireThat(!alive, 'conflict', `The Agent from before Rho restarted may still be answering (process ${last.pid}). End that process before sending another message.`);
|
|
445
485
|
}
|
|
446
486
|
requireThat(c.turns.length < this.limits.maxTurns, 'limit', `A conversation keeps at most ${this.limits.maxTurns} messages. Start a new conversation.`, { limit: 'maxTurns', maximum: this.limits.maxTurns });
|
|
487
|
+
// A session continued in a terminal or another app may be open there now.
|
|
488
|
+
if (c.acpSessionId && (c.client === 'claude-code' || c.client === 'codex'))
|
|
489
|
+
await this.notHeld(project, c.client, c.acpSessionId, true, c.id);
|
|
447
490
|
requireThat(Buffer.byteLength(JSON.stringify({ ...c, observation: undefined })) + this.limits.maxObservationBytes + this.limits.maxTurnBytes + this.limits.maxMessageBytes <= this.limits.maxRecordBytes, 'limit', 'This conversation has reached the size Rho keeps. Start a new conversation.', { limit: 'maxRecordBytes', maximum: this.limits.maxRecordBytes });
|
|
448
491
|
this.capacity();
|
|
449
492
|
for (const old of c.turns)
|
|
@@ -510,7 +553,19 @@ export class Conversations {
|
|
|
510
553
|
}
|
|
511
554
|
settingsView(s) {
|
|
512
555
|
const service = this.config.backend ?? 'claude-acp';
|
|
513
|
-
return { version: s.version, updatedAt: s.updatedAt, backend: s.backend, serviceBackend: service, choices: s.choices, offered: s.offered, checking: [...this.checks.keys()], describe: { enabled: s.describe?.enabled !== false } };
|
|
556
|
+
return { version: s.version, updatedAt: s.updatedAt, backend: s.backend, serviceBackend: service, choices: s.choices, offered: s.offered, checking: [...this.checks.keys()], describe: { enabled: s.describe?.enabled !== false }, agents: this.agents() };
|
|
557
|
+
}
|
|
558
|
+
// Which Agents this service can start: the person's installed clients, found as the adapters
|
|
559
|
+
// find them, and an ACP command the person named. Looking starts nothing.
|
|
560
|
+
agents() {
|
|
561
|
+
const found = (problem) => problem ? { found: false, problem } : { found: true };
|
|
562
|
+
const command = this.config.acpCommand;
|
|
563
|
+
return {
|
|
564
|
+
// A command the embedding names is its own choice and is not looked for, as the adapters do.
|
|
565
|
+
'claude-acp': found(this.config.claudeAcp?.command ? null : missingClient('claude', acpEnvironment('claude', { ...process.env }, this.binary))),
|
|
566
|
+
'codex-acp': found(this.config.codexAcp?.command ? null : missingClient('codex', acpEnvironment('codex', { ...process.env }, this.config.codex))),
|
|
567
|
+
acp: command ? { found: true, name: /(^|[\\/])dsh(\.[a-z]+)?$/i.test(command.file) || command.args.some(a => /(^|[\\/])dsh(\.[a-z]+)?$/i.test(a)) ? 'DeepSeek Harness' : null } : { found: false, problem: 'Start Rho with --agent-backend acp --acp-command to use another ACP Agent.' }
|
|
568
|
+
};
|
|
514
569
|
}
|
|
515
570
|
async settings() { return this.settingsView(await this.readSettings()); }
|
|
516
571
|
// The person's Agent and settings for new conversations. Each Agent's choices are replaced as a
|
|
@@ -733,12 +788,20 @@ export class Conversations {
|
|
|
733
788
|
}
|
|
734
789
|
});
|
|
735
790
|
}
|
|
791
|
+
async changedElsewhere(project, c) {
|
|
792
|
+
if (!c.acpSessionId || (c.client !== 'claude-code' && c.client !== 'codex'))
|
|
793
|
+
return false;
|
|
794
|
+
const previous = c.turns.at(-2), found = await this.ports.nativeSession?.(project, c.client, c.acpSessionId).catch(() => null);
|
|
795
|
+
return !!found && !!previous?.endedAt && Date.parse(found.lastAt) > Date.parse(previous.endedAt) + 5000;
|
|
796
|
+
}
|
|
736
797
|
async driver(project, c) {
|
|
737
798
|
return this.serial('driver-pool', async () => {
|
|
738
799
|
const key = project.id + '/' + c.id, old = this.drivers.get(key);
|
|
739
800
|
if (old?.timer)
|
|
740
801
|
clearTimeout(old.timer);
|
|
741
|
-
|
|
802
|
+
// An open connection keeps the session as it last saw it. When the session went on in
|
|
803
|
+
// another window since, it is opened again from the Agent's own record.
|
|
804
|
+
if (old?.driver.connected && !await this.changedElsewhere(project, c))
|
|
742
805
|
return old.driver;
|
|
743
806
|
if (old)
|
|
744
807
|
await this.retire(key);
|
|
@@ -770,7 +833,7 @@ export class Conversations {
|
|
|
770
833
|
} };
|
|
771
834
|
// Every session started here is told about the project's shared notes and its section.
|
|
772
835
|
const instructions = notesInstructions(c.id, { notebook: !!this.ports.notebook?.() });
|
|
773
|
-
const common = { cwd: project.root, mcpServers, limits: this.limits, sessionId: c.acpSessionId, config: c.config, instructions, ...observed };
|
|
836
|
+
const common = { cwd: project.root, mcpServers, limits: this.limits, sessionId: c.acpSessionId, ...(!c.acpSessionId && c.forkFrom ? { forkFrom: c.forkFrom } : {}), config: c.config, instructions, ...observed };
|
|
774
837
|
const driver = c.backend === 'codex-acp'
|
|
775
838
|
? new CodexAcpConversation({ ...this.config.codexAcp, ...common, codex: this.config.codex })
|
|
776
839
|
: c.backend === 'claude-acp'
|
package/dist/discovery.js
CHANGED
|
@@ -4,7 +4,7 @@ import { protocol } from './protocol.js';
|
|
|
4
4
|
import { schemaClosure } from './protocol-discovery.js';
|
|
5
5
|
const size = (v) => Buffer.byteLength(JSON.stringify(v)) + 64;
|
|
6
6
|
export const applicationEffects = Object.fromEntries([
|
|
7
|
-
...['extension.list', 'extension.describe', 'extension.content', 'extension.read', 'protocol.describe', 'protocol.validate', 'project.entries', 'capability.list', 'skill.list', 'skill.read', 'action.lookup', 'action.observe', 'action.output', 'action.list', 'composition.read', 'observation.snapshot', 'interaction.selection.read', 'workspace.observe', 'task.map.read'].map(m => [m, 'read']),
|
|
7
|
+
...['session.instructions', 'extension.list', 'extension.describe', 'extension.content', 'extension.read', 'protocol.describe', 'protocol.validate', 'project.entries', 'capability.list', 'skill.list', 'skill.read', 'action.lookup', 'action.observe', 'action.output', 'action.list', 'composition.read', 'observation.snapshot', 'interaction.selection.read', 'workspace.observe', 'task.map.read'].map(m => [m, 'read']),
|
|
8
8
|
...['composition.put', 'view.close', 'capability.configure', 'composition.apply', 'composition.undo', 'view.protection.set', 'interaction.selection.set', 'selection.set', 'workspace.enhance', 'task.map.declare', 'task.map.present', 'task.map.respond'].map(m => [m, 'metadata']),
|
|
9
9
|
...['workspace.save', 'workspace.export', 'workspace.restore', 'action.export'].map(m => [m, 'file']),
|
|
10
10
|
...['extension.call', 'extension.install', 'extension.manage', 'action.retry', 'action.cancel'].map(m => [m, 'native']),
|
package/dist/protocol.d.ts
CHANGED
|
@@ -173,6 +173,7 @@ export declare const descriptions: {
|
|
|
173
173
|
'conversation.settings.update': string;
|
|
174
174
|
'conversation.settings.check': string;
|
|
175
175
|
'session.connect': string;
|
|
176
|
+
'session.instructions': string;
|
|
176
177
|
'conversation.read': string;
|
|
177
178
|
'project.work.read': string;
|
|
178
179
|
'resource.register': string;
|
package/dist/protocol.js
CHANGED
|
@@ -71,7 +71,8 @@ export const operations = { ...base.operations, ...extensionOperations,
|
|
|
71
71
|
'data.compare': obj({ ...project, left: ref, right: ref, options: obj({ leftKey: text, rightKey: text, leftNamespace: text, rightNamespace: text, mapping: { const: 'equal-string' }, leftField: text, rightField: text, join: { enum: ['left', 'inner', 'full'] } }) }),
|
|
72
72
|
'media.describe': obj({ ...project, resource: ref }),
|
|
73
73
|
// A conversation the person starts in Rho: their words, under a request identity they hold.
|
|
74
|
-
'conversation.start': obj({ ...project, changeId, text: { type: 'string', minLength: 1, maxLength: 16384 }, backend: agentBackend, config: choices, contexts: { ...array(digest), maxItems: 8 }
|
|
74
|
+
'conversation.start': obj({ ...project, changeId, text: { type: 'string', minLength: 1, maxLength: 16384 }, backend: agentBackend, config: choices, contexts: { ...array(digest), maxItems: 8 },
|
|
75
|
+
continue: obj({ session: { type: 'string', pattern: '^x-[a-f0-9]{64}$' }, copy: { type: 'boolean' } }, ['session']) }, ['projectId', 'changeId', 'text']),
|
|
75
76
|
'conversation.send': obj({ ...project, conversationId: changeId, changeId, text: { type: 'string', minLength: 1, maxLength: 16384 }, contexts: { ...array(digest), maxItems: 8 }, continuationOf: changeId }, ['projectId', 'conversationId', 'changeId', 'text']),
|
|
76
77
|
'conversation.permission': obj({ ...project, conversationId: changeId, turnId: changeId, permissionId: changeId, optionId: { type: 'string', minLength: 1, maxLength: 256 } }),
|
|
77
78
|
'conversation.stop': obj({ ...project, conversationId: changeId, turnId: changeId }),
|
|
@@ -87,7 +88,8 @@ export const operations = { ...base.operations, ...extensionOperations,
|
|
|
87
88
|
'conversation.settings.update': obj({ ...project, expectedVersion: integer, backend: { anyOf: [agentBackend, { type: 'null' }] }, choices: obj({ 'claude-acp': choices, 'codex-acp': choices, 'acp': choices }, []), describe: obj({ enabled: { type: 'boolean' } }, ['enabled']) }, ['projectId', 'expectedVersion']),
|
|
88
89
|
'conversation.settings.check': obj({ ...project, backend: agentBackend }),
|
|
89
90
|
// Claude Code or Codex running outside Rho, given this project and told how to start Rho's MCP server.
|
|
90
|
-
'session.connect': obj({ ...project, agent: outsideAgent })
|
|
91
|
+
'session.connect': obj({ ...project, agent: outsideAgent }),
|
|
92
|
+
'session.instructions': obj(project)
|
|
91
93
|
};
|
|
92
94
|
// Every owner-declared operation and response comes from the snapshot, scoped once.
|
|
93
95
|
for (const [method, contract] of Object.entries(ownerOperations))
|
|
@@ -200,14 +202,17 @@ const catalog = { ...contract.capabilityCatalogSchema };
|
|
|
200
202
|
const conversationSummary = response({ conversationId: text, title: text, client: text, principal: { anyOf: [text, { type: 'null' }] }, createdAt: text, updatedAt: text, version: integer, turns: integer, state: { enum: ['idle', 'running', 'completed', 'failed', 'stopped', 'unknown'] }, lastAt: text,
|
|
201
203
|
backend: text, agentTitle: { anyOf: [text, { type: 'null' }] }, adapter: { anyOf: [response({ name: text, version: text }, ['name']), { type: 'null' }] }, model: { anyOf: [text, { type: 'null' }] }, config: choices, turnLimit: { type: 'boolean' }, waitingPermission: { type: 'boolean' },
|
|
202
204
|
usage: { anyOf: [response({ used: { type: 'number', minimum: 0 }, size: { type: 'number', minimum: 0 }, cost: { anyOf: [response({ amount: { type: 'number' }, currency: text }), { type: 'null' }] } }), { type: 'null' }] },
|
|
203
|
-
costUsd: { anyOf: [{ type: 'number' }, { type: 'null' }] }, sessionId: { anyOf: [text, { type: 'null' }] }, trashedAt: { anyOf: [text, { type: 'null' }] }
|
|
205
|
+
costUsd: { anyOf: [{ type: 'number' }, { type: 'null' }] }, sessionId: { anyOf: [text, { type: 'null' }] }, trashedAt: { anyOf: [text, { type: 'null' }] },
|
|
206
|
+
origin: { anyOf: [response({ producer: text, where: text, copyOf: text }, ['producer', 'where']), { type: 'null' }] } }, ['conversationId', 'title', 'client', 'principal', 'createdAt', 'updatedAt', 'version', 'turns', 'state', 'lastAt']);
|
|
204
207
|
const sessionObservation = ownerSchema('agent-session', 'sessionObservationSchema');
|
|
205
208
|
// What one Agent offered, from a check or a conversation's first session, and the person's choices.
|
|
206
209
|
const offered = response({ at: text, source: { enum: ['check', 'conversation'] }, adapter: { anyOf: [response({ name: text, version: text }, ['name']), { type: 'null' }] }, options: array(ownerSchema('agent-session', 'configOptionSchema')), omitted: integer });
|
|
207
210
|
const agentSettings = response({ version: integer, updatedAt: { anyOf: [text, { type: 'null' }] }, backend: { anyOf: [agentBackend, { type: 'null' }] }, serviceBackend: { enum: ['claude-acp', 'codex-acp', 'claude-cli', 'acp'] },
|
|
208
211
|
choices: response({ 'claude-acp': choices, 'codex-acp': choices, 'acp': choices }, []), offered: response({ 'claude-acp': offered, 'codex-acp': offered, 'acp': offered }, []), checking: array(agentBackend),
|
|
209
212
|
// Whether a model describes the files a message writes; the facts are read either way.
|
|
210
|
-
describe: response({ enabled: { type: 'boolean' } })
|
|
213
|
+
describe: response({ enabled: { type: 'boolean' } }),
|
|
214
|
+
// Which Agents this service can start, without starting them.
|
|
215
|
+
agents: response(Object.fromEntries(['claude-acp', 'codex-acp', 'acp'].map(k => [k, response({ found: { type: 'boolean' }, problem: text, name: { anyOf: [text, { type: 'null' }] } }, ['found'])]))) }, ['version', 'updatedAt', 'backend', 'serviceBackend', 'choices', 'offered', 'checking']);
|
|
211
216
|
// A tool step keeps its original identity, the Agent's bounded human title, its ACP
|
|
212
217
|
// category and one identifying input (command, path, pattern or query), for a command the
|
|
213
218
|
// Agent's own few words on what it is for (purpose), when it was
|
|
@@ -333,6 +338,7 @@ export const responses = { ...base.responses, ...ownerResponses, ...extensionRes
|
|
|
333
338
|
'conversation.settings.update': agentSettings,
|
|
334
339
|
'conversation.settings.check': agentSettings,
|
|
335
340
|
'session.connect': response({ principal: text, label: text, methods: integer, granted: { type: 'boolean' }, added: addedTo }),
|
|
341
|
+
'session.instructions': response({ request: text, prompt: text, command: array(text) }),
|
|
336
342
|
'protocol.describe': response({ apiVersion: { const: '2.0' }, schemaVersion: { const: 2 }, revision: digest, entries: array(response({ method: text, summary: text })), next: { type: ['string', 'null'] }, complete: { type: 'boolean' }, contract: response({ method: text, inputSchema: { type: 'object' }, outputSchema: { type: 'object' }, definitions: { type: 'object' }, description: text }) }, ['apiVersion', 'schemaVersion', 'revision', 'entries', 'next', 'complete']),
|
|
337
343
|
'capability.list': { ...catalog, properties: { ...catalog.properties, host: contract.cliSchema }, required: [...(catalog.required ?? []), 'host'] },
|
|
338
344
|
'capability.configure': response({ version: positive, enabled: array(text), receipt: response({ changeId: text, author: text, version: positive, enabled: array(text), at: text }) }),
|
|
@@ -395,7 +401,7 @@ export const descriptions = {
|
|
|
395
401
|
'action.submit': 'Execute the explicitly selected project R script snapshot and requirements. Persist operationId before delivery; identical retries retain the original Core identity. Output directory is the first R argument, followed by explicit args or frozen resource paths. Artifact paths are output-relative; completion is run-directory-relative, returned as plan.completion, so an inventory can remain beside immutable producer artifacts.',
|
|
396
402
|
'binding.activity.append': 'Registered native adapter only: retain original CLI tool and artifact-ready observations. This does not execute, validate, or present artifacts.',
|
|
397
403
|
'task.routing.put': 'Control plane only: conditionally assign one task and business binding to this project without starting a native session.',
|
|
398
|
-
'conversation.start': 'Person at this machine only: start a conversation with the selected Claude Code or Codex Agent in this project. Rho selects the Agent plugin backend, attaches this project\'s MCP grant and records the message before sending it. Without backend or config, the person\'s saved Agent and settings are used and recorded with the conversation. A repeated changeId with the same text returns the original conversation and starts nothing.',
|
|
404
|
+
'conversation.start': 'Person at this machine only: start a conversation with the selected Claude Code or Codex Agent in this project. Rho selects the Agent plugin backend, attaches this project\'s MCP grant and records the message before sending it. Without backend or config, the person\'s saved Agent and settings are used and recorded with the conversation. A repeated changeId with the same text returns the original conversation and starts nothing. With continue, the message continues an Agent session listed by agent.sessions.list (one started in a terminal, an editor or the Agent\'s own app) through that same session, or through a copy of it with copy; a session another live window holds open is refused unless copied.',
|
|
399
405
|
'conversation.send': 'Person at this machine only: send the next message to a conversation started in Rho. A continuationOf links a new message to the latest confirmed Agent turn-limit stop; the original request is not resent. Refused while the previous message is being answered. A repeated changeId with the same input returns the original message.',
|
|
400
406
|
'conversation.decide': 'Person at this machine only: say to use an answer, or that it needs changes and why. The decision holds for the file versions that message kept when it was given and says nothing of later versions. A new decision supersedes the one before; none is removed. Refused while the Agent is still answering.',
|
|
401
407
|
'conversation.stop': 'Person at this machine only: ask the original Agent to stop answering one message. ACP cancellation is confirmed by the original prompt response; a lost connection keeps the outcome unknown. Legacy CLI conversations confirm process exit. Steps already taken stay done.',
|
|
@@ -432,6 +438,7 @@ export const descriptions = {
|
|
|
432
438
|
'conversation.settings.update': 'Person at this machine only: choose the Agent and its settings for new conversations, among the values that Agent last offered, and whether a model describes the files a message writes. A stale expectedVersion is refused.',
|
|
433
439
|
'conversation.settings.check': 'Person at this machine only: start the chosen Agent in this project to read the settings it offers, then remove that session and end the process. No message is sent; one check per Agent runs at a time.',
|
|
434
440
|
'session.connect': 'Person at this machine only: give Claude Code or Codex running outside Rho access to this project, then add Rho\'s MCP server with that Agent\'s own `mcp add` (Claude Code: for this project folder; Codex: in its user configuration). A grant whose connection still reaches this service is kept, so a connected session keeps working; a new grant the Agent refuses is removed. No message is sent and no Agent session starts.',
|
|
441
|
+
'session.instructions': 'Person at this machine or operator only: read a prompt for the person\'s own MCP client to connect itself to this existing project. Returns local launcher paths, never credential contents. Reading or copying grants no access, edits no client configuration and starts no runtime.',
|
|
435
442
|
'conversation.read': 'Read a conversation started in Rho: the person\'s messages, the Agent\'s visible text and tool steps, and how each message ended. Reading starts and resumes nothing.',
|
|
436
443
|
'project.work.read': 'Person at this machine only: read linked Agent sessions, retained native activity identities, current referenced files and the shared notes document. Source cursors and resource versions remain independent. Does not start an Agent, collector or Host.',
|
|
437
444
|
'resource.register': 'Declare scoped existing bytes and their type/schema at the supplied digest. This read and registration starts no runtime.',
|
package/dist/server.d.ts
CHANGED
|
@@ -168,22 +168,14 @@ export declare class RhoService {
|
|
|
168
168
|
agentConnection(projectId: string, externalSessionId: string, file: string, methods?: string[], pluginFilter?: string[], profile?: 'layered-business' | 'layered-presentation', who?: {
|
|
169
169
|
label?: string;
|
|
170
170
|
conversationId?: string;
|
|
171
|
-
}): Promise<
|
|
172
|
-
apiVersion: "2.0";
|
|
173
|
-
pluginFilter?: ("media" | "r" | "agent" | "files" | "skills" | "documents" | "requirements" | "tables" | "charts" | "annotations" | "console" | "kernel")[] | undefined;
|
|
174
|
-
profile?: "layered-business" | "layered-presentation" | undefined;
|
|
175
|
-
url: string;
|
|
176
|
-
token: string;
|
|
177
|
-
projectId: string;
|
|
178
|
-
externalSessionId: string;
|
|
179
|
-
methods: string[];
|
|
180
|
-
}>;
|
|
171
|
+
}): Promise<any>;
|
|
181
172
|
private authenticate;
|
|
182
173
|
private handle;
|
|
183
174
|
private readBody;
|
|
184
175
|
private json;
|
|
185
176
|
private serial;
|
|
186
177
|
private recordActivity;
|
|
178
|
+
private agentSessions;
|
|
187
179
|
private projectActivity;
|
|
188
180
|
private agentSummaries;
|
|
189
181
|
private dataDir;
|
package/dist/server.js
CHANGED
|
@@ -19,7 +19,7 @@ import { Interactions } from './interactions.js';
|
|
|
19
19
|
import { TaskMap } from './task-map.js';
|
|
20
20
|
import { Capabilities } from './capabilities.js';
|
|
21
21
|
import { BrowserAccess, localBrowserId, localBrowserPrincipal } from './browser-access.js';
|
|
22
|
-
import { addToAgent, connectLimits, mcpCommand, mcpName, outsideLabel } from './agent-connect.js';
|
|
22
|
+
import { addToAgent, connectionInstructions, connectLimits, mcpCommand, mcpName, outsideLabel } from './agent-connect.js';
|
|
23
23
|
import { NativeFolderPicker, nativeFolderLimits } from './native-folders.js';
|
|
24
24
|
import { homedir } from 'node:os';
|
|
25
25
|
import { serveWeb, servePageFrame, pageFramePath, extensionFramePath, webRoot, installedWebEntries } from './web-assets.js';
|
|
@@ -452,6 +452,7 @@ export class RhoService {
|
|
|
452
452
|
return [await writeClaudePlugin(path.join(this.state.directory, 'claude-plugins'), { name: 'rho', version: selectedPluginVersions.agent, description: 'Skills and hooks of the plugins selected in Rho.', ...declared })];
|
|
453
453
|
},
|
|
454
454
|
changed: async (project, conversationId) => { void this.serial(project.id, () => this.emit(project, 'conversation.changed', conversationId, localBrowserId)).catch(() => { }); },
|
|
455
|
+
nativeSession: async (project, producer, sessionId) => (await import('@rho-workspace/plugin-agent/native-sessions')).nativeSession(project.root, producer, sessionId),
|
|
455
456
|
// The person's own Claude Code when one is named, with no tools and in an empty folder of
|
|
456
457
|
// Rho's state, so it reads only what it is given and loads no project instructions. What
|
|
457
458
|
// a call cost comes from Claude Code when it knows the model's price, else from models.dev.
|
|
@@ -570,11 +571,22 @@ export class RhoService {
|
|
|
570
571
|
requireThat(selected.every(m => !extensionManagement.includes(m) && !!operations[m] && !m.startsWith('agent.sessions.') && !m.startsWith('session.') && !m.startsWith('conversation.') && (!m.startsWith('project.') || m === 'project.entries') && !['task.routing.put', 'console.run', 'console.stop', 'kernel.stop'].includes(m)), 'invalid', 'Unknown or operator-only Agent capability.');
|
|
571
572
|
requireThat(await Promise.all(selected.map(method => capabilities.isAvailable(method, state))).then(values => values.every(Boolean)), 'unavailable', 'A requested Agent operation has no selected provider.');
|
|
572
573
|
requireThat(!profile || new Set(['capability.index', 'capability.resolve', ...selected]).size <= layeredGrantLimits.maxMethods, 'limit', `Layered connection methods exceed the ${layeredGrantLimits.maxMethods}-method profile; choose an explicit narrower grant.`, { limit: 'maxMethods', maximum: layeredGrantLimits.maxMethods });
|
|
573
|
-
const token = randomBytes(32).toString('hex');
|
|
574
574
|
const id = 'agent:' + digest(projectId + ':' + externalSessionId).slice(0, 32);
|
|
575
|
+
const grantedMethods = [...new Set([...(profile ? ['capability.index', 'capability.resolve'] : ['protocol.describe', 'capability.list']), ...selected])];
|
|
576
|
+
const existing = this.state.principals.find(p => p.id === id);
|
|
577
|
+
const kept = await fs.readFile(file, 'utf8').then(text => JSON.parse(text), () => null);
|
|
578
|
+
// Repeating the same setup must not disconnect a client already using its grant.
|
|
579
|
+
// Changed scope, expired access or a stale address still requires a fresh credential.
|
|
580
|
+
if (existing && !(existing.expiresAt && Date.parse(existing.expiresAt) <= Date.now())
|
|
581
|
+
&& kept?.url === this.url && kept.projectId === projectId && kept.externalSessionId === externalSessionId
|
|
582
|
+
&& typeof kept.token === 'string' && digest(kept.token) === existing.tokenHash
|
|
583
|
+
&& canonical([...existing.methods].sort()) === canonical([...grantedMethods].sort())
|
|
584
|
+
&& kept.profile === profile && canonical(existing.pluginFilter ?? []) === canonical(pluginFilter ?? []))
|
|
585
|
+
return kept;
|
|
586
|
+
const token = randomBytes(32).toString('hex');
|
|
575
587
|
this.state.principals = this.state.principals.filter(p => p.id !== id);
|
|
576
588
|
requireThat(!pluginFilter || pluginFilter.every(id => this.plugins.isKnown(id)), 'invalid', 'Unknown MCP plugin filter.');
|
|
577
|
-
await this.state.addPrincipal({ id, tokenHash: digest(token), projectId, label: who.label ?? externalSessionId, ...(who.conversationId ? { conversationId: who.conversationId } : {}), methods:
|
|
589
|
+
await this.state.addPrincipal({ id, tokenHash: digest(token), projectId, label: who.label ?? externalSessionId, ...(who.conversationId ? { conversationId: who.conversationId } : {}), methods: grantedMethods, ...(profile ? { profile } : {}), ...(pluginFilter ? { pluginFilter } : {}) });
|
|
578
590
|
const connection = { url: this.url, token, projectId, externalSessionId, methods: [...new Set([...(profile ? ['capability.index', 'capability.resolve'] : []), ...selected])], ...(profile ? { profile } : {}), ...(pluginFilter ? { pluginFilter } : {}), apiVersion: '2.0' };
|
|
579
591
|
await atomicJson(file, connection);
|
|
580
592
|
return connection;
|
|
@@ -785,6 +797,32 @@ export class RhoService {
|
|
|
785
797
|
// What the project's Agents did, for the person's screen:
|
|
786
798
|
// only principals whose grant names this project, only the fields the activity record
|
|
787
799
|
// keeps. A grant is the scope; an Agent need not have opened a binding.
|
|
800
|
+
// Every Agent session in the project folder, wherever it was started: Harness conversations Rho
|
|
801
|
+
// observes and the sessions Claude Code and Codex keep in their own files. Each names the Agent
|
|
802
|
+
// Rho continues it with and the Rho conversation that already continues it. Reads files only.
|
|
803
|
+
async agentSessions(p) {
|
|
804
|
+
const agent = this.plugins.optional('agent');
|
|
805
|
+
const external = agent ? agent.listExternalSessions(p.root) : { sessions: [], omitted: 0 };
|
|
806
|
+
const native = (await import('@rho-workspace/plugin-agent/native-sessions')).listNativeSessions(p.root);
|
|
807
|
+
const conversations = [...await this.conversations.list(p, {}), ...await this.conversations.list(p, { trashed: true })];
|
|
808
|
+
const bySession = new Map(conversations.filter(c => c.sessionId).map(c => [c.sessionId, c]));
|
|
809
|
+
const sessions = [...external.sessions, ...native.sessions].flatMap(s => {
|
|
810
|
+
const conversation = bySession.get(s.sessionId);
|
|
811
|
+
if (conversation?.trashedAt)
|
|
812
|
+
return [];
|
|
813
|
+
const continueWith = s.producer === 'claude-code' ? 'claude-acp' : s.producer === 'codex' ? 'codex-acp' : s.producer === 'dsh' && this.config.acpCommand ? 'acp' : null;
|
|
814
|
+
// Harness reports a turn in progress, not whether a window holds the session idle; a turn
|
|
815
|
+
// Rho itself is running is Rho's.
|
|
816
|
+
const open = s.producer === 'dsh' && s.state === 'running' && conversation?.state !== 'running' ? { where: 'DeepSeek Harness' } : s.open;
|
|
817
|
+
return [{ ...s, continueWith, ...(open !== undefined ? { open } : {}), ...(conversation ? { conversationId: conversation.conversationId } : {}) }];
|
|
818
|
+
});
|
|
819
|
+
// A session held only by Rho's own connection is not open elsewhere.
|
|
820
|
+
for (const s of sessions)
|
|
821
|
+
if (s.open && s.conversationId && await this.conversations.owns(p, s.conversationId, s.open.pid))
|
|
822
|
+
s.open = null;
|
|
823
|
+
sessions.sort((a, b) => b.lastAt.localeCompare(a.lastAt));
|
|
824
|
+
return { sessions: sessions.slice(0, 128), omitted: external.omitted + native.omitted + Math.max(0, sessions.length - 128) };
|
|
825
|
+
}
|
|
788
826
|
async projectActivity(p) {
|
|
789
827
|
const agent = this.plugins.project(p.root, p.id).agent;
|
|
790
828
|
if (!agent)
|
|
@@ -1156,10 +1194,15 @@ export class RhoService {
|
|
|
1156
1194
|
await this.emit(p, 'composition.changed', b.compositionId);
|
|
1157
1195
|
return { document, viewId, composition };
|
|
1158
1196
|
}
|
|
1197
|
+
if (method === 'session.instructions') {
|
|
1198
|
+
requireThat(principal.id === localBrowserId || principal.id === 'operator', 'unauthorized', 'Only the person at this machine can read Agent setup instructions.');
|
|
1199
|
+
return connectionInstructions(p, this.state.directory);
|
|
1200
|
+
}
|
|
1159
1201
|
if (method === 'session.grant') {
|
|
1160
1202
|
requireThat(principal.id === 'operator', 'unauthorized', 'Only the local operator may provision a project-scoped session.');
|
|
1161
1203
|
const file = path.join(this.state.directory, 'agent-' + digest(p.id + ':' + b.externalSessionId) + '.json');
|
|
1162
1204
|
const connection = await this.agentConnection(p.id, b.externalSessionId, file, b.methods, undefined, b.profile);
|
|
1205
|
+
await this.emit(p, 'activity.changed', method);
|
|
1163
1206
|
return { profile: connection.profile, connectionFile: file, projectId: p.id, methods: connection.methods, transport: 'stdio-mcp', externalSessionId: b.externalSessionId };
|
|
1164
1207
|
}
|
|
1165
1208
|
// The person's own Claude Code or Codex, outside Rho: the same grant the operator gives, kept
|
|
@@ -1190,11 +1233,16 @@ export class RhoService {
|
|
|
1190
1233
|
// The person at this machine talks to a conversation Rho started; Agents and narrower links
|
|
1191
1234
|
// cannot start one, and an Agent started this way cannot start another.
|
|
1192
1235
|
if (method.startsWith('agent.sessions.')) {
|
|
1193
|
-
requireThat(principal.id === localBrowserId, 'unauthorized', '
|
|
1194
|
-
const agent = this.plugins.need('agent');
|
|
1236
|
+
requireThat(principal.id === localBrowserId, 'unauthorized', 'Agent session history belongs to the person at this machine.');
|
|
1195
1237
|
if (method === 'agent.sessions.list')
|
|
1196
|
-
return
|
|
1197
|
-
|
|
1238
|
+
return this.agentSessions(p);
|
|
1239
|
+
const listed = (await this.agentSessions(p)).sessions.find(s => s.id === b.session);
|
|
1240
|
+
requireThat(listed, 'not_found', 'That Agent session is not in this project.');
|
|
1241
|
+
const read = listed.producer === 'claude-code' || listed.producer === 'codex'
|
|
1242
|
+
? await (await import('@rho-workspace/plugin-agent/native-sessions')).readNativeSession(p.root, b)
|
|
1243
|
+
: this.plugins.need('agent').readExternalSession(p.root, b);
|
|
1244
|
+
requireThat(read, 'not_found', 'That Agent session is no longer in this project.');
|
|
1245
|
+
return { ...read, session: { ...read.session, ...listed } };
|
|
1198
1246
|
}
|
|
1199
1247
|
if (method === 'project.work.read') {
|
|
1200
1248
|
requireThat(principal.id === localBrowserId, 'scope', 'Project work is available to the person at this machine.');
|
|
@@ -1235,10 +1283,25 @@ export class RhoService {
|
|
|
1235
1283
|
if (method === 'conversation.settings.read')
|
|
1236
1284
|
return this.conversations.settings();
|
|
1237
1285
|
this.plugins.need('agent');
|
|
1286
|
+
if (method === 'conversation.start' && b.continue) {
|
|
1287
|
+
const listed = (await this.agentSessions(p)).sessions.find(s => s.id === b.continue.session);
|
|
1288
|
+
requireThat(listed, 'not_found', 'That Agent session is not in this project. Nothing was sent.');
|
|
1289
|
+
requireThat(listed.continueWith, 'condition', listed.producer === 'dsh'
|
|
1290
|
+
? 'Rho continues DeepSeek Harness sessions through Harness\'s ACP command. Start Rho with --agent-backend acp --acp-command \'["dsh","acp"]\', or continue in Harness. Nothing was sent.'
|
|
1291
|
+
: 'Rho cannot continue this Agent\'s sessions. Nothing was sent.');
|
|
1292
|
+
requireThat(!listed.conversationId, 'conflict', 'This session already continues as a conversation here. Open it to send your message.');
|
|
1293
|
+
requireThat(!b.continue.copy || listed.producer !== 'dsh', 'condition', 'DeepSeek Harness cannot copy a session. Continue it in place once it is not answering. Nothing was sent.');
|
|
1294
|
+
requireThat(!listed.open || listed.producer !== 'dsh', 'condition', 'DeepSeek Harness is answering in this session now. Continue it once that turn ends. Nothing was sent.', { heldBy: 'DeepSeek Harness' });
|
|
1295
|
+
return this.conversations.start(p, principal.id, { ...b, continue: { producer: listed.producer, sessionId: listed.sessionId, title: listed.title, where: listed.where ?? (listed.producer === 'dsh' ? 'DeepSeek Harness' : 'another window'), copy: !!b.continue.copy, backend: listed.continueWith } });
|
|
1296
|
+
}
|
|
1238
1297
|
if (method === 'conversation.start')
|
|
1239
1298
|
return this.conversations.start(p, principal.id, b);
|
|
1240
|
-
if (method === 'conversation.send')
|
|
1299
|
+
if (method === 'conversation.send') {
|
|
1300
|
+
// A Harness session reports its own turns through its adapter; Rho does not write into one it is answering.
|
|
1301
|
+
const listed = (await this.agentSessions(p)).sessions.find(s => s.conversationId === b.conversationId && s.producer === 'dsh');
|
|
1302
|
+
requireThat(!listed?.open, 'condition', 'DeepSeek Harness is answering in this session now. Send once that turn ends. Nothing was sent.', { heldBy: 'DeepSeek Harness' });
|
|
1241
1303
|
return this.conversations.send(p, principal.id, b);
|
|
1304
|
+
}
|
|
1242
1305
|
if (method === 'conversation.configure')
|
|
1243
1306
|
return this.conversations.configure(p, principal.id, b);
|
|
1244
1307
|
if (method === 'conversation.settings.update')
|