gestalt-mobile 0.5.0 → 0.6.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-CGAy94nY.js +12 -0
- package/dist/client/assets/index-DSU_Fcvo.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/app.js +18 -4
- package/dist/server/server/composition.js +24 -2
- package/dist/server/server/features/plans/application/parse-supervised-plan.js +242 -0
- package/dist/server/server/features/plans/application/ports.js +6 -0
- package/dist/server/server/features/plans/application/supervised-plan-registry.js +22 -0
- package/dist/server/server/features/plans/close-plan/endpoint.js +33 -0
- package/dist/server/server/features/plans/domain/supervised-plan.js +6 -0
- package/dist/server/server/features/plans/get-plan/endpoint.js +14 -0
- package/dist/server/server/features/sessions/model/relay-session.js +5 -0
- package/dist/server/server/features/sessions/select-model/endpoint.js +23 -0
- package/dist/server/server/platform/codex/codex-process-launcher.js +8 -0
- package/dist/server/server/platform/codex/session-runtime.js +44 -3
- package/dist/server/server/platform/plans/filesystem-plan-status-source.js +301 -0
- package/package.json +3 -1
- package/dist/client/assets/index--vTfuwTT.js +0 -11
- package/dist/client/assets/index-BqpvGqIw.css +0 -1
|
@@ -0,0 +1,242 @@
|
|
|
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 { isAbsolute, relative, resolve } from 'node:path';
|
|
7
|
+
const headingPattern = /^(\*{1,2}) (TODO|WIP|DONE) \[#([ABC])\] (.+)$/;
|
|
8
|
+
const propertyPattern = /^:([A-Z_]+):(?:[ \t](.*))?$/;
|
|
9
|
+
const descriptionPattern = /^- ([A-Za-z][A-Za-z ]*?) ::(?:[ \t](.*))?$/;
|
|
10
|
+
/**
|
|
11
|
+
* Projects only the small, validated org-plan dialect used by supervised plans.
|
|
12
|
+
* `planPath` is used solely for admission control and never retained in the model.
|
|
13
|
+
*/
|
|
14
|
+
export function parseSupervisedPlan(input) {
|
|
15
|
+
if (!isPlanPathWithinWorkspace(input.planPath, input.workspacePath))
|
|
16
|
+
return unavailable('PATH_OUTSIDE_WORKSPACE');
|
|
17
|
+
const lines = input.source.replace(/\r\n?/g, '\n').split('\n');
|
|
18
|
+
const metadata = new Map();
|
|
19
|
+
const headings = [];
|
|
20
|
+
for (let index = 0; index < lines.length;) {
|
|
21
|
+
const line = lines[index];
|
|
22
|
+
const metadataMatch = /^#\+([A-Z]+):(?:[ \t](.*))?$/.exec(line);
|
|
23
|
+
if (metadataMatch) {
|
|
24
|
+
metadata.set(metadataMatch[1], metadataMatch[2] ?? '');
|
|
25
|
+
index += 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!line.startsWith('*')) {
|
|
29
|
+
index += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const headingMatch = headingPattern.exec(line);
|
|
33
|
+
if (!headingMatch)
|
|
34
|
+
return unavailable('MALFORMED_ORG');
|
|
35
|
+
const level = headingMatch[1].length;
|
|
36
|
+
const propertiesStart = lines[index + 1];
|
|
37
|
+
if (propertiesStart !== ':PROPERTIES:')
|
|
38
|
+
return unavailable('MALFORMED_ORG');
|
|
39
|
+
const properties = new Map();
|
|
40
|
+
index += 2;
|
|
41
|
+
let closed = false;
|
|
42
|
+
for (; index < lines.length; index += 1) {
|
|
43
|
+
const propertyLine = lines[index];
|
|
44
|
+
if (propertyLine === ':END:') {
|
|
45
|
+
closed = true;
|
|
46
|
+
index += 1;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const property = propertyPattern.exec(propertyLine);
|
|
50
|
+
if (!property || properties.has(property[1]))
|
|
51
|
+
return unavailable('MALFORMED_ORG');
|
|
52
|
+
properties.set(property[1], property[2] ?? '');
|
|
53
|
+
}
|
|
54
|
+
if (!closed)
|
|
55
|
+
return unavailable('MALFORMED_ORG');
|
|
56
|
+
const descriptions = new Map();
|
|
57
|
+
while (index < lines.length && !lines[index].startsWith('*')) {
|
|
58
|
+
const description = descriptionPattern.exec(lines[index]);
|
|
59
|
+
if (description) {
|
|
60
|
+
const key = description[1];
|
|
61
|
+
if (descriptions.has(key))
|
|
62
|
+
return unavailable('MALFORMED_ORG');
|
|
63
|
+
descriptions.set(key, description[2] ?? '');
|
|
64
|
+
}
|
|
65
|
+
else if (lines[index].trim() !== '') {
|
|
66
|
+
return unavailable('MALFORMED_ORG');
|
|
67
|
+
}
|
|
68
|
+
index += 1;
|
|
69
|
+
}
|
|
70
|
+
const id = properties.get('ID');
|
|
71
|
+
if (!id)
|
|
72
|
+
return unavailable('MISSING_REQUIRED_FIELD');
|
|
73
|
+
headings.push({
|
|
74
|
+
id,
|
|
75
|
+
title: headingMatch[4],
|
|
76
|
+
level,
|
|
77
|
+
state: headingMatch[2],
|
|
78
|
+
priority: headingMatch[3],
|
|
79
|
+
properties,
|
|
80
|
+
descriptions,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const title = metadata.get('TITLE');
|
|
84
|
+
if (!title)
|
|
85
|
+
return unavailable('MISSING_TITLE');
|
|
86
|
+
if (!headings.length)
|
|
87
|
+
return unavailable('MALFORMED_ORG');
|
|
88
|
+
if (new Set(headings.map((heading) => heading.id)).size !== headings.length)
|
|
89
|
+
return unavailable('DUPLICATE_ID');
|
|
90
|
+
if (!hasValidWipPath(headings))
|
|
91
|
+
return unavailable('MULTIPLE_WIP');
|
|
92
|
+
const built = buildSteps(headings);
|
|
93
|
+
if (!built)
|
|
94
|
+
return unavailable('MISSING_REQUIRED_FIELD');
|
|
95
|
+
const allSteps = headings;
|
|
96
|
+
const allDone = allSteps.every((step) => step.state === 'DONE');
|
|
97
|
+
const currentStepId = findCurrentStepId(allSteps, allDone);
|
|
98
|
+
if (!currentStepId)
|
|
99
|
+
return unavailable('MALFORMED_ORG');
|
|
100
|
+
return {
|
|
101
|
+
kind: 'available',
|
|
102
|
+
plan: freezePlan({
|
|
103
|
+
title,
|
|
104
|
+
...(metadata.has('SUBTITLE') ? { subtitle: metadata.get('SUBTITLE') } : {}),
|
|
105
|
+
...(metadata.has('DATE') ? { date: metadata.get('DATE') } : {}),
|
|
106
|
+
...(metadata.has('KEYWORDS') ? { keywords: metadata.get('KEYWORDS') } : {}),
|
|
107
|
+
steps: built,
|
|
108
|
+
totalSteps: allSteps.length,
|
|
109
|
+
doneSteps: allSteps.filter((step) => step.state === 'DONE').length,
|
|
110
|
+
allDone,
|
|
111
|
+
currentStepId,
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function isPlanPathWithinWorkspace(planPath, workspacePath) {
|
|
116
|
+
if (!isAbsolute(planPath) || !isAbsolute(workspacePath))
|
|
117
|
+
return false;
|
|
118
|
+
const pathWithinWorkspace = relative(resolve(workspacePath), resolve(planPath));
|
|
119
|
+
return (pathWithinWorkspace === '' ||
|
|
120
|
+
(!pathWithinWorkspace.startsWith('..') && !isAbsolute(pathWithinWorkspace)));
|
|
121
|
+
}
|
|
122
|
+
function buildSteps(headings) {
|
|
123
|
+
const parents = [];
|
|
124
|
+
for (let index = 0; index < headings.length; index += 1) {
|
|
125
|
+
const heading = headings[index];
|
|
126
|
+
if (heading.level === 1) {
|
|
127
|
+
const step = makeStep(heading);
|
|
128
|
+
if (!step)
|
|
129
|
+
return null;
|
|
130
|
+
parents.push(step);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const parent = parents.at(-1);
|
|
134
|
+
if (!parent)
|
|
135
|
+
return null;
|
|
136
|
+
const child = makeStep(heading);
|
|
137
|
+
if (!child)
|
|
138
|
+
return null;
|
|
139
|
+
parent.children.push(child);
|
|
140
|
+
}
|
|
141
|
+
return parents;
|
|
142
|
+
}
|
|
143
|
+
function makeStep(heading) {
|
|
144
|
+
const description = descriptionFor(heading);
|
|
145
|
+
if (!description)
|
|
146
|
+
return null;
|
|
147
|
+
if (heading.level === 1) {
|
|
148
|
+
const reviewStatus = heading.properties.get('REVIEW_STATUS');
|
|
149
|
+
const skills = heading.properties.get('SKILLS');
|
|
150
|
+
if ((reviewStatus !== 'UNREVIEWED' && reviewStatus !== 'REVIEWED') || skills === undefined)
|
|
151
|
+
return null;
|
|
152
|
+
return {
|
|
153
|
+
id: heading.id,
|
|
154
|
+
title: heading.title,
|
|
155
|
+
level: heading.level,
|
|
156
|
+
state: heading.state,
|
|
157
|
+
priority: heading.priority,
|
|
158
|
+
reviewStatus: reviewStatus,
|
|
159
|
+
skills: skills.split(/\s+/).filter(Boolean),
|
|
160
|
+
description,
|
|
161
|
+
children: [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (heading.properties.has('REVIEW_STATUS') || heading.properties.has('SKILLS'))
|
|
165
|
+
return null;
|
|
166
|
+
return {
|
|
167
|
+
id: heading.id,
|
|
168
|
+
title: heading.title,
|
|
169
|
+
level: heading.level,
|
|
170
|
+
state: heading.state,
|
|
171
|
+
priority: heading.priority,
|
|
172
|
+
description,
|
|
173
|
+
children: [],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function descriptionFor(heading) {
|
|
177
|
+
const fields = heading.level === 1 ? ['Effort', 'Goal', 'Notes'] : ['Why', 'Change', 'Tests', 'Done when'];
|
|
178
|
+
if (!fields.every((field) => heading.descriptions.has(field)))
|
|
179
|
+
return null;
|
|
180
|
+
const description = heading.descriptions;
|
|
181
|
+
return heading.level === 1
|
|
182
|
+
? {
|
|
183
|
+
effort: description.get('Effort'),
|
|
184
|
+
goal: description.get('Goal'),
|
|
185
|
+
notes: description.get('Notes'),
|
|
186
|
+
}
|
|
187
|
+
: {
|
|
188
|
+
why: description.get('Why'),
|
|
189
|
+
change: description.get('Change'),
|
|
190
|
+
tests: description.get('Tests'),
|
|
191
|
+
doneWhen: description.get('Done when'),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function hasValidWipPath(headings) {
|
|
195
|
+
const wipByLevel = new Map();
|
|
196
|
+
for (const heading of headings) {
|
|
197
|
+
if (heading.state !== 'WIP')
|
|
198
|
+
continue;
|
|
199
|
+
const sameLevel = wipByLevel.get(heading.level) ?? [];
|
|
200
|
+
sameLevel.push(heading);
|
|
201
|
+
wipByLevel.set(heading.level, sameLevel);
|
|
202
|
+
}
|
|
203
|
+
if ([...wipByLevel.values()].some((steps) => steps.length > 1))
|
|
204
|
+
return false;
|
|
205
|
+
const l2Wip = wipByLevel.get(2)?.[0];
|
|
206
|
+
if (!l2Wip)
|
|
207
|
+
return true;
|
|
208
|
+
const parentIndex = headings.indexOf(l2Wip) - 1;
|
|
209
|
+
for (let index = parentIndex; index >= 0; index -= 1) {
|
|
210
|
+
if (headings[index].level === 1)
|
|
211
|
+
return headings[index].state === 'WIP';
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
function findCurrentStepId(headings, allDone) {
|
|
216
|
+
const wip = headings
|
|
217
|
+
.filter((heading) => heading.state === 'WIP')
|
|
218
|
+
.sort((a, b) => b.level - a.level)[0];
|
|
219
|
+
if (wip)
|
|
220
|
+
return wip.id;
|
|
221
|
+
const review = headings.find((heading) => heading.level === 1 &&
|
|
222
|
+
heading.state === 'DONE' &&
|
|
223
|
+
heading.properties.get('REVIEW_STATUS') === 'UNREVIEWED');
|
|
224
|
+
if (review)
|
|
225
|
+
return review.id;
|
|
226
|
+
const todo = headings.find((heading) => heading.state === 'TODO');
|
|
227
|
+
if (todo)
|
|
228
|
+
return todo.id;
|
|
229
|
+
return allDone ? (headings.findLast((heading) => heading.level === 1)?.id ?? null) : null;
|
|
230
|
+
}
|
|
231
|
+
function freezePlan(plan) {
|
|
232
|
+
const freezeStep = (step) => Object.freeze({
|
|
233
|
+
...step,
|
|
234
|
+
...(step.skills ? { skills: Object.freeze([...step.skills]) } : {}),
|
|
235
|
+
description: Object.freeze({ ...step.description }),
|
|
236
|
+
children: Object.freeze(step.children.map(freezeStep)),
|
|
237
|
+
});
|
|
238
|
+
return Object.freeze({ ...plan, steps: Object.freeze(plan.steps.map(freezeStep)) });
|
|
239
|
+
}
|
|
240
|
+
function unavailable(reason) {
|
|
241
|
+
return { kind: 'unavailable', reason };
|
|
242
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
/** Retains at most one validated plan projection for each relay session. */
|
|
7
|
+
export class SupervisedPlanRegistry {
|
|
8
|
+
plans = new Map();
|
|
9
|
+
accept(sessionId, update) {
|
|
10
|
+
if (update.kind === 'updated')
|
|
11
|
+
this.plans.set(sessionId, { plan: update.plan, identity: update.identity });
|
|
12
|
+
}
|
|
13
|
+
find(sessionId) {
|
|
14
|
+
return this.plans.get(sessionId)?.plan ?? null;
|
|
15
|
+
}
|
|
16
|
+
identity(sessionId) {
|
|
17
|
+
return this.plans.get(sessionId)?.identity ?? null;
|
|
18
|
+
}
|
|
19
|
+
clear(sessionId) {
|
|
20
|
+
this.plans.delete(sessionId);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { problem } from '../../../platform/http/problem.js';
|
|
7
|
+
export function registerClosePlan(app, deps) {
|
|
8
|
+
app.delete('/api/sessions/:id/plan', async (request, reply) => {
|
|
9
|
+
const id = request.params.id;
|
|
10
|
+
if (!deps.exists(id))
|
|
11
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
12
|
+
const plan = deps.find(id);
|
|
13
|
+
if (!plan)
|
|
14
|
+
return reply.code(204).send();
|
|
15
|
+
if (!plan.allDone)
|
|
16
|
+
return reply
|
|
17
|
+
.code(409)
|
|
18
|
+
.type('application/problem+json')
|
|
19
|
+
.send(problem('PLAN_INCOMPLETE', 409, 'Only completed supervised plans can be closed.'));
|
|
20
|
+
try {
|
|
21
|
+
await deps.removeStatus(id);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return reply
|
|
25
|
+
.code(503)
|
|
26
|
+
.type('application/problem+json')
|
|
27
|
+
.send(problem('PLAN_CLOSE_UNAVAILABLE', 503, 'The relay could not close the supervised plan.', true));
|
|
28
|
+
}
|
|
29
|
+
deps.clear(id);
|
|
30
|
+
deps.closed(id);
|
|
31
|
+
return reply.code(204).send();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
export function registerGetPlan(app, deps) {
|
|
7
|
+
app.get('/api/sessions/:id/plan', (request, reply) => {
|
|
8
|
+
const id = request.params.id;
|
|
9
|
+
if (!deps.exists(id))
|
|
10
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
11
|
+
const plan = deps.find(id);
|
|
12
|
+
return plan ? reply.send(plan) : reply.code(204).send();
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -67,6 +67,11 @@ export class RelaySession {
|
|
|
67
67
|
bindThread(value, now) {
|
|
68
68
|
return this.transition({ threadId: threadId(value), state: 'ready' }, 'ThreadBound', now);
|
|
69
69
|
}
|
|
70
|
+
selectModel(value, now) {
|
|
71
|
+
if (this.value.state !== 'ready')
|
|
72
|
+
throw new DomainError('SESSION_NOT_READY');
|
|
73
|
+
return this.transition({ model: value }, 'ModelSelected', now);
|
|
74
|
+
}
|
|
70
75
|
startTurn(value, now) {
|
|
71
76
|
if (this.value.state === 'turnActive')
|
|
72
77
|
throw new DomainError('SESSION_TURN_ACTIVE');
|
|
@@ -0,0 +1,23 @@
|
|
|
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 { RelaySession } from '../model/relay-session.js';
|
|
7
|
+
export function registerSelectModel(app, deps) {
|
|
8
|
+
app.post('/api/sessions/:id/model', async (request, reply) => {
|
|
9
|
+
const session = deps.find(request.params.id);
|
|
10
|
+
if (!session)
|
|
11
|
+
return reply.code(404).send({ code: 'SESSION_NOT_FOUND' });
|
|
12
|
+
const model = request.body.model;
|
|
13
|
+
if (typeof model !== 'string')
|
|
14
|
+
return reply.code(400).send({ code: 'MODEL_REQUIRED' });
|
|
15
|
+
if (!(await deps.models()).includes(model))
|
|
16
|
+
return reply.code(400).send({ code: 'MODEL_UNAVAILABLE' });
|
|
17
|
+
if (session.state !== 'ready')
|
|
18
|
+
return reply.code(409).send({ code: 'SESSION_NOT_READY' });
|
|
19
|
+
const selected = RelaySession.rehydrate(session).selectModel(model, deps.now()).snapshot;
|
|
20
|
+
deps.save(selected);
|
|
21
|
+
return reply.send(selected);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -10,6 +10,7 @@ export function launchCodexAppServer(input) {
|
|
|
10
10
|
const launch = profileAppServerCommand(input.profile, undefined, input.skillsConfig);
|
|
11
11
|
const child = spawn(launch.command, launch.args, {
|
|
12
12
|
cwd: input.cwd,
|
|
13
|
+
env: codexChildEnvironment(input.environment),
|
|
13
14
|
shell: false,
|
|
14
15
|
stdio: 'pipe',
|
|
15
16
|
});
|
|
@@ -25,3 +26,10 @@ export function launchCodexAppServer(input) {
|
|
|
25
26
|
},
|
|
26
27
|
};
|
|
27
28
|
}
|
|
29
|
+
/** Prevents a relay's own ambient status path from reaching discovery-only children. */
|
|
30
|
+
export function codexChildEnvironment(environment) {
|
|
31
|
+
const inherited = { ...process.env };
|
|
32
|
+
delete inherited.GESTALT_MOBILE_ORG_PLAN_STATUS_FILE;
|
|
33
|
+
delete inherited.GESTALT_MOBILE_ORG_PLAN_STATUS_DIRECTORY;
|
|
34
|
+
return { ...inherited, ...environment };
|
|
35
|
+
}
|
|
@@ -11,19 +11,24 @@ export class CodexSessionRuntime {
|
|
|
11
11
|
onServerRequest;
|
|
12
12
|
onProcessExit;
|
|
13
13
|
resolveSkills;
|
|
14
|
-
|
|
14
|
+
planStatusSource;
|
|
15
|
+
onPlanStatus;
|
|
16
|
+
constructor(launch, processes = new Map(), onNotification, onServerRequest, onProcessExit, resolveSkills, planStatusSource, onPlanStatus) {
|
|
15
17
|
this.launch = launch;
|
|
16
18
|
this.processes = processes;
|
|
17
19
|
this.onNotification = onNotification;
|
|
18
20
|
this.onServerRequest = onServerRequest;
|
|
19
21
|
this.onProcessExit = onProcessExit;
|
|
20
22
|
this.resolveSkills = resolveSkills;
|
|
23
|
+
this.planStatusSource = planStatusSource;
|
|
24
|
+
this.onPlanStatus = onPlanStatus;
|
|
21
25
|
}
|
|
22
26
|
pendingRequests = new Map();
|
|
23
27
|
exitUnsubscribers = new Map();
|
|
24
28
|
threadIds = new Map();
|
|
29
|
+
planStatusLeases = new Map();
|
|
25
30
|
async start(session, now, settings = {}) {
|
|
26
|
-
const process =
|
|
31
|
+
const process = await this.launchForSession(session);
|
|
27
32
|
try {
|
|
28
33
|
process.rpc.onNotification((notification) => this.onNotification?.(session.id, notification));
|
|
29
34
|
process.rpc.onServerRequest((request) => this.holdServerRequest(session.id, request));
|
|
@@ -46,6 +51,7 @@ export class CodexSessionRuntime {
|
|
|
46
51
|
}
|
|
47
52
|
catch (error) {
|
|
48
53
|
process.close();
|
|
54
|
+
this.releasePlanStatus(session.id);
|
|
49
55
|
throw error;
|
|
50
56
|
}
|
|
51
57
|
}
|
|
@@ -55,6 +61,7 @@ export class CodexSessionRuntime {
|
|
|
55
61
|
this.processes.get(sessionId)?.close();
|
|
56
62
|
this.processes.delete(sessionId);
|
|
57
63
|
this.threadIds.delete(sessionId);
|
|
64
|
+
this.releasePlanStatus(sessionId);
|
|
58
65
|
}
|
|
59
66
|
async release(sessionId) {
|
|
60
67
|
const process = this.processes.get(sessionId);
|
|
@@ -73,6 +80,8 @@ export class CodexSessionRuntime {
|
|
|
73
80
|
stopAll() {
|
|
74
81
|
for (const sessionId of [...this.processes.keys()])
|
|
75
82
|
this.stop(sessionId);
|
|
83
|
+
for (const sessionId of [...this.planStatusLeases.keys()])
|
|
84
|
+
this.releasePlanStatus(sessionId);
|
|
76
85
|
}
|
|
77
86
|
resolveServerRequest(sessionId, requestId, result) {
|
|
78
87
|
const key = `${sessionId}:${requestId}`;
|
|
@@ -90,6 +99,7 @@ export class CodexSessionRuntime {
|
|
|
90
99
|
const result = (await process.rpc.request('turn/start', {
|
|
91
100
|
threadId: session.threadId,
|
|
92
101
|
input: [{ type: 'text', text, text_elements: [] }],
|
|
102
|
+
...(session.model ? { model: session.model } : {}),
|
|
93
103
|
}));
|
|
94
104
|
if (!result.turn?.id)
|
|
95
105
|
throw new Error('CODEX_TURN_ID_MISSING');
|
|
@@ -123,7 +133,7 @@ export class CodexSessionRuntime {
|
|
|
123
133
|
async restore(session, now) {
|
|
124
134
|
if (!session.threadId)
|
|
125
135
|
throw new Error('CODEX_THREAD_ID_MISSING');
|
|
126
|
-
const process =
|
|
136
|
+
const process = await this.launchForSession(session);
|
|
127
137
|
try {
|
|
128
138
|
process.rpc.onNotification((notification) => this.onNotification?.(session.id, notification));
|
|
129
139
|
process.rpc.onServerRequest((request) => this.holdServerRequest(session.id, request));
|
|
@@ -142,6 +152,7 @@ export class CodexSessionRuntime {
|
|
|
142
152
|
}
|
|
143
153
|
catch (error) {
|
|
144
154
|
process.close();
|
|
155
|
+
this.releasePlanStatus(session.id);
|
|
145
156
|
throw error;
|
|
146
157
|
}
|
|
147
158
|
}
|
|
@@ -156,7 +167,37 @@ export class CodexSessionRuntime {
|
|
|
156
167
|
this.processes.delete(sessionId);
|
|
157
168
|
this.threadIds.delete(sessionId);
|
|
158
169
|
this.exitUnsubscribers.delete(sessionId);
|
|
170
|
+
this.releasePlanStatus(sessionId);
|
|
159
171
|
this.onProcessExit?.(sessionId);
|
|
160
172
|
}) ?? (() => { }));
|
|
161
173
|
}
|
|
174
|
+
async launchForSession(session) {
|
|
175
|
+
const lease = this.planStatusSource
|
|
176
|
+
? await this.planStatusSource.open({ id: session.id, workspacePath: session.workspacePath }, (update) => this.onPlanStatus?.(session.id, update))
|
|
177
|
+
: undefined;
|
|
178
|
+
if (lease)
|
|
179
|
+
this.planStatusLeases.set(session.id, lease);
|
|
180
|
+
try {
|
|
181
|
+
return this.launch({
|
|
182
|
+
profile: session.profile,
|
|
183
|
+
cwd: session.workspacePath,
|
|
184
|
+
skillsConfig: await this.resolveSkills?.(session),
|
|
185
|
+
...(lease
|
|
186
|
+
? {
|
|
187
|
+
environment: {
|
|
188
|
+
GESTALT_MOBILE_ORG_PLAN_STATUS_DIRECTORY: lease.statusDirectory,
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
: {}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
this.releasePlanStatus(session.id);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
releasePlanStatus(sessionId) {
|
|
200
|
+
this.planStatusLeases.get(sessionId)?.close();
|
|
201
|
+
this.planStatusLeases.delete(sessionId);
|
|
202
|
+
}
|
|
162
203
|
}
|