@rynx-ai/daemon 0.1.10 → 0.1.11-beta.10

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 (156) hide show
  1. package/bundled-plugins/.rynx-plugin/marketplace.json +10 -0
  2. package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +25 -0
  3. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.d.ts +48 -0
  4. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +56 -0
  5. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.d.ts +85 -0
  6. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.js +333 -0
  7. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.d.ts +109 -0
  8. package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.js +347 -0
  9. package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +95 -0
  10. package/bundled-plugins/plugins/lark/dist/host-adapters.js +506 -0
  11. package/bundled-plugins/plugins/lark/dist/index.d.ts +7 -0
  12. package/bundled-plugins/plugins/lark/dist/index.js +7 -0
  13. package/bundled-plugins/plugins/lark/dist/lark-card-stream.d.ts +363 -0
  14. package/bundled-plugins/plugins/lark/dist/lark-card-stream.js +1335 -0
  15. package/bundled-plugins/plugins/lark/dist/lark-config-store.d.ts +30 -0
  16. package/bundled-plugins/plugins/lark/dist/lark-config-store.js +135 -0
  17. package/bundled-plugins/plugins/lark/dist/lark-content.d.ts +30 -0
  18. package/bundled-plugins/plugins/lark/dist/lark-content.js +138 -0
  19. package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.d.ts +65 -0
  20. package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.js +106 -0
  21. package/bundled-plugins/plugins/lark/dist/lark-diff-card.d.ts +25 -0
  22. package/bundled-plugins/plugins/lark/dist/lark-diff-card.js +58 -0
  23. package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.d.ts +16 -0
  24. package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.js +141 -0
  25. package/bundled-plugins/plugins/lark/dist/lark-resume-card.d.ts +31 -0
  26. package/bundled-plugins/plugins/lark/dist/lark-resume-card.js +105 -0
  27. package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.d.ts +18 -0
  28. package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.js +95 -0
  29. package/bundled-plugins/plugins/lark/dist/lark-sdk/client.d.ts +8 -0
  30. package/bundled-plugins/plugins/lark/dist/lark-sdk/client.js +32 -0
  31. package/bundled-plugins/plugins/lark/dist/lark-sdk/index.d.ts +1 -0
  32. package/bundled-plugins/plugins/lark/dist/lark-sdk/index.js +1 -0
  33. package/bundled-plugins/plugins/lark/dist/lark-sender.d.ts +114 -0
  34. package/bundled-plugins/plugins/lark/dist/lark-sender.js +323 -0
  35. package/bundled-plugins/plugins/lark/dist/lark-session-store.d.ts +13 -0
  36. package/bundled-plugins/plugins/lark/dist/lark-session-store.js +14 -0
  37. package/bundled-plugins/plugins/lark/dist/lark-settings-card.d.ts +55 -0
  38. package/bundled-plugins/plugins/lark/dist/lark-settings-card.js +158 -0
  39. package/bundled-plugins/plugins/lark/dist/lark-setup.d.ts +39 -0
  40. package/bundled-plugins/plugins/lark/dist/lark-setup.js +103 -0
  41. package/bundled-plugins/plugins/lark/dist/lark.d.ts +507 -0
  42. package/bundled-plugins/plugins/lark/dist/lark.js +1975 -0
  43. package/bundled-plugins/plugins/lark/dist/runtime.d.ts +13 -0
  44. package/bundled-plugins/plugins/lark/dist/runtime.js +131328 -0
  45. package/bundled-plugins/plugins/lark/dist/settings.d.ts +7 -0
  46. package/bundled-plugins/plugins/lark/dist/settings.js +126971 -0
  47. package/bundled-plugins/plugins/lark/package.json +57 -0
  48. package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
  49. package/bundled-plugins/plugins/lark/ui/assets/index-D8Amoe6a.js +201 -0
  50. package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  51. package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  52. package/bundled-plugins/plugins/lark/ui/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  53. package/bundled-plugins/plugins/lark/ui/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  54. package/bundled-plugins/plugins/lark/ui/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  55. package/bundled-plugins/plugins/lark/ui/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  56. package/bundled-plugins/plugins/lark/ui/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  57. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  58. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  59. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  60. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  61. package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  62. package/bundled-plugins/plugins/lark/ui/index.html +13 -0
  63. package/dist/agent-file.js +0 -1
  64. package/dist/browser-artifact-management.d.ts +4 -1
  65. package/dist/browser-artifact-management.js +18 -10
  66. package/dist/browser-artifacts.d.ts +1 -0
  67. package/dist/browser-artifacts.js +1 -0
  68. package/dist/bundled-plugins.d.ts +15 -0
  69. package/dist/bundled-plugins.js +134 -0
  70. package/dist/chrome-for-testing-runtime.d.ts +27 -0
  71. package/dist/chrome-for-testing-runtime.js +193 -0
  72. package/dist/chrome-for-testing-store.d.ts +2 -0
  73. package/dist/chrome-for-testing-store.js +9 -0
  74. package/dist/control-deps.d.ts +6 -2
  75. package/dist/control-deps.js +88 -82
  76. package/dist/daemon-server.d.ts +7 -1
  77. package/dist/daemon-server.js +310 -87
  78. package/dist/db.js +181 -124
  79. package/dist/entry-path.d.ts +3 -0
  80. package/dist/entry-path.js +12 -0
  81. package/dist/index-daemon.js +3 -66
  82. package/dist/lifecycle.d.ts +12 -0
  83. package/dist/lifecycle.js +16 -0
  84. package/dist/maintenance-lease.d.ts +39 -0
  85. package/dist/maintenance-lease.js +250 -0
  86. package/dist/migrations/cleanup-legacy-sessions.d.ts +1 -1
  87. package/dist/migrations/cleanup-legacy-sessions.js +33 -15
  88. package/dist/official-plugins.d.ts +2 -0
  89. package/dist/official-plugins.js +2 -0
  90. package/dist/plugin-cli.d.ts +1 -3
  91. package/dist/plugin-cli.js +56 -45
  92. package/dist/plugin-host-leases.d.ts +4 -0
  93. package/dist/plugin-host-leases.js +7 -0
  94. package/dist/plugin-host-rpc.d.ts +73 -6
  95. package/dist/plugin-host-rpc.js +354 -81
  96. package/dist/plugin-installer.d.ts +24 -55
  97. package/dist/plugin-installer.js +258 -412
  98. package/dist/plugin-management-service.d.ts +30 -23
  99. package/dist/plugin-management-service.js +288 -105
  100. package/dist/plugin-marketplace.d.ts +35 -0
  101. package/dist/plugin-marketplace.js +299 -0
  102. package/dist/plugin-package.d.ts +22 -24
  103. package/dist/plugin-package.js +520 -571
  104. package/dist/plugin-skill-context.d.ts +4 -0
  105. package/dist/plugin-skill-context.js +35 -0
  106. package/dist/plugin-store.d.ts +36 -37
  107. package/dist/plugin-store.js +138 -92
  108. package/dist/plugin-supervisor.d.ts +50 -125
  109. package/dist/plugin-supervisor.js +678 -1030
  110. package/dist/pm2.d.ts +11 -2
  111. package/dist/pm2.js +130 -28
  112. package/dist/process.d.ts +12 -0
  113. package/dist/process.js +91 -0
  114. package/dist/runtime-project-store.d.ts +9 -0
  115. package/dist/runtime-project-store.js +83 -0
  116. package/dist/session-fallback-workspace.d.ts +22 -0
  117. package/dist/session-fallback-workspace.js +120 -0
  118. package/dist/session-fork-store.d.ts +41 -0
  119. package/dist/session-fork-store.js +288 -0
  120. package/dist/session-launch-operations.d.ts +23 -2
  121. package/dist/session-launch-operations.js +48 -7
  122. package/dist/session-meta-store.d.ts +12 -9
  123. package/dist/session-meta-store.js +50 -49
  124. package/dist/session-portal-grant-store.d.ts +29 -0
  125. package/dist/session-portal-grant-store.js +84 -0
  126. package/dist/setup-service.d.ts +21 -0
  127. package/dist/setup-service.js +39 -0
  128. package/dist/skills-catalog.d.ts +11 -2
  129. package/dist/skills-catalog.js +307 -7
  130. package/dist/system-dependencies.d.ts +26 -0
  131. package/dist/system-dependencies.js +149 -0
  132. package/package.json +36 -20
  133. package/dist/browser-cli-args.d.ts +0 -20
  134. package/dist/browser-cli-args.js +0 -100
  135. package/dist/channel-store.d.ts +0 -22
  136. package/dist/channel-store.js +0 -65
  137. package/dist/cli.d.ts +0 -2
  138. package/dist/cli.js +0 -732
  139. package/dist/control-client.d.ts +0 -122
  140. package/dist/control-client.js +0 -855
  141. package/dist/control-endpoint.d.ts +0 -14
  142. package/dist/control-endpoint.js +0 -124
  143. package/dist/desktop-browser-host-client.d.ts +0 -35
  144. package/dist/desktop-browser-host-client.js +0 -415
  145. package/dist/instance-store.d.ts +0 -38
  146. package/dist/instance-store.js +0 -85
  147. package/dist/plugin-install-restart.d.ts +0 -13
  148. package/dist/plugin-install-restart.js +0 -24
  149. package/dist/plugin-runtime-control.d.ts +0 -2
  150. package/dist/plugin-runtime-control.js +0 -37
  151. package/dist/registry.d.ts +0 -10
  152. package/dist/registry.js +0 -52
  153. package/dist/setup.d.ts +0 -20
  154. package/dist/setup.js +0 -374
  155. package/dist/update.d.ts +0 -25
  156. package/dist/update.js +0 -133
