@rho-workspace/rho 0.5.4 → 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.
Files changed (161) hide show
  1. package/dist/agent-connect.d.ts +8 -0
  2. package/dist/agent-connect.js +15 -0
  3. package/dist/browser-access.js +3 -2
  4. package/dist/capabilities.d.ts +2 -2
  5. package/dist/capabilities.js +6 -1
  6. package/dist/cli.js +219 -176
  7. package/dist/contract.generated.d.ts +472 -4
  8. package/dist/contract.generated.js +1 -1
  9. package/dist/contracts/owners.generated.js +455 -6
  10. package/dist/conversations.d.ts +157 -4
  11. package/dist/conversations.js +69 -6
  12. package/dist/discovery.d.ts +1 -0
  13. package/dist/discovery.js +3 -3
  14. package/dist/extension-contract.d.ts +99 -0
  15. package/dist/extension-contract.js +52 -0
  16. package/dist/extension-directory.d.ts +9 -0
  17. package/dist/extension-directory.js +11 -0
  18. package/dist/extension-worker.d.ts +1 -0
  19. package/dist/extension-worker.js +36 -0
  20. package/dist/extensions.d.ts +157 -0
  21. package/dist/extensions.js +349 -0
  22. package/dist/plugin-cli.d.ts +1 -0
  23. package/dist/plugin-cli.js +139 -0
  24. package/dist/plugin-inventory.json +18 -0
  25. package/dist/plugins.d.ts +3 -3
  26. package/dist/project-work.d.ts +3 -1
  27. package/dist/project-work.js +15 -2
  28. package/dist/protocol.d.ts +8 -0
  29. package/dist/protocol.js +16 -7
  30. package/dist/server.d.ts +5 -10
  31. package/dist/server.js +122 -12
  32. package/dist/web/.vite/manifest.json +67 -67
  33. package/dist/web/assets/{canvas-BXhC8RDZ.js → canvas-D8rDdfDz.js} +1 -1
  34. package/dist/web/assets/choices-D-YfnFP6.js +1 -0
  35. package/dist/web/assets/{console-Dbdo-5oO.js → console-Trqc0swD.js} +1 -1
  36. package/dist/web/assets/{dist-B2V0VDO0.js → dist-DZmszrjG.js} +2 -2
  37. package/dist/web/assets/dist-DpIHRk3B.js +1 -0
  38. package/dist/web/assets/{dist-D25Vn17i.js → dist-eMx6b8N1.js} +1 -1
  39. package/dist/web/assets/dist-pF902jHA.js +3 -0
  40. package/dist/web/assets/explore-in-BCHqjJ7B.js +1 -0
  41. package/dist/web/assets/graph-lENgfQNH.js +2 -0
  42. package/dist/web/assets/index-B3_OVy91.css +1 -0
  43. package/dist/web/assets/{index-BHQIaDsx.js → index-BpL_Kl_s.js} +87 -86
  44. package/dist/web/assets/{notes-meaning-DdPbtNzJ.js → notes-meaning-DcVGUBBY.js} +1 -1
  45. package/dist/web/assets/overview-CDnVKlOt.js +6 -0
  46. package/dist/web/assets/preview-BEC1E9MK.js +1 -0
  47. package/dist/web/assets/{review-CmWou4Iv.js → review-DYP0aCXb.js} +1 -1
  48. package/dist/web/assets/{skills-Dy7n3L9d.js → skills-1ec0Z-jx.js} +1 -1
  49. package/dist/web/assets/{skills-work-Cj0W6b-I.js → skills-work-DNexV4OJ.js} +1 -1
  50. package/dist/web/assets/{studio-process-QOY_n2_8.js → studio-process-EYQgikxq.js} +2 -2
  51. package/dist/web/assets/{timeline-events-DmX1BCh3.js → timeline-events-Chj2Gbx7.js} +1 -1
  52. package/dist/web/assets/{work-turns-Co0C71rJ.js → work-turns-CT9RVcWY.js} +1 -1
  53. package/dist/web/assets/workspace-COmCbGV3.js +5 -0
  54. package/dist/web/composition.json +65 -65
  55. package/dist/web/index.html +3 -3
  56. package/dist/web/renderer.json +1 -1
  57. package/dist/web-assets.d.ts +2 -1
  58. package/dist/web-assets.js +3 -2
  59. package/dist/work-contract.js +2 -2
  60. package/docs/FRONTEND.md +26 -18
  61. package/docs/HEADLESS.md +38 -28
  62. package/docs/PLUGIN-AUTHORING.md +4 -0
  63. package/docs/THIRD-PARTY-PLUGINS.md +190 -0
  64. package/node_modules/@rho-workspace/plugin-agent/README.md +68 -0
  65. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +1 -0
  66. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +9 -1
  67. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
  68. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +10 -0
  69. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.d.ts +1 -0
  70. package/node_modules/@rho-workspace/plugin-agent/dist/activity-normalize.js +2 -2
  71. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.d.ts +6 -0
  72. package/node_modules/@rho-workspace/plugin-agent/dist/dsh-session.js +174 -0
  73. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.d.ts +82 -0
  74. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +20 -0
  75. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +84 -0
  76. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.js +190 -0
  77. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +373 -0
  78. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +1 -0
  79. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
  80. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
  81. package/node_modules/@rho-workspace/plugin-agent/dist/operations.d.ts +33 -0
  82. package/node_modules/@rho-workspace/plugin-agent/dist/operations.js +3 -0
  83. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.d.ts +14 -0
  84. package/node_modules/@rho-workspace/plugin-agent/dist/presentation.js +2 -0
  85. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.d.ts +15 -0
  86. package/node_modules/@rho-workspace/plugin-agent/dist/ui-external-session.js +37 -0
  87. package/node_modules/@rho-workspace/plugin-agent/package.json +6 -1
  88. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
  89. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
  90. package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
  91. package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
  92. package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
  93. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
  94. package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
  95. package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
  96. package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
  97. package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
  98. package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
  99. package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
  100. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
  101. package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
  102. package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
  103. package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
  104. package/node_modules/hono/dist/cjs/utils/url.js +1 -1
  105. package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
  106. package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
  107. package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
  108. package/node_modules/hono/dist/jsx/base.js +49 -32
  109. package/node_modules/hono/dist/jsx/components.js +2 -2
  110. package/node_modules/hono/dist/jsx/constants.js +2 -1
  111. package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
  112. package/node_modules/hono/dist/middleware/etag/index.js +3 -1
  113. package/node_modules/hono/dist/middleware/language/language.js +1 -1
  114. package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
  115. package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
  116. package/node_modules/hono/dist/utils/cookie.js +2 -2
  117. package/node_modules/hono/dist/utils/mime.js +1 -1
  118. package/node_modules/hono/dist/utils/url.js +1 -1
  119. package/node_modules/hono/package.json +1 -1
  120. package/package.json +5 -3
  121. package/protocol/index.json +93 -33
  122. package/protocol/operations/agent.sessions.list.json +146 -0
  123. package/protocol/operations/agent.sessions.read.json +315 -0
  124. package/protocol/operations/conversation.configure.json +29 -0
  125. package/protocol/operations/conversation.decide.json +29 -0
  126. package/protocol/operations/conversation.list.json +29 -0
  127. package/protocol/operations/conversation.permission.json +29 -0
  128. package/protocol/operations/conversation.read.json +29 -0
  129. package/protocol/operations/conversation.restore.json +29 -0
  130. package/protocol/operations/conversation.send.json +29 -0
  131. package/protocol/operations/conversation.settings.check.json +92 -0
  132. package/protocol/operations/conversation.settings.read.json +92 -0
  133. package/protocol/operations/conversation.settings.update.json +92 -0
  134. package/protocol/operations/conversation.start.json +46 -1
  135. package/protocol/operations/conversation.stop.json +29 -0
  136. package/protocol/operations/conversation.trash.json +29 -0
  137. package/protocol/operations/extension.call.json +132 -0
  138. package/protocol/operations/extension.content.json +78 -0
  139. package/protocol/operations/extension.describe.json +195 -0
  140. package/protocol/operations/extension.install.json +198 -0
  141. package/protocol/operations/extension.list.json +123 -0
  142. package/protocol/operations/extension.manage.json +55 -0
  143. package/protocol/operations/extension.read.json +112 -0
  144. package/protocol/operations/observation.snapshot.json +29 -0
  145. package/protocol/operations/project.work.read.json +3 -2
  146. package/protocol/operations/session.grant.json +9 -0
  147. package/protocol/operations/session.instructions.json +43 -0
  148. package/scripts/third-party-acceptance.mjs +77 -0
  149. package/vendor/composition.json +83 -83
  150. package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
  151. package/web/theme.css +12 -0
  152. package/dist/web/assets/choices-DU9kHlyQ.js +0 -1
  153. package/dist/web/assets/dist-BV3hWfn_.js +0 -3
  154. package/dist/web/assets/dist-CLNF2g-E.js +0 -1
  155. package/dist/web/assets/explore-in-8Q5ftt-E.js +0 -1
  156. package/dist/web/assets/graph-hS2kvHTI.js +0 -2
  157. package/dist/web/assets/index-Cj1_iqyK.css +0 -1
  158. package/dist/web/assets/overview-BV6ZO5TO.js +0 -6
  159. package/dist/web/assets/preview-n5lOOdjN.js +0 -1
  160. package/dist/web/assets/workspace-BKFefRfJ.js +0 -5
  161. package/vendor/rho-workspace-plugin-agent-0.4.28.tgz +0 -0
