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.
- package/dist/client/assets/index-Db9p-xm2.css +1 -0
- package/dist/client/assets/index-NHzJXg1w.js +29 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/app.js +2 -0
- package/dist/server/server/composition.contracts.js +142 -6
- package/dist/server/server/composition.js +39 -8
- package/dist/server/server/features/agent-activity/model.js +5 -0
- package/dist/server/server/features/agent-activity/registry.js +12 -1
- package/dist/server/server/features/autopilot/application/service.js +53 -1
- package/dist/server/server/features/files/application/ports.js +6 -0
- package/dist/server/server/features/files/copy-entry/endpoint.js +48 -0
- package/dist/server/server/features/files/delete-entry/endpoint.js +23 -0
- package/dist/server/server/features/files/domain/relative-directory.js +15 -0
- package/dist/server/server/features/files/domain/workspace-directory.js +6 -0
- package/dist/server/server/features/files/list-directory/endpoint.js +49 -0
- package/dist/server/server/features/files/move-entry/endpoint.js +4 -0
- package/dist/server/server/features/files/register-routes.js +19 -0
- package/dist/server/server/features/files/upload-file/endpoint.js +35 -0
- package/dist/server/server/features/sessions/list-sessions/endpoint.js +10 -6
- package/dist/server/server/features/sessions/model/relay-session.js +19 -0
- package/dist/server/server/features/sessions/promote-recent-thread/use-case.js +13 -3
- package/dist/server/server/features/sessions/register-routes.js +1 -0
- package/dist/server/server/features/sessions/start-session/use-case.js +3 -5
- package/dist/server/server/platform/auth/sqlite-authorization-store.js +98 -205
- package/dist/server/server/platform/auth/sqlite.js +54 -0
- package/dist/server/server/platform/codex/activity-facts.js +4 -0
- package/dist/server/server/platform/codex/recent-thread-lister.js +37 -11
- package/dist/server/server/platform/codex/rollout-execution-policy.js +68 -0
- package/dist/server/server/platform/codex/session-runtime.js +76 -17
- package/dist/server/server/platform/filesystem/filesystem-workspace-files.js +559 -0
- package/dist/server/server/platform/persistence/migrate.js +5 -1
- package/dist/server/server/platform/persistence/sqlite-session-repository.js +11 -3
- package/package.json +1 -1
- package/dist/client/assets/index-CnXbn39K.js +0 -29
- 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
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
|
436
|
-
return decodeThreadStart(await process.rpc.request('thread/start', this.threadStartParams(session
|
|
464
|
+
async startThread(process, session) {
|
|
465
|
+
return decodeThreadStart(await process.rpc.request('thread/start', this.threadStartParams(session)));
|
|
437
466
|
}
|
|
438
|
-
threadStartParams(session
|
|
467
|
+
threadStartParams(session) {
|
|
439
468
|
return {
|
|
440
469
|
cwd: session.workspacePath,
|
|
441
|
-
approvalPolicy:
|
|
470
|
+
approvalPolicy: session.executionPolicy?.approvalPolicy ?? 'on-request',
|
|
442
471
|
dynamicTools: [gestaltQuizDynamicTool, gestaltOrgPlanAttentionDynamicTool],
|
|
443
|
-
...(
|
|
444
|
-
...(
|
|
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 [];
|