@@ -0,0 +1,288 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { newSessionId, newSessionItemId, normalizeSessionTitle, resolvedExecutionSnapshotSchema, rynxHome, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
5
+ import { MachineSessionServiceFailure, } from "@rynx-ai/server";
6
+ import { db } from "./db.js";
7
+ const FORK_COLS = [
8
+ "operation_id",
9
+ "request_hash",
10
+ "source_session_id",
11
+ "target_session_id",
12
+ "title",
13
+ "fork_position",
14
+ "state",
15
+ "error",
16
+ ].join(", ");
17
+ /**
18
+ * SQLite/resource-file implementation of the fork publication boundary.
19
+ *
20
+ * Provider context is prepared before {@link commit}; this store then publishes
21
+ * Session metadata, an independent canonical transcript, referenced committed
22
+ * resources, and the completed operation in one SQLite transaction. Resource
23
+ * files use deterministic target ids, so a crash before the transaction can be
24
+ * retried safely.
25
+ */
26
+ export class SqliteSessionForkStore {
27
+ resourceRoot;
28
+ constructor(resourceRoot = join(rynxHome(), "resources")) {
29
+ this.resourceRoot = resourceRoot;
30
+ }
31
+ claim(input) {
32
+ const existing = this.read(input.operationId);
33
+ if (existing) {
34
+ if (existing.request_hash !== input.requestHash ||
35
+ existing.source_session_id !== input.sourceSessionId) {
36
+ throw new MachineSessionServiceFailure("conflict", "operationId was already used for a different Session fork");
37
+ }
38
+ return toOperation(existing);
39
+ }
40
+ const now = new Date().toISOString();
41
+ let targetSessionId = newSessionId();
42
+ while (db().prepare("SELECT 1 FROM sessions WHERE id = ?").pluck().get(targetSessionId) ||
43
+ db().prepare("SELECT 1 FROM session_fork_operations WHERE target_session_id = ?")
44
+ .pluck().get(targetSessionId)) {
45
+ targetSessionId = newSessionId();
46
+ }
47
+ try {
48
+ db().prepare(`INSERT INTO session_fork_operations (
49
+ operation_id, request_hash, source_session_id, target_session_id,
50
+ title, state, created_at, updated_at
51
+ ) VALUES (?, ?, ?, ?, ?, 'reserved', ?, ?)`).run(input.operationId, input.requestHash, input.sourceSessionId, targetSessionId, normalizeSessionTitle(input.title) ?? null, now, now);
52
+ }
53
+ catch (error) {
54
+ const raced = this.read(input.operationId);
55
+ if (raced)
56
+ return this.claim(input);
57
+ const busy = db().prepare(`SELECT operation_id
58
+ FROM session_fork_operations
59
+ WHERE source_session_id = ? AND state = 'reserved'`).get(input.sourceSessionId);
60
+ if (busy) {
61
+ throw new MachineSessionServiceFailure("conflict", "source Session already has a fork in progress");
62
+ }
63
+ throw error;
64
+ }
65
+ return toOperation(this.read(input.operationId));
66
+ }
67
+ commit(input) {
68
+ const operation = this.read(input.operationId);
69
+ if (!operation) {
70
+ throw new MachineSessionServiceFailure("not_found", "Session fork operation not found");
71
+ }
72
+ if (operation.state === "target_deleted") {
73
+ throw new MachineSessionServiceFailure("conflict", "fork target was deleted");
74
+ }
75
+ if (operation.target_session_id !== input.target.id) {
76
+ throw new MachineSessionServiceFailure("conflict", "fork target identity changed");
77
+ }
78
+ if (operation.state === "completed")
79
+ return;
80
+ this.publishTarget({
81
+ sourceSessionId: operation.source_session_id,
82
+ target: input.target,
83
+ items: input.items,
84
+ operationId: input.operationId,
85
+ });
86
+ }
87
+ publishNativeFork(input) {
88
+ const existing = db().prepare("SELECT forked_from_session_id FROM sessions WHERE id = ?").get(input.target.id);
89
+ if (existing) {
90
+ if (existing.forked_from_session_id === input.sourceSessionId)
91
+ return;
92
+ throw new MachineSessionServiceFailure("conflict", "target Session already exists");
93
+ }
94
+ this.publishTarget(input);
95
+ }
96
+ publishTarget(input) {
97
+ const itemIds = new Map();
98
+ for (const item of input.items)
99
+ itemIds.set(item.id, newSessionItemId(item.type));
100
+ const resourceIds = referencedResourceIds(input.items);
101
+ const resourceMap = new Map();
102
+ const resources = resourceIds.map((resourceId) => {
103
+ const source = db().prepare(`SELECT * FROM session_resources
104
+ WHERE session_id = ? AND id = ? AND state = 'committed'`).get(input.sourceSessionId, resourceId);
105
+ if (!source) {
106
+ throw new MachineSessionServiceFailure("failed_precondition", `fork history references unavailable resource ${resourceId}`);
107
+ }
108
+ const targetId = deterministicId("res", input.target.id, source.id);
109
+ resourceMap.set(source.id, targetId);
110
+ const target = {
111
+ ...source,
112
+ id: targetId,
113
+ session_id: input.target.id,
114
+ client_upload_id: deterministicId("fork", input.target.id, source.client_upload_id),
115
+ upload_id: deterministicId("upl", input.target.id, source.upload_id),
116
+ message_item_id: source.message_item_id
117
+ ? itemIds.get(source.message_item_id) ?? null
118
+ : null,
119
+ };
120
+ this.copyResourceFile(source, target);
121
+ return target;
122
+ });
123
+ const clonedItems = input.items.map((item, position) => {
124
+ const remapped = remapItemResources(item, resourceMap);
125
+ return {
126
+ ...remapped,
127
+ id: itemIds.get(item.id),
128
+ sessionId: input.target.id,
129
+ position,
130
+ };
131
+ });
132
+ const conn = db();
133
+ conn.transaction(() => {
134
+ if (input.operationId) {
135
+ const current = this.read(input.operationId);
136
+ if (current?.state === "completed")
137
+ return;
138
+ if (!current || current.state !== "reserved") {
139
+ throw new MachineSessionServiceFailure("conflict", "Session fork state changed");
140
+ }
141
+ }
142
+ else {
143
+ const existing = conn.prepare("SELECT forked_from_session_id FROM sessions WHERE id = ?").get(input.target.id);
144
+ if (existing?.forked_from_session_id === input.sourceSessionId)
145
+ return;
146
+ if (existing) {
147
+ throw new MachineSessionServiceFailure("conflict", "target Session already exists");
148
+ }
149
+ }
150
+ conn.prepare(`INSERT INTO sessions (
151
+ id, workspace, execution, title, forked_from_session_id,
152
+ source, created_at, updated_at
153
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(input.target.id, JSON.stringify(sessionWorkspaceSnapshotSchema.parse(input.target.workspace)), JSON.stringify(resolvedExecutionSnapshotSchema.parse(input.target.execution)), normalizeSessionTitle(input.target.title) ?? null, input.target.forkedFromSessionId ?? null, input.target.source, input.target.createdAt, input.target.updatedAt ?? input.target.createdAt);
154
+ const insertResource = conn.prepare(`INSERT INTO session_resources (
155
+ id, session_id, client_upload_id, upload_id, filename, media_type,
156
+ byte_length, uploaded_bytes, sha256, width, height, state,
157
+ message_item_id, created_at, committed_at
158
+ ) VALUES (
159
+ @id, @session_id, @client_upload_id, @upload_id, @filename, @media_type,
160
+ @byte_length, @uploaded_bytes, @sha256, @width, @height, 'committed',
161
+ @message_item_id, @created_at, @committed_at
162
+ )`);
163
+ for (const resource of resources)
164
+ insertResource.run(resource);
165
+ const insertItem = conn.prepare(`INSERT INTO session_items (
166
+ id, session_id, position, response_id, type, status,
167
+ data, created_by, created_at
168
+ ) VALUES (
169
+ @id, @session_id, @position, @response_id, @type, @status,
170
+ @data, @created_by, @created_at
171
+ )`);
172
+ for (const item of clonedItems) {
173
+ insertItem.run({
174
+ id: item.id,
175
+ session_id: item.sessionId,
176
+ position: item.position,
177
+ response_id: item.responseId,
178
+ type: item.type,
179
+ status: item.status,
180
+ data: JSON.stringify(item.data),
181
+ created_by: item.createdBy ?? null,
182
+ created_at: item.createdAt,
183
+ });
184
+ }
185
+ if (input.operationId) {
186
+ const completedAt = new Date().toISOString();
187
+ conn.prepare(`UPDATE session_fork_operations
188
+ SET state = 'completed', fork_position = ?, error = NULL,
189
+ updated_at = ?, completed_at = ?
190
+ WHERE operation_id = ? AND state = 'reserved'`).run(clonedItems.at(-1)?.position ?? -1, completedAt, completedAt, input.operationId);
191
+ }
192
+ }).immediate();
193
+ }
194
+ markError(operationId, error) {
195
+ db().prepare(`UPDATE session_fork_operations
196
+ SET error = ?, updated_at = ?
197
+ WHERE operation_id = ? AND state = 'reserved'`).run(error.slice(0, 4_096), new Date().toISOString(), operationId);
198
+ }
199
+ listReserved() {
200
+ return db().prepare(`SELECT ${FORK_COLS}
201
+ FROM session_fork_operations
202
+ WHERE state = 'reserved'
203
+ ORDER BY created_at`).all().map(toOperation);
204
+ }
205
+ hasReservedSource(sessionId) {
206
+ return Boolean(db().prepare(`SELECT 1
207
+ FROM session_fork_operations
208
+ WHERE source_session_id = ? AND state = 'reserved'`).pluck().get(sessionId));
209
+ }
210
+ markTargetDeleted(sessionId) {
211
+ const now = new Date().toISOString();
212
+ db().prepare(`UPDATE session_fork_operations
213
+ SET state = 'target_deleted', updated_at = ?, deleted_at = ?
214
+ WHERE target_session_id = ? AND state IN ('reserved', 'completed')`).run(now, now, sessionId);
215
+ }
216
+ read(operationId) {
217
+ return db().prepare(`SELECT ${FORK_COLS}
218
+ FROM session_fork_operations
219
+ WHERE operation_id = ?`).get(operationId);
220
+ }
221
+ copyResourceFile(source, target) {
222
+ const sourcePath = this.resourcePath(source);
223
+ if (!existsSync(sourcePath)) {
224
+ throw new MachineSessionServiceFailure("failed_precondition", `fork resource file is missing: ${source.id}`);
225
+ }
226
+ const targetDir = this.sessionDirectory(target.session_id);
227
+ mkdirSync(targetDir, { recursive: true, mode: 0o700 });
228
+ chmodSync(targetDir, 0o700);
229
+ copyFileSync(sourcePath, this.resourcePath(target));
230
+ chmodSync(this.resourcePath(target), 0o600);
231
+ }
232
+ sessionDirectory(sessionId) {
233
+ const key = createHash("sha256").update(sessionId).digest("hex").slice(0, 32);
234
+ return join(this.resourceRoot, key);
235
+ }
236
+ resourcePath(row) {
237
+ return join(this.sessionDirectory(row.session_id), `${row.id}.${extension(row.media_type)}`);
238
+ }
239
+ }
240
+ function toOperation(row) {
241
+ return {
242
+ operationId: row.operation_id,
243
+ requestHash: row.request_hash,
244
+ sourceSessionId: row.source_session_id,
245
+ targetSessionId: row.target_session_id,
246
+ ...(row.title ? { title: row.title } : {}),
247
+ ...(row.fork_position === null ? {} : { forkPosition: row.fork_position }),
248
+ state: row.state,
249
+ ...(row.error ? { error: row.error } : {}),
250
+ };
251
+ }
252
+ function referencedResourceIds(items) {
253
+ const ids = new Set();
254
+ for (const item of items) {
255
+ if (item.type !== "message" || item.data.role !== "user")
256
+ continue;
257
+ for (const part of item.data.content) {
258
+ if (part.type === "input_image")
259
+ ids.add(part.resourceId);
260
+ }
261
+ }
262
+ return [...ids];
263
+ }
264
+ function remapItemResources(item, resources) {
265
+ const cloned = structuredClone(item);
266
+ if (cloned.type !== "message" || cloned.data.role !== "user")
267
+ return cloned;
268
+ cloned.data.content = cloned.data.content.map((part) => part.type === "input_image"
269
+ ? { ...part, resourceId: resources.get(part.resourceId) ?? part.resourceId }
270
+ : part);
271
+ return cloned;
272
+ }
273
+ function deterministicId(prefix, targetSessionId, sourceId) {
274
+ const digest = createHash("sha256")
275
+ .update(`${targetSessionId}\0${sourceId}`)
276
+ .digest("hex");
277
+ return `${prefix}_${digest.slice(0, 40)}`;
278
+ }
279
+ function extension(mediaType) {
280
+ switch (mediaType) {
281
+ case "image/png":
282
+ return "png";
283
+ case "image/jpeg":
284
+ return "jpg";
285
+ case "image/webp":
286
+ return "webp";
287
+ }
288
+ }
@@ -9,12 +9,22 @@ export interface EnsurePluginSessionLaunchInput {
9
9
  operationId: string;
10
10
  /** SHA-256 of the host-normalized, execution-affecting launch request. */
11
11
  requestHash: string;
12
+ /** SHA-256 before mutable filesystem selectors are canonicalized. */
13
+ rawRequestHash?: string;
12
14
  session: PluginSessionLaunchMeta;
13
15
  }
14
16
  export interface EnsurePluginSessionLaunchResult {
15
17
  sessionId: string;
16
18
  disposition: "created" | "replayed";
17
19
  }
20
+ export type ReplayPluginSessionLaunchInput = Omit<EnsurePluginSessionLaunchInput, "session"> & {
21
+ /**
22
+ * A caller doing a pre-canonicalization lookup may treat a hash mismatch as
23
+ * a cache miss, then retry with the canonical hash. The canonical lookup
24
+ * must keep the default strict conflict behavior.
25
+ */
26
+ allowRequestHashMismatch?: boolean;
27
+ };
18
28
  export declare class SessionLaunchOperationConflictError extends Error {
19
29
  readonly pluginId: string;
20
30
  readonly scopeId: string;
@@ -30,12 +40,23 @@ export declare class SessionLaunchOperationTargetDeletedError extends Error {
30
40
  readonly code = "SESSION_LAUNCH_OPERATION_TARGET_DELETED";
31
41
  constructor(pluginId: string, scopeId: string, operationId: string, sessionId: string);
32
42
  }
43
+ /**
44
+ * Read the durable idempotency fence before resolving mutable launch selectors
45
+ * such as a Project or Agent. A replay must not fail merely because the
46
+ * original template was edited or deleted after the Session snapshot existed.
47
+ *
48
+ * `undefined` means the caller must resolve snapshots and proceed through
49
+ * {@link ensurePluginSessionLaunch}; that function remains the atomic race
50
+ * winner when two first attempts arrive concurrently.
51
+ */
52
+ export declare function replayPluginSessionLaunch(input: ReplayPluginSessionLaunchInput): EnsurePluginSessionLaunchResult | undefined;
33
53
  /**
34
54
  * Atomically allocate a persisted machine session for one plugin operation.
35
55
  *
36
56
  * The durable operation row is the idempotency boundary across runner crashes,
37
- * daemon restarts, and remote lease re-delivery. It deliberately excludes any
38
- * Workspace concept: `scopeId` and `operationId` are opaque to the daemon.
57
+ * daemon restarts, and remote lease re-delivery. `scopeId` and `operationId`
58
+ * remain opaque; the Session row stores only the already-resolved workspace
59
+ * snapshot, never a Project identity.
39
60
  */
40
61
  export declare function ensurePluginSessionLaunch(input: EnsurePluginSessionLaunchInput): EnsurePluginSessionLaunchResult;
41
62
  export {};
@@ -31,22 +31,63 @@ export class SessionLaunchOperationTargetDeletedError extends Error {
31
31
  this.name = "SessionLaunchOperationTargetDeletedError";
32
32
  }
33
33
  }
34
+ /**
35
+ * Read the durable idempotency fence before resolving mutable launch selectors
36
+ * such as a Project or Agent. A replay must not fail merely because the
37
+ * original template was edited or deleted after the Session snapshot existed.
38
+ *
39
+ * `undefined` means the caller must resolve snapshots and proceed through
40
+ * {@link ensurePluginSessionLaunch}; that function remains the atomic race
41
+ * winner when two first attempts arrive concurrently.
42
+ */
43
+ export function replayPluginSessionLaunch(input) {
44
+ const pluginId = requiredId(input.pluginId, "pluginId");
45
+ const scopeId = requiredId(input.scopeId, "scopeId");
46
+ const operationId = requiredId(input.operationId, "operationId");
47
+ const requestHash = requiredRequestHash(input.requestHash);
48
+ const existing = readOperation(pluginId, scopeId, operationId);
49
+ if (!existing)
50
+ return undefined;
51
+ if (existing.request_hash !== requestHash &&
52
+ existing.raw_request_hash !== requestHash) {
53
+ if (input.allowRequestHashMismatch)
54
+ return undefined;
55
+ throw new SessionLaunchOperationConflictError(pluginId, scopeId, operationId);
56
+ }
57
+ if (existing.state === "deleted") {
58
+ throw new SessionLaunchOperationTargetDeletedError(pluginId, scopeId, operationId, existing.session_id);
59
+ }
60
+ if (!sessionExists(existing.session_id)) {
61
+ db().transaction(() => {
62
+ tombstoneOperations(existing.session_id, new Date().toISOString());
63
+ }).immediate();
64
+ throw new SessionLaunchOperationTargetDeletedError(pluginId, scopeId, operationId, existing.session_id);
65
+ }
66
+ return { disposition: "replayed", sessionId: existing.session_id };
67
+ }
34
68
  /**
35
69
  * Atomically allocate a persisted machine session for one plugin operation.
36
70
  *
37
71
  * The durable operation row is the idempotency boundary across runner crashes,
38
- * daemon restarts, and remote lease re-delivery. It deliberately excludes any
39
- * Workspace concept: `scopeId` and `operationId` are opaque to the daemon.
72
+ * daemon restarts, and remote lease re-delivery. `scopeId` and `operationId`
73
+ * remain opaque; the Session row stores only the already-resolved workspace
74
+ * snapshot, never a Project identity.
40
75
  */
41
76
  export function ensurePluginSessionLaunch(input) {
42
77
  const pluginId = requiredId(input.pluginId, "pluginId");
43
78
  const scopeId = requiredId(input.scopeId, "scopeId");
44
79
  const operationId = requiredId(input.operationId, "operationId");
45
80
  const requestHash = requiredRequestHash(input.requestHash);
81
+ const rawRequestHash = input.rawRequestHash
82
+ ? requiredRequestHash(input.rawRequestHash)
83
+ : requestHash;
46
84
  const ensure = db().transaction(() => {
47
85
  const existing = readOperation(pluginId, scopeId, operationId);
48
86
  if (existing) {
49
- if (existing.request_hash !== requestHash) {
87
+ if (existing.request_hash !== requestHash &&
88
+ existing.raw_request_hash !== requestHash &&
89
+ existing.request_hash !== rawRequestHash &&
90
+ existing.raw_request_hash !== rawRequestHash) {
50
91
  throw new SessionLaunchOperationConflictError(pluginId, scopeId, operationId);
51
92
  }
52
93
  if (existing.state === "deleted") {
@@ -78,10 +119,10 @@ export function ensurePluginSessionLaunch(input) {
78
119
  }
79
120
  db()
80
121
  .prepare(`INSERT INTO plugin_session_launch_operations
81
- (plugin_id, scope_id, operation_id, request_hash, session_id, state,
122
+ (plugin_id, scope_id, operation_id, request_hash, raw_request_hash, session_id, state,
82
123
  created_at, updated_at, deleted_at)
83
- VALUES (?, ?, ?, ?, ?, 'active', ?, ?, NULL)`)
84
- .run(pluginId, scopeId, operationId, requestHash, sessionId, now, now);
124
+ VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, NULL)`)
125
+ .run(pluginId, scopeId, operationId, requestHash, rawRequestHash, sessionId, now, now);
85
126
  return { disposition: "created", sessionId };
86
127
  });
87
128
  const result = ensure.immediate();
@@ -92,7 +133,7 @@ export function ensurePluginSessionLaunch(input) {
92
133
  }
93
134
  function readOperation(pluginId, scopeId, operationId) {
94
135
  return db()
95
- .prepare(`SELECT request_hash, session_id, state
136
+ .prepare(`SELECT request_hash, raw_request_hash, session_id, state
96
137
  FROM plugin_session_launch_operations
97
138
  WHERE plugin_id = ? AND scope_id = ? AND operation_id = ?`)
98
139
  .get(pluginId, scopeId, operationId);
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * The unified machine-session record (SQLite) — one row per session regardless
3
- * of origin (the web console AND every channel). Holds identity/meta (source /
4
- * provider / agent / inline config / model / title / …) so the **session list survives a
5
- * daemon restart**; the transcript lives in `session_items` (see
3
+ * of origin (the web console AND every channel). Holds immutable workspace /
4
+ * execution snapshots plus display metadata so the **session list survives a
5
+ * daemon restart** without re-reading a Project or Agent; the transcript lives
6
+ * in `session_items` (see
6
7
  * {@link import("./session-log-store.js")}). Runtime `status` is intentionally
7
8
  * not stored — it is reconstructed as `idle` after a restart.
8
9
  *
9
10
  * Exposes both the standalone functions the control-plane deps wire in and a
10
- * {@link SessionRegistry} adapter ({@link sessionRegistry}) injected into channels
11
- * via the `ChannelContext`, so a channel registers its sessions the same way the
12
- * console does.
11
+ * {@link SessionRegistry} adapter ({@link sessionRegistry}) used by daemon
12
+ * session services and capability-checked plugin Host RPC.
13
13
  */
14
14
  import { type MachineSessionRecord, type SessionRegistry } from "@rynx-ai/core";
15
15
  /** Back-compat alias for the unified record. */
@@ -17,9 +17,12 @@ export type StoredSessionMeta = MachineSessionRecord;
17
17
  export declare function listSessionMetas(): StoredSessionMeta[];
18
18
  export declare function getSessionMeta(id: string): StoredSessionMeta | undefined;
19
19
  export declare function createSessionMeta(meta: StoredSessionMeta): void;
20
- export declare function updateSessionMeta(meta: StoredSessionMeta): void;
21
20
  export declare function setSessionTitle(id: string, title: string): void;
21
+ export declare function setSessionExecution(id: string, execution: MachineSessionRecord["execution"]): void;
22
22
  export declare function removeSessionMeta(id: string): void;
23
- /** {@link SessionRegistry} over the same `sessions` table — the form channels
24
- * receive through their `ChannelContext`. */
23
+ /** {@link SessionRegistry} over the same `sessions` table. */
25
24
  export declare const sessionRegistry: SessionRegistry;
25
+ /** Generic plugin-principal ownership, including descendants forked from a
26
+ * plugin-created Session. Portal authorization calls this on every request so
27
+ * a grant cannot cross into console or another plugin's Sessions. */
28
+ export declare function sessionBelongsToPlugin(pluginId: string, sessionId: string): boolean;
@@ -1,19 +1,19 @@
1
1
  /**
2
2
  * The unified machine-session record (SQLite) — one row per session regardless
3
- * of origin (the web console AND every channel). Holds identity/meta (source /
4
- * provider / agent / inline config / model / title / …) so the **session list survives a
5
- * daemon restart**; the transcript lives in `session_items` (see
3
+ * of origin (the web console AND every channel). Holds immutable workspace /
4
+ * execution snapshots plus display metadata so the **session list survives a
5
+ * daemon restart** without re-reading a Project or Agent; the transcript lives
6
+ * in `session_items` (see
6
7
  * {@link import("./session-log-store.js")}). Runtime `status` is intentionally
7
8
  * not stored — it is reconstructed as `idle` after a restart.
8
9
  *
9
10
  * Exposes both the standalone functions the control-plane deps wire in and a
10
- * {@link SessionRegistry} adapter ({@link sessionRegistry}) injected into channels
11
- * via the `ChannelContext`, so a channel registers its sessions the same way the
12
- * console does.
11
+ * {@link SessionRegistry} adapter ({@link sessionRegistry}) used by daemon
12
+ * session services and capability-checked plugin Host RPC.
13
13
  */
14
- import { normalizeSessionTitle, } from "@rynx-ai/core";
14
+ import { normalizeSessionTitle, resolvedExecutionSnapshotSchema, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
15
15
  import { db } from "./db.js";
16
- const COLS = "id, provider, agent, config, model, reasoning_effort, title, source, created_at, updated_at";
16
+ const COLS = "id, workspace, execution, title, forked_from_session_id, source, created_at, updated_at";
17
17
  export function listSessionMetas() {
18
18
  return db()
19
19
  .prepare(`SELECT ${COLS} FROM sessions ORDER BY created_at`)
@@ -27,50 +27,32 @@ export function getSessionMeta(id) {
27
27
  export function createSessionMeta(meta) {
28
28
  db()
29
29
  .prepare(`INSERT OR IGNORE INTO sessions
30
- (id, provider, agent, config, model, reasoning_effort, title, source, created_at, updated_at)
31
- VALUES (@id, @provider, @agent, @config, @model, @reasoning_effort, @title, @source, @created_at, @updated_at)`)
30
+ (id, workspace, execution, title, forked_from_session_id, source, created_at, updated_at)
31
+ VALUES (
32
+ @id, @workspace, @execution, @title, @forked_from_session_id,
33
+ @source, @created_at, @updated_at
34
+ )`)
32
35
  .run({
33
36
  id: meta.id,
34
- provider: meta.provider ?? null,
35
- agent: meta.agent ?? null,
36
- config: meta.config ? JSON.stringify(meta.config) : null,
37
- model: meta.model ?? null,
38
- reasoning_effort: meta.reasoningEffort ?? null,
37
+ workspace: JSON.stringify(sessionWorkspaceSnapshotSchema.parse(meta.workspace)),
38
+ execution: JSON.stringify(resolvedExecutionSnapshotSchema.parse(meta.execution)),
39
39
  title: normalizeSessionTitle(meta.title) ?? null,
40
+ forked_from_session_id: meta.forkedFromSessionId ?? null,
40
41
  source: meta.source,
41
42
  created_at: meta.createdAt,
42
43
  updated_at: meta.updatedAt ?? meta.createdAt,
43
44
  });
44
45
  }
45
- export function updateSessionMeta(meta) {
46
- db()
47
- .prepare(`UPDATE sessions
48
- SET provider = @provider,
49
- agent = @agent,
50
- config = @config,
51
- model = @model,
52
- reasoning_effort = @reasoning_effort,
53
- title = @title,
54
- source = @source,
55
- updated_at = @updated_at
56
- WHERE id = @id`)
57
- .run({
58
- id: meta.id,
59
- provider: meta.provider ?? null,
60
- agent: meta.agent ?? null,
61
- config: meta.config ? JSON.stringify(meta.config) : null,
62
- model: meta.model ?? null,
63
- reasoning_effort: meta.reasoningEffort ?? null,
64
- title: normalizeSessionTitle(meta.title) ?? null,
65
- source: meta.source,
66
- updated_at: meta.updatedAt ?? new Date().toISOString(),
67
- });
68
- }
69
46
  export function setSessionTitle(id, title) {
70
47
  db()
71
48
  .prepare("UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?")
72
49
  .run(normalizeSessionTitle(title), new Date().toISOString(), id);
73
50
  }
51
+ export function setSessionExecution(id, execution) {
52
+ db()
53
+ .prepare("UPDATE sessions SET execution = ?, updated_at = ? WHERE id = ?")
54
+ .run(JSON.stringify(resolvedExecutionSnapshotSchema.parse(execution)), new Date().toISOString(), id);
55
+ }
74
56
  export function removeSessionMeta(id) {
75
57
  db().transaction(() => {
76
58
  const now = new Date().toISOString();
@@ -79,32 +61,51 @@ export function removeSessionMeta(id) {
79
61
  SET state = 'deleted', updated_at = ?, deleted_at = ?
80
62
  WHERE session_id = ? AND state = 'active'`)
81
63
  .run(now, now, id);
64
+ db()
65
+ .prepare(`UPDATE session_fork_operations
66
+ SET state = 'target_deleted', updated_at = ?, deleted_at = ?
67
+ WHERE target_session_id = ? AND state IN ('reserved', 'completed')`)
68
+ .run(now, now, id);
82
69
  db().prepare("DELETE FROM sessions WHERE id = ?").run(id);
83
70
  }).immediate();
84
71
  }
85
- /** {@link SessionRegistry} over the same `sessions` table — the form channels
86
- * receive through their `ChannelContext`. */
72
+ /** {@link SessionRegistry} over the same `sessions` table. */
87
73
  export const sessionRegistry = {
88
74
  create: createSessionMeta,
89
75
  get: getSessionMeta,
90
76
  list: listSessionMetas,
91
77
  setTitle: setSessionTitle,
78
+ setExecution: setSessionExecution,
92
79
  remove: removeSessionMeta,
93
80
  };
81
+ /** Generic plugin-principal ownership, including descendants forked from a
82
+ * plugin-created Session. Portal authorization calls this on every request so
83
+ * a grant cannot cross into console or another plugin's Sessions. */
84
+ export function sessionBelongsToPlugin(pluginId, sessionId) {
85
+ const seen = new Set();
86
+ let current = sessionRegistry.get(sessionId);
87
+ while (current && !seen.has(current.id)) {
88
+ if (current.source === `plugin:${pluginId}`)
89
+ return true;
90
+ seen.add(current.id);
91
+ current = current.forkedFromSessionId
92
+ ? sessionRegistry.get(current.forkedFromSessionId)
93
+ : undefined;
94
+ }
95
+ return false;
96
+ }
94
97
  function rowToMeta(row) {
95
98
  return {
96
99
  id: row.id,
97
- provider: row.provider === "codex" || row.provider === "traex" || row.provider === "claude"
98
- ? row.provider
99
- : undefined,
100
- agent: row.agent ?? undefined,
101
- config: row.config ? JSON.parse(row.config) : undefined,
102
- model: row.model ?? undefined,
103
- reasoningEffort: (row.reasoning_effort ?? undefined),
100
+ workspace: sessionWorkspaceSnapshotSchema.parse(JSON.parse(row.workspace)),
101
+ execution: resolvedExecutionSnapshotSchema.parse(JSON.parse(row.execution)),
104
102
  // Normalize on read as well so pre-boundary database rows cannot poison an
105
103
  // entire Remote Session list page after upgrading the daemon.
106
104
  title: normalizeSessionTitle(row.title ?? undefined),
107
- source: row.source ?? "console",
105
+ ...(row.forked_from_session_id
106
+ ? { forkedFromSessionId: row.forked_from_session_id }
107
+ : {}),
108
+ source: row.source,
108
109
  createdAt: row.created_at,
109
110
  updatedAt: row.updated_at ?? row.created_at,
110
111
  };
@@ -0,0 +1,29 @@
1
+ export type SessionPortalScope = "plugin_sessions" | "single_session";
2
+ export type SessionPortalAccess = "read_only" | "read_write";
3
+ export interface SessionPortalAuthorization {
4
+ portalId: string;
5
+ pluginId: string;
6
+ scope: SessionPortalScope;
7
+ access: SessionPortalAccess;
8
+ sessionId?: string;
9
+ expiresAt: string;
10
+ }
11
+ /** Issue a browser handoff into the native Session workbench. The Host knows
12
+ * only the generic Session scope and access level; product/user policy stays in
13
+ * the plugin that asks for the ticket. */
14
+ export declare function issueSessionPortalTicket(input: {
15
+ pluginId: string;
16
+ scope: SessionPortalScope;
17
+ access: SessionPortalAccess;
18
+ sessionId?: string;
19
+ now?: Date;
20
+ }): {
21
+ path: string;
22
+ expiresAt: string;
23
+ };
24
+ /** Atomically consume a one-use ticket and mint a fixed-lifetime browser grant. */
25
+ export declare function redeemSessionPortalTicket(ticket: string, now?: Date): (SessionPortalAuthorization & {
26
+ secret: string;
27
+ }) | undefined;
28
+ export declare function authorizeSessionPortalGrant(portalId: string, secret: string, now?: Date): SessionPortalAuthorization | undefined;
29
+ export declare function pruneSessionPortalAuthorization(now?: Date): void;