@@ -0,0 +1,82 @@
1
+ export declare const externalSessionLimits: {
2
+ maxSessions: number;
3
+ maxTurns: number;
4
+ maxTools: number;
5
+ maxRecordBytes: number;
6
+ maxEvents: number;
7
+ maxSourceBytes: number;
8
+ maxTextChars: number;
9
+ maxDetailChars: number;
10
+ };
11
+ export declare const externalResultSchema: {
12
+ type: string;
13
+ properties: Record<string, any>;
14
+ required: string[];
15
+ additionalProperties: boolean;
16
+ };
17
+ export declare const externalToolSchema: {
18
+ type: string;
19
+ properties: Record<string, any>;
20
+ required: string[];
21
+ additionalProperties: boolean;
22
+ };
23
+ export declare const externalTurnSchema: {
24
+ type: string;
25
+ properties: Record<string, any>;
26
+ required: string[];
27
+ additionalProperties: boolean;
28
+ };
29
+ export declare const externalSessionSummarySchema: {
30
+ type: string;
31
+ properties: Record<string, any>;
32
+ required: string[];
33
+ additionalProperties: boolean;
34
+ };
35
+ export declare const externalSessionSchemas: {
36
+ summary: {
37
+ type: string;
38
+ properties: Record<string, any>;
39
+ required: string[];
40
+ additionalProperties: boolean;
41
+ };
42
+ turn: {
43
+ type: string;
44
+ properties: Record<string, any>;
45
+ required: string[];
46
+ additionalProperties: boolean;
47
+ };
48
+ };
49
+ export declare const externalSessionOperations: {
50
+ 'agent.sessions.list': {
51
+ inputSchema: {
52
+ type: string;
53
+ properties: Record<string, any>;
54
+ required: string[];
55
+ additionalProperties: boolean;
56
+ };
57
+ outputSchema: {
58
+ type: string;
59
+ properties: Record<string, any>;
60
+ required: string[];
61
+ additionalProperties: boolean;
62
+ };
63
+ requires: never[];
64
+ effect: "read";
65
+ };
66
+ 'agent.sessions.read': {
67
+ inputSchema: {
68
+ type: string;
69
+ properties: Record<string, any>;
70
+ required: string[];
71
+ additionalProperties: boolean;
72
+ };
73
+ outputSchema: {
74
+ type: string;
75
+ properties: Record<string, any>;
76
+ required: string[];
77
+ additionalProperties: boolean;
78
+ };
79
+ requires: never[];
80
+ effect: "read";
81
+ };
82
+ };
@@ -0,0 +1,20 @@
1
+ // A conversation an Agent holds in its own record: a retained observation of another client's
2
+ // conversation, or a session the Agent keeps in its own files. Reading it starts nothing.
3
+ const text = { type: 'string' }, count = { type: 'integer', minimum: 0 };
4
+ const obj = (properties, required = Object.keys(properties)) => ({ type: 'object', properties, required, additionalProperties: false });
5
+ const array = (items, maxItems) => ({ type: 'array', items, maxItems });
6
+ export const externalSessionLimits = { maxSessions: 64, maxTurns: 200, maxTools: 256, maxRecordBytes: 4194304, maxEvents: 32768, maxSourceBytes: 33554432, maxTextChars: 32768, maxDetailChars: 8192 };
7
+ const state = { enum: ['running', 'completed', 'failed', 'stopped', 'unknown'] };
8
+ export const externalResultSchema = obj({ path: text, id: text, version: text, projectId: text, toolId: text }, ['path', 'toolId']);
9
+ export const externalToolSchema = obj({ id: text, at: text, name: text, detail: text, output: text, outcome: { enum: ['requested', 'returned', 'failed', 'unknown'] }, exitCode: { type: 'integer' }, paths: array(text, 32) }, ['id', 'at', 'name', 'detail', 'output', 'outcome', 'paths']);
10
+ export const externalTurnSchema = obj({ id: text, at: text, endedAt: text, state, prompt: text, reply: text, messages: array(obj({ at: text, text }), 200), tools: array(externalToolSchema, externalSessionLimits.maxTools), results: array(externalResultSchema, 64), omittedTools: count, omittedChars: count, where: text }, ['id', 'at', 'state', 'prompt', 'reply', 'messages', 'tools', 'results', 'omittedTools', 'omittedChars']);
11
+ export const externalSessionSummarySchema = obj({ id: { type: 'string', pattern: '^x-[a-f0-9]{64}$' }, producer: text, sessionId: text, title: text, lastAt: text, observedAt: text, state, turns: count, sourceCursor: count, omittedTurns: count, problem: text, inputOwner: { const: 'external' },
12
+ // Native sessions: where it was started, a live process holding it, the command that continues it in a terminal
13
+ // and its size. The application adds the Agent it continues with in Rho and the Rho conversation that continued it.
14
+ where: text, open: { anyOf: [{ type: 'null' }, obj({ where: text, since: text, pid: count }, ['where'])] }, resume: text, bytes: count,
15
+ continueWith: { enum: ['claude-acp', 'codex-acp', 'acp', null] }, conversationId: text }, ['id', 'producer', 'sessionId', 'title', 'lastAt', 'observedAt', 'state', 'turns', 'sourceCursor', 'omittedTurns']);
16
+ export const externalSessionSchemas = { summary: externalSessionSummarySchema, turn: externalTurnSchema };
17
+ export const externalSessionOperations = {
18
+ 'agent.sessions.list': { inputSchema: obj({}), outputSchema: obj({ sessions: array(externalSessionSummarySchema, 128), omitted: count }), requires: [], effect: 'read' },
19
+ 'agent.sessions.read': { inputSchema: obj({ session: { type: 'string', pattern: '^x-[a-f0-9]{64}$' }, before: count, limit: { type: 'integer', minimum: 1, maximum: 50 } }, ['session']), outputSchema: obj({ session: externalSessionSummarySchema, first: count, turns: array(externalTurnSchema, 50) }), requires: [], effect: 'read' },
20
+ };
@@ -0,0 +1,84 @@
1
+ export { externalSessionLimits } from './external-session-contract.js';
2
+ export interface ExternalTool {
3
+ id: string;
4
+ at: string;
5
+ name: string;
6
+ detail: string;
7
+ output: string;
8
+ outcome: 'requested' | 'returned' | 'failed' | 'unknown';
9
+ exitCode?: number;
10
+ paths: string[];
11
+ }
12
+ export interface ExternalResult {
13
+ path: string;
14
+ toolId: string;
15
+ id?: string;
16
+ version?: string;
17
+ projectId?: string;
18
+ }
19
+ export interface ExternalTurn {
20
+ id: string;
21
+ at: string;
22
+ endedAt?: string;
23
+ state: 'running' | 'completed' | 'failed' | 'stopped' | 'unknown';
24
+ prompt: string;
25
+ reply: string;
26
+ messages: Array<{
27
+ at: string;
28
+ text: string;
29
+ }>;
30
+ tools: ExternalTool[];
31
+ results: ExternalResult[];
32
+ omittedTools: number;
33
+ omittedChars: number;
34
+ where?: string;
35
+ }
36
+ export interface ExternalSessionSummary {
37
+ id: string;
38
+ producer: string;
39
+ sessionId: string;
40
+ title: string;
41
+ lastAt: string;
42
+ observedAt: string;
43
+ state: ExternalTurn['state'];
44
+ turns: number;
45
+ sourceCursor: number;
46
+ omittedTurns: number;
47
+ problem?: string;
48
+ inputOwner?: 'external';
49
+ where?: string;
50
+ open?: {
51
+ where: string;
52
+ since?: string;
53
+ pid?: number;
54
+ } | null;
55
+ resume?: string;
56
+ bytes?: number;
57
+ continueWith?: 'claude-acp' | 'codex-acp' | 'acp' | null;
58
+ conversationId?: string;
59
+ }
60
+ export interface ExternalSessionRecord {
61
+ format: 1;
62
+ projectRoot: string;
63
+ summary: ExternalSessionSummary;
64
+ turns: ExternalTurn[];
65
+ sourceDigest: string;
66
+ }
67
+ export declare const externalSessionId: (producer: string, session: string) => string;
68
+ export declare function listExternalSessions(root: string): {
69
+ sessions: ExternalSessionSummary[];
70
+ omitted: number;
71
+ };
72
+ export declare function readExternalSession(root: string, input: {
73
+ session: string;
74
+ before?: number;
75
+ limit?: number;
76
+ }): {
77
+ session: ExternalSessionSummary;
78
+ first: number;
79
+ turns: ExternalTurn[];
80
+ };
81
+ /** Native adapters retain an observation under their existing filesystem authority.
82
+ * No managed execution, Agent input, grant change or runtime startup is involved. */
83
+ export declare function saveExternalSession(root: string, value: ExternalSessionRecord, previousDigest?: string): ExternalSessionSummary;
84
+ export declare function retainedExternalSession(root: string, producer: string, sessionId: string): ExternalSessionRecord | undefined;
@@ -0,0 +1,190 @@
1
+ import fs, { constants } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { Ajv } from 'ajv';
5
+ import { externalSessionLimits as limits, externalSessionSummarySchema, externalTurnSchema } from './external-session-contract.js';
6
+ export { externalSessionLimits } from './external-session-contract.js';
7
+ export const externalSessionId = (producer, session) => 'x-' + createHash('sha256').update(producer + '\0' + session).digest('hex');
8
+ const schema = new Ajv({ strict: false }).compile({ type: 'object', additionalProperties: false, required: ['format', 'projectRoot', 'summary', 'turns', 'sourceDigest'], properties: {
9
+ format: { const: 1 }, projectRoot: { type: 'string' }, summary: externalSessionSummarySchema,
10
+ turns: { type: 'array', maxItems: limits.maxTurns, items: externalTurnSchema }, sourceDigest: { type: 'string', pattern: '^[a-f0-9]{64}$' }
11
+ } });
12
+ function directory(root, create = false) {
13
+ let dir = fs.realpathSync(root);
14
+ for (const part of ['.rho', 'agent', 'external']) {
15
+ dir = path.join(dir, part);
16
+ if (create) {
17
+ try {
18
+ fs.mkdirSync(dir, { mode: 0o700 });
19
+ }
20
+ catch (e) {
21
+ if (e.code !== 'EEXIST')
22
+ throw e;
23
+ }
24
+ }
25
+ const stat = fs.lstatSync(dir);
26
+ if (!stat.isDirectory() || stat.isSymbolicLink() || fs.realpathSync(dir) !== dir)
27
+ throw new Error('External session storage must stay in the project.');
28
+ }
29
+ return dir;
30
+ }
31
+ function read(file, root) {
32
+ const fd = fs.openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
33
+ try {
34
+ const stat = fs.fstatSync(fd);
35
+ if (!stat.isFile() || stat.size > limits.maxRecordBytes)
36
+ throw new Error('External session record exceeds 4 MiB.');
37
+ const b = Buffer.alloc(stat.size + 1), n = fs.readSync(fd, b, 0, b.length, 0);
38
+ if (n !== stat.size)
39
+ throw new Error('External session changed during observation.');
40
+ const v = JSON.parse(b.subarray(0, n).toString('utf8'));
41
+ if (!schema(v) || v.projectRoot !== root || v.summary.id !== externalSessionId(v.summary.producer, v.summary.sessionId))
42
+ throw new Error('Invalid external session record.');
43
+ return v;
44
+ }
45
+ finally {
46
+ fs.closeSync(fd);
47
+ }
48
+ }
49
+ function observed(v) {
50
+ // No heartbeat proves the model or its native subprocess is alive.
51
+ if (v.summary.state !== 'running' || Date.now() - Date.parse(v.summary.observedAt) < 10000)
52
+ return v;
53
+ return { ...v, summary: { ...v.summary, state: 'unknown' }, turns: v.turns.map(t => t.state !== 'running' ? t : { ...t, state: 'unknown', tools: t.tools.map(tool => tool.outcome === 'requested' ? { ...tool, outcome: 'unknown' } : tool) }) };
54
+ }
55
+ export function listExternalSessions(root) {
56
+ root = fs.realpathSync(root);
57
+ const sessions = [];
58
+ let omitted = 0;
59
+ try {
60
+ const dir = directory(root), entries = fs.opendirSync(dir);
61
+ try {
62
+ let e, scanned = 0;
63
+ while ((e = entries.readSync())) {
64
+ if (++scanned > limits.maxSessions * 4) {
65
+ omitted++;
66
+ break;
67
+ }
68
+ if (!/^x-[a-f0-9]{64}\.json$/.test(e.name))
69
+ continue;
70
+ if (sessions.length >= limits.maxSessions) {
71
+ omitted++;
72
+ continue;
73
+ }
74
+ try {
75
+ sessions.push(observed(read(path.join(dir, e.name), root)).summary);
76
+ }
77
+ catch {
78
+ omitted++;
79
+ }
80
+ }
81
+ }
82
+ finally {
83
+ entries.closeSync();
84
+ }
85
+ }
86
+ catch (e) {
87
+ if (e.code !== 'ENOENT')
88
+ omitted++;
89
+ }
90
+ return { sessions: sessions.sort((a, b) => b.lastAt.localeCompare(a.lastAt)), omitted };
91
+ }
92
+ export function readExternalSession(root, input) {
93
+ if (!/^x-[a-f0-9]{64}$/.test(input.session))
94
+ throw new Error('Invalid external session identity.');
95
+ root = fs.realpathSync(root);
96
+ const v = observed(read(path.join(directory(root), input.session + '.json'), root));
97
+ const end = Math.min(v.turns.length, input.before ?? v.turns.length), first = Math.max(0, end - Math.min(20, Math.max(1, input.limit ?? 4)));
98
+ return { session: v.summary, first, turns: v.turns.slice(first, end) };
99
+ }
100
+ /** Native adapters retain an observation under their existing filesystem authority.
101
+ * No managed execution, Agent input, grant change or runtime startup is involved. */
102
+ export function saveExternalSession(root, value, previousDigest) {
103
+ root = fs.realpathSync(root);
104
+ if (value.projectRoot !== root || !schema(value) || value.summary.id !== externalSessionId(value.summary.producer, value.summary.sessionId))
105
+ throw new Error('Invalid external session observation.');
106
+ const bytes = JSON.stringify(value);
107
+ if (Buffer.byteLength(bytes) > limits.maxRecordBytes)
108
+ throw new Error('External session exceeds maxRecordBytes; previous observation retained.');
109
+ const dir = directory(root, true), file = path.join(dir, value.summary.id + '.json');
110
+ // The lock covers only a small local observation write; never steal a live writer's lock.
111
+ const lock = file + '.lock';
112
+ let fd;
113
+ try {
114
+ fd = fs.openSync(lock, 'wx', 0o600);
115
+ }
116
+ catch {
117
+ // Recover only a positively dead writer. Unknown owners and live PIDs retain the lock.
118
+ try {
119
+ const stat = fs.lstatSync(lock);
120
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 32)
121
+ throw new Error();
122
+ const pid = Number(fs.readFileSync(lock, 'utf8'));
123
+ if (!Number.isSafeInteger(pid) || pid <= 0)
124
+ throw new Error();
125
+ try {
126
+ process.kill(pid, 0);
127
+ throw new Error();
128
+ }
129
+ catch (e) {
130
+ if (e.code !== 'ESRCH')
131
+ throw e;
132
+ }
133
+ const now = fs.lstatSync(lock);
134
+ if (now.ino !== stat.ino || now.dev !== stat.dev)
135
+ throw new Error();
136
+ fs.unlinkSync(lock);
137
+ fd = fs.openSync(lock, 'wx', 0o600);
138
+ }
139
+ catch {
140
+ throw new Error('External session observation is busy; previous observation retained.');
141
+ }
142
+ }
143
+ fs.writeFileSync(fd, String(process.pid));
144
+ const temp = file + '.' + randomUUID() + '.tmp';
145
+ try {
146
+ let old;
147
+ try {
148
+ old = read(file, root);
149
+ }
150
+ catch (e) {
151
+ if (e.code !== 'ENOENT')
152
+ throw e;
153
+ }
154
+ if (old) {
155
+ if (value.summary.sourceCursor < old.summary.sourceCursor)
156
+ throw new Error('External source moved backwards; previous observation retained.');
157
+ if (value.summary.sourceCursor === old.summary.sourceCursor && old.sourceDigest !== value.sourceDigest)
158
+ throw new Error('External source identity conflicts with retained content.');
159
+ if (value.summary.sourceCursor > old.summary.sourceCursor && previousDigest !== old.sourceDigest)
160
+ throw new Error('External source prefix conflicts with retained content.');
161
+ }
162
+ else {
163
+ const entries = fs.readdirSync(dir).filter(n => /^x-[a-f0-9]{64}\.json$/.test(n));
164
+ if (entries.length >= limits.maxSessions)
165
+ throw new Error('External session maxSessions reached; previous observations retained.');
166
+ }
167
+ fs.writeFileSync(temp, bytes, { flag: 'wx', mode: 0o600 });
168
+ fs.renameSync(temp, file);
169
+ }
170
+ finally {
171
+ fs.closeSync(fd);
172
+ fs.unlinkSync(lock);
173
+ try {
174
+ fs.unlinkSync(temp);
175
+ }
176
+ catch { }
177
+ }
178
+ return value.summary;
179
+ }
180
+ export function retainedExternalSession(root, producer, sessionId) {
181
+ root = fs.realpathSync(root);
182
+ try {
183
+ return read(path.join(directory(root), externalSessionId(producer, sessionId) + '.json'), root);
184
+ }
185
+ catch (e) {
186
+ if (e.code === 'ENOENT')
187
+ return undefined;
188
+ throw e;
189
+ }
190
+ }