gestalt-mobile 0.20.1 → 0.22.0

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 (35) hide show
  1. package/dist/client/assets/index-Db9p-xm2.css +1 -0
  2. package/dist/client/assets/index-NHzJXg1w.js +29 -0
  3. package/dist/client/index.html +2 -2
  4. package/dist/server/server/app.js +2 -0
  5. package/dist/server/server/composition.contracts.js +142 -6
  6. package/dist/server/server/composition.js +39 -8
  7. package/dist/server/server/features/agent-activity/model.js +5 -0
  8. package/dist/server/server/features/agent-activity/registry.js +12 -1
  9. package/dist/server/server/features/autopilot/application/service.js +53 -1
  10. package/dist/server/server/features/files/application/ports.js +6 -0
  11. package/dist/server/server/features/files/copy-entry/endpoint.js +48 -0
  12. package/dist/server/server/features/files/delete-entry/endpoint.js +23 -0
  13. package/dist/server/server/features/files/domain/relative-directory.js +15 -0
  14. package/dist/server/server/features/files/domain/workspace-directory.js +6 -0
  15. package/dist/server/server/features/files/list-directory/endpoint.js +49 -0
  16. package/dist/server/server/features/files/move-entry/endpoint.js +4 -0
  17. package/dist/server/server/features/files/register-routes.js +19 -0
  18. package/dist/server/server/features/files/upload-file/endpoint.js +35 -0
  19. package/dist/server/server/features/sessions/list-sessions/endpoint.js +10 -6
  20. package/dist/server/server/features/sessions/model/relay-session.js +19 -0
  21. package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +13 -3
  22. package/dist/server/server/features/sessions/register-routes.js +1 -0
  23. package/dist/server/server/features/sessions/start-session/use-case.js +3 -5
  24. package/dist/server/server/platform/auth/sqlite-authorization-store.js +98 -205
  25. package/dist/server/server/platform/auth/sqlite.js +54 -0
  26. package/dist/server/server/platform/codex/activity-facts.js +4 -0
  27. package/dist/server/server/platform/codex/recent-thread-lister.js +37 -11
  28. package/dist/server/server/platform/codex/rollout-execution-policy.js +68 -0
  29. package/dist/server/server/platform/codex/session-runtime.js +76 -17
  30. package/dist/server/server/platform/filesystem/filesystem-workspace-files.js +559 -0
  31. package/dist/server/server/platform/persistence/migrate.js +5 -1
  32. package/dist/server/server/platform/persistence/sqlite-session-repository.js +11 -3
  33. package/package.json +1 -1
  34. package/dist/client/assets/index-CnXbn39K.js +0 -29
  35. package/dist/client/assets/index-DYERmevO.css +0 -1
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright (C) 2026 Dyne.org foundation
3
+ * Designed by Denis Roio <jaromil@dyne.org>
4
+ * SPDX-License-Identifier: AGPL-3.0-or-later
5
+ */
6
+ import { createReadStream } from 'node:fs';
7
+ import { isAbsolute } from 'node:path';
8
+ import { createInterface } from 'node:readline';
9
+ import { createSessionExecutionPolicy, } from '../../features/sessions/model/relay-session.js';
10
+ /** Reads policy metadata only; prompts and model output are never retained or logged. */
11
+ export async function readRolloutExecutionPolicy(rolloutPath) {
12
+ if (!isAbsolute(rolloutPath))
13
+ return undefined;
14
+ let gestaltOrigin = false;
15
+ let first;
16
+ let last;
17
+ try {
18
+ const lines = createInterface({
19
+ input: createReadStream(rolloutPath, { encoding: 'utf8' }),
20
+ crlfDelay: Infinity,
21
+ });
22
+ for await (const line of lines) {
23
+ let record;
24
+ try {
25
+ record = JSON.parse(line);
26
+ }
27
+ catch {
28
+ continue;
29
+ }
30
+ if (!record || typeof record !== 'object')
31
+ continue;
32
+ const { type, payload } = record;
33
+ if (!payload || typeof payload !== 'object')
34
+ continue;
35
+ if (type === 'session_meta') {
36
+ gestaltOrigin = payload.originator === 'gestalt-mobile';
37
+ continue;
38
+ }
39
+ if (type !== 'turn_context')
40
+ continue;
41
+ const policy = decodeExecutionPolicy(payload);
42
+ if (!policy)
43
+ continue;
44
+ first ??= policy;
45
+ last = policy;
46
+ }
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ // Gestalt does not mutate policy after thread creation. Its first context is
52
+ // therefore authoritative and cannot be contaminated by an earlier bad import.
53
+ return gestaltOrigin ? first : last;
54
+ }
55
+ function decodeExecutionPolicy(payload) {
56
+ const sandboxPolicy = payload.sandbox_policy;
57
+ const sandbox = sandboxPolicy && typeof sandboxPolicy === 'object'
58
+ ? sandboxPolicy.type
59
+ : undefined;
60
+ const approvalPolicy = payload.approval_policy;
61
+ if (sandbox !== 'read-only' && sandbox !== 'workspace-write' && sandbox !== 'danger-full-access')
62
+ return undefined;
63
+ if (approvalPolicy !== 'untrusted' &&
64
+ approvalPolicy !== 'on-request' &&
65
+ approvalPolicy !== 'never')
66
+ return undefined;
67
+ return createSessionExecutionPolicy({ sandbox, approvalPolicy });
68
+ }
@@ -22,11 +22,13 @@ class SessionResource {
22
22
  unregister;
23
23
  onDisposed;
24
24
  disposed = false;
25
+ explicitShutdown = false;
25
26
  pendingRequests = new Map();
26
27
  threadId;
27
28
  pendingThreadName;
28
29
  writtenThreadName;
29
30
  capabilities = new Map();
31
+ spawnedAgentModels = new Map();
30
32
  constructor(sessionId, process, planStatusLease, planMeasurementToken, unregister, onDisposed) {
31
33
  this.sessionId = sessionId;
32
34
  this.process = process;
@@ -50,6 +52,12 @@ class SessionResource {
50
52
  this.onDisposed();
51
53
  return true;
52
54
  }
55
+ beginExplicitShutdown() {
56
+ this.explicitShutdown = true;
57
+ }
58
+ get exitedUnexpectedly() {
59
+ return !this.explicitShutdown;
60
+ }
53
61
  get active() {
54
62
  return !this.disposed;
55
63
  }
@@ -90,14 +98,14 @@ export class CodexSessionRuntime {
90
98
  sessions = new Map();
91
99
  historyReads = new Map();
92
100
  writerAcquisitions = new Map();
93
- async start(session, now, settings = {}) {
101
+ async start(session, now) {
94
102
  const resource = await this.createResource(session);
95
103
  try {
96
104
  await resource.process.rpc.request('initialize', {
97
105
  clientInfo: { name: 'gestalt-mobile', version: '0.1.0' },
98
106
  capabilities: { experimentalApi: true },
99
107
  });
100
- const startedThreadId = await this.startThread(resource.process, session, settings);
108
+ const startedThreadId = await this.startThread(resource.process, session);
101
109
  resource.threadId = startedThreadId;
102
110
  this.sessions.set(session.id, resource);
103
111
  await this.writePendingThreadName(session.id);
@@ -111,10 +119,15 @@ export class CodexSessionRuntime {
111
119
  }
112
120
  }
113
121
  stop(sessionId) {
114
- this.sessions.get(sessionId)?.dispose();
122
+ const resource = this.sessions.get(sessionId);
123
+ resource?.beginExplicitShutdown();
124
+ resource?.dispose();
115
125
  }
116
126
  async release(sessionId) {
117
127
  const resource = this.sessions.get(sessionId);
128
+ // Mark intent before the await below: a process-exit callback can race the
129
+ // unsubscribe response, but it must not turn an explicit close into recovery.
130
+ resource?.beginExplicitShutdown();
118
131
  if (resource?.threadId) {
119
132
  try {
120
133
  await resource.process.rpc.request('thread/unsubscribe', { threadId: resource.threadId });
@@ -127,7 +140,12 @@ export class CodexSessionRuntime {
127
140
  }
128
141
  /** Releases all relay-owned app-server children during graceful shutdown. */
129
142
  stopAll() {
130
- for (const resource of [...this.sessions.values()])
143
+ // Exit delivery can already be queued when shutdown begins. Declare the
144
+ // shared intent for every owner before closing any process.
145
+ const resources = [...this.sessions.values()];
146
+ for (const resource of resources)
147
+ resource.beginExplicitShutdown();
148
+ for (const resource of resources)
131
149
  resource.dispose();
132
150
  }
133
151
  /** Keeps a detached session's supervised-plan status observable without launching Codex. */
@@ -236,7 +254,7 @@ export class CodexSessionRuntime {
236
254
  throw new Error('CODEX_THREAD_ID_MISSING');
237
255
  const owned = this.sessions.get(session.id);
238
256
  if (owned)
239
- return this.decodeHistory(owned.process, session.threadId);
257
+ return this.decodeHistory(owned.process, session.threadId, owned);
240
258
  const existing = this.historyReads.get(session.id);
241
259
  if (existing)
242
260
  return existing;
@@ -294,6 +312,9 @@ export class CodexSessionRuntime {
294
312
  ...(typeof value.agentRole === 'string' && value.agentRole.length <= 128
295
313
  ? { role: value.agentRole }
296
314
  : {}),
315
+ ...(owned.spawnedAgentModels.get(value.id)
316
+ ? { model: owned.spawnedAgentModels.get(value.id) }
317
+ : {}),
297
318
  },
298
319
  ];
299
320
  }));
@@ -329,8 +350,14 @@ export class CodexSessionRuntime {
329
350
  process.close();
330
351
  }
331
352
  }
332
- async decodeHistory(process, threadId) {
333
- const result = decodeThreadRead(await process.rpc.request('thread/read', { threadId, includeTurns: true }));
353
+ async decodeHistory(process, threadId, resource) {
354
+ const response = await process.rpc.request('thread/read', { threadId, includeTurns: true });
355
+ if (resource) {
356
+ resource.spawnedAgentModels.clear();
357
+ for (const [childId, model] of decodeSpawnedAgentModels(response))
358
+ resource.spawnedAgentModels.set(childId, model);
359
+ }
360
+ const result = decodeThreadRead(response);
334
361
  const rawTurns = result;
335
362
  const activeTurn = rawTurns.find((turn) => turn.status === 'inProgress' && typeof turn.id === 'string');
336
363
  return {
@@ -360,6 +387,10 @@ export class CodexSessionRuntime {
360
387
  await resource.process.rpc.request('thread/resume', {
361
388
  threadId: session.threadId,
362
389
  cwd: session.workspacePath,
390
+ ...(session.executionPolicy?.approvalPolicy
391
+ ? { approvalPolicy: session.executionPolicy.approvalPolicy }
392
+ : {}),
393
+ ...(session.executionPolicy?.sandbox ? { sandbox: session.executionPolicy.sandbox } : {}),
363
394
  dynamicTools: [gestaltQuizDynamicTool, gestaltOrgPlanAttentionDynamicTool],
364
395
  });
365
396
  result = {
@@ -371,9 +402,7 @@ export class CodexSessionRuntime {
371
402
  catch (error) {
372
403
  if (!canRebindMissingRollout(session, error))
373
404
  throw error;
374
- const replacementThreadId = await this.startThread(resource.process, session, {
375
- model: session.model,
376
- });
405
+ const replacementThreadId = await this.startThread(resource.process, session);
377
406
  result = rebindMissingRollout(session, error, replacementThreadId, now);
378
407
  }
379
408
  resource.threadId = result.session.threadId;
@@ -432,16 +461,16 @@ export class CodexSessionRuntime {
432
461
  resource.pendingRequests.set(requestId, { resolve, reject });
433
462
  });
434
463
  }
435
- async startThread(process, session, settings = {}) {
436
- return decodeThreadStart(await process.rpc.request('thread/start', this.threadStartParams(session, settings)));
464
+ async startThread(process, session) {
465
+ return decodeThreadStart(await process.rpc.request('thread/start', this.threadStartParams(session)));
437
466
  }
438
- threadStartParams(session, settings) {
467
+ threadStartParams(session) {
439
468
  return {
440
469
  cwd: session.workspacePath,
441
- approvalPolicy: settings.approvalPolicy ?? 'on-request',
470
+ approvalPolicy: session.executionPolicy?.approvalPolicy ?? 'on-request',
442
471
  dynamicTools: [gestaltQuizDynamicTool, gestaltOrgPlanAttentionDynamicTool],
443
- ...(settings.model ? { model: settings.model } : {}),
444
- ...(settings.sandbox ? { sandbox: settings.sandbox } : {}),
472
+ ...(session.model ? { model: session.model } : {}),
473
+ ...(session.executionPolicy?.sandbox ? { sandbox: session.executionPolicy.sandbox } : {}),
445
474
  };
446
475
  }
447
476
  async createResource(session) {
@@ -477,7 +506,7 @@ export class CodexSessionRuntime {
477
506
  // Own process exit before resume/initialization can make this resource appear healthy.
478
507
  // A short-lived child could otherwise leave a durable ready session without a writer.
479
508
  const exitUnsubscribe = process.onExit?.(() => {
480
- if (resource.dispose())
509
+ if (resource.dispose() && resource.exitedUnexpectedly)
481
510
  this.onProcessExit?.(session.id);
482
511
  }) ?? (() => { });
483
512
  resource.attach([exitUnsubscribe]);
@@ -666,6 +695,36 @@ function decodeHistoryItem(value) {
666
695
  }
667
696
  return [];
668
697
  }
698
+ /** Recovers optional spawn metadata without exposing collaboration prompts to chat history. */
699
+ function decodeSpawnedAgentModels(value) {
700
+ const models = new Map();
701
+ const turns = asRecord(asRecord(value)?.thread)?.turns;
702
+ if (!Array.isArray(turns) || turns.length > 10_000)
703
+ return models;
704
+ for (const turn of turns) {
705
+ const items = asRecord(turn)?.items;
706
+ if (!Array.isArray(items) || items.length > 10_000)
707
+ continue;
708
+ for (const candidate of items) {
709
+ const item = asRecord(candidate);
710
+ const model = boundedString(item?.model, 256);
711
+ if (item?.type !== 'collabAgentToolCall' ||
712
+ (item.tool !== 'spawnAgent' && item.tool !== 'spawn_agent') ||
713
+ !model ||
714
+ !Array.isArray(item.receiverThreadIds) ||
715
+ item.receiverThreadIds.length > 64)
716
+ continue;
717
+ for (const receiver of item.receiverThreadIds) {
718
+ const childId = boundedString(receiver, 256);
719
+ if (childId)
720
+ models.set(childId, model);
721
+ if (models.size >= 64)
722
+ return models;
723
+ }
724
+ }
725
+ }
726
+ return models;
727
+ }
669
728
  function decodeUserMessageContent(value) {
670
729
  if (!Array.isArray(value) || value.length > 1_000)
671
730
  return [];