@awak-app/simy-cli 0.7.41 → 0.7.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.7.41",
3
+ "version": "0.7.42",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,7 @@ import {
14
14
  } from "./durable-local-task-steps-contract.js";
15
15
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
16
16
 
17
- export const CLI_VERSION = "0.7.41";
17
+ export const CLI_VERSION = "0.7.42";
18
18
  export const CLI_API_CONTRACT_VERSION = 10;
19
19
 
20
20
  export function narrativeImageCapabilityMetadata() {
@@ -1,20 +1,53 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
2
 
3
- function updateRows(previous, incoming, full, unchanged = false) {
3
+ const catalogScope = status => JSON.stringify([status.webOrigin, status.auth?.userId, status.auth?.state,
4
+ status.access?.organizationId, status.access?.revision, status.access?.state]);
5
+
6
+ function updateRows(previous, incoming, full, { unchanged = false, immutableInput = false, copyOnWrite = false } = {}) {
4
7
  if (full || previous === null || previous.length !== incoming.length) {
5
- const rows = structuredClone(incoming);
8
+ const rows = immutableInput ? incoming : structuredClone(incoming);
6
9
  return { rows, payload: { rows } };
7
10
  }
8
11
  if (unchanged) return { rows: previous, payload: { count: previous.length, changes: [] } };
9
12
  const changes = [];
13
+ let rows = previous;
10
14
  for (let index = 0; index < incoming.length; index++) {
11
15
  if (!isDeepStrictEqual(previous[index], incoming[index])) {
12
- const row = structuredClone(incoming[index]);
13
- previous[index] = row;
16
+ const row = immutableInput ? incoming[index] : structuredClone(incoming[index]);
17
+ if (!immutableInput) {
18
+ if (copyOnWrite && rows === previous) rows = previous.slice();
19
+ rows[index] = row;
20
+ }
14
21
  changes.push([index, row]);
15
22
  }
16
23
  }
17
- return { rows: previous, payload: { count: previous.length, changes } };
24
+ return { rows: immutableInput ? incoming : rows, payload: { count: incoming.length, changes } };
25
+ }
26
+
27
+ // Detach mutable producer data once. Unchanged catalog rows may be shared only
28
+ // between these owned read-only snapshots; changed rows get detached copies and
29
+ // a new array. Older queued frames and durable writes must remain immutable.
30
+ export function createDesktopStatusSnapshot() {
31
+ let rows = null, candidates = null, scope = null;
32
+ return status => {
33
+ const supervision = status?.sessionSupervision;
34
+ if (!Array.isArray(supervision?.sessions)) {
35
+ rows = candidates = scope = null;
36
+ return structuredClone(status);
37
+ }
38
+ const nextScope = catalogScope(status);
39
+ if (nextScope !== scope) { rows = candidates = null; scope = nextScope; }
40
+ const hasCandidates = Array.isArray(supervision.selectionCandidates);
41
+ const detached = structuredClone({ ...status, sessionSupervision: { ...supervision,
42
+ sessions: [], ...(hasCandidates ? { selectionCandidates: [] } : {}) } });
43
+ rows = updateRows(rows, supervision.sessions, false, { copyOnWrite: true }).rows;
44
+ detached.sessionSupervision.sessions = rows;
45
+ if (hasCandidates) {
46
+ candidates = updateRows(candidates, supervision.selectionCandidates, false, { copyOnWrite: true }).rows;
47
+ detached.sessionSupervision.selectionCandidates = candidates;
48
+ } else candidates = null;
49
+ return detached;
50
+ };
18
51
  }
19
52
 
20
53
  // One encoder per ordered output stream. Call only when a frame actually enters
@@ -30,11 +63,10 @@ export function createDesktopCatalogEncoder({ immutableInput = false } = {}) {
30
63
  }
31
64
  const status = frame.status;
32
65
  const incoming = status.sessionSupervision.sessions;
33
- const identity = JSON.stringify([status.webOrigin, status.auth?.userId, status.auth?.state, status.access?.organizationId,
34
- status.access?.revision, status.access?.state]);
66
+ const identity = catalogScope(status);
35
67
  const previous = revision++;
36
68
  const full = rows === null || scope !== identity || rows.length !== incoming.length;
37
- const snapshot = updateRows(rows, incoming, full, immutableInput && sourceRows === incoming);
69
+ const snapshot = updateRows(rows, incoming, full, { immutableInput, unchanged: immutableInput && sourceRows === incoming });
38
70
  rows = snapshot.rows; sourceRows = incoming; scope = identity;
39
71
  const catalog = { version: 1, revision, scope, ...(full ? {} : { baseRevision: previous }), ...snapshot.payload };
40
72
  const { sessions, ...metadata } = status.sessionSupervision;
@@ -42,7 +74,8 @@ export function createDesktopCatalogEncoder({ immutableInput = false } = {}) {
42
74
  if (Array.isArray(metadata.selectionCandidates)) {
43
75
  // This is another complete directory, used by the conversation picker.
44
76
  // Do not silently retransmit it as supposedly small status metadata.
45
- const snapshot = updateRows(candidates, metadata.selectionCandidates, full, immutableInput && sourceCandidates === metadata.selectionCandidates);
77
+ const snapshot = updateRows(candidates, metadata.selectionCandidates, full,
78
+ { immutableInput, unchanged: immutableInput && sourceCandidates === metadata.selectionCandidates });
46
79
  candidates = snapshot.rows; sourceCandidates = metadata.selectionCandidates; catalog.selectionCandidates = snapshot.payload;
47
80
  const { selectionCandidates, ...small } = metadata;
48
81
  supervision = small;
@@ -1,5 +1,5 @@
1
1
  import { createDesktopFrameWriter } from './desktop-frame-writer.js';
2
- import { createDesktopCatalogEncoder } from './desktop-catalog-frames.js';
2
+ import { createDesktopCatalogEncoder, createDesktopStatusSnapshot } from './desktop-catalog-frames.js';
3
3
  import { boundedDiscovery } from './session-supervision/progressive-discovery.js';
4
4
  import { createCatalogRefreshPolicy } from './session-supervision/catalog-refresh.js';
5
5
  import { supervisedSession } from './session-supervision/discovery-window.js';
@@ -197,6 +197,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
197
197
  // deltas later, after backpressure coalescing. Registries may mutate objects,
198
198
  // so the comparison snapshot must remain detached from producer references.
199
199
  let encodedStatus = null, lastStatus = null, lastStatusFrame = null, updateProgress = null;
200
+ const snapshotStatus = createDesktopStatusSnapshot();
200
201
  let projectCatalogFrame = status => status;
201
202
  let resolveFirstStatus;
202
203
  const firstStatus = new Promise(resolve => { resolveFirstStatus = resolve; });
@@ -206,6 +207,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
206
207
  let acceptanceReadbackSequence = 0;
207
208
  let remoteHosts;
208
209
  const emitStatus = status => {
210
+ let catalogObservation;
209
211
  status = projectCatalogFrame(status);
210
212
  status = { ...status, windowUpdate: { version: 1, preservesWork: Boolean(ownerTransport), prepared: windowUpdatePrepared } };
211
213
  const updateSettings = updater?.snapshot?.() || {};
@@ -229,16 +231,20 @@ export async function startDesktopHost({ input = process.stdin, output = process
229
231
  if (preferenceScope && desktopAccessReady(status.access) && status.sessionSupervision?.sessions) {
230
232
  const catalogScope = preferenceScope;
231
233
  status = { ...status, sessionSupervision: { ...status.sessionSupervision, sessions: status.sessionSupervision.sessions.filter(row => controllerPreferences.get(catalogScope, row).deleted !== true).map(row => { const saved = controllerPreferences.get(catalogScope, row); return { ...row, controllerPreferences: saved, ...(saved.pinned === undefined ? {} : { listPinned: saved.pinned, listPinScopes: saved.pinScopes }), ...(saved.hidden === undefined ? {} : { localHidden: saved.hidden }) }; }) } };
232
- const catalog = { observedAt: Date.now(), sessions: status.sessionSupervision.sessions, hosts: status.remoteHosts?.items || [], providerLinks };
233
- void controllerPreferences.replaceCatalog(catalogScope, catalog).catch(() => {});
234
+ catalogObservation = { scope: catalogScope, observedAt: Date.now() };
234
235
  }
235
236
  if (updateProgress) status = { ...status, update: { ...status.update, ...updateProgress } };
236
237
  const { sampledAt, ...content } = status;
237
238
  if (!lastStatus || !isDeepStrictEqual(content, lastStatus)) {
238
239
  if (!catalogEncoder) encodedStatus = JSON.stringify(content).slice(1, -1);
239
- lastStatus = structuredClone(content);
240
+ lastStatus = snapshotStatus(content);
240
241
  }
241
242
  const ownedStatus = { ...lastStatus, ...(sampledAt === undefined ? {} : { sampledAt }) };
243
+ if (catalogObservation) {
244
+ const catalog = { observedAt: catalogObservation.observedAt, sessions: ownedStatus.sessionSupervision.sessions,
245
+ hosts: ownedStatus.remoteHosts?.items || [], providerLinks: structuredClone(providerLinks) };
246
+ void controllerPreferences.replaceCatalog(catalogObservation.scope, catalog, { immutableInput: true }).catch(() => {});
247
+ }
242
248
  lastStatusFrame = ownedStatus;
243
249
  ownerTransport?.publishDesktop?.({ type: 'status', status: ownedStatus });
244
250
  const observation = sampledAt === undefined ? '' : `"sampledAt":${JSON.stringify(sampledAt)}${encodedStatus ? ',' : ''}`;
@@ -41,11 +41,13 @@ export function createControllerStore(root) {
41
41
  await persist(scope, next); return result;
42
42
  });
43
43
  }
44
- function replaceCatalog(scope, catalog) {
44
+ function replaceCatalog(scope, catalog, { immutableInput = false } = {}) {
45
45
  // Own the incoming snapshot immediately, but do not clone the previous
46
46
  // multi-MiB catalog just to replace it. Preference writes still serialize
47
47
  // on the same queue and commit only after their atomic write succeeds.
48
- const snapshot = structuredClone(catalog);
48
+ // Only the desktop's detached read-only snapshots may be borrowed. Generic
49
+ // callers retain immediate mutation isolation, including while queued.
50
+ const snapshot = immutableInput ? catalog : structuredClone(catalog);
49
51
  return enqueue(async () => {
50
52
  const settings = { ...await load(scope) };
51
53
  delete settings.catalog;