gestalt-mobile 0.12.0 → 0.14.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/README.md +42 -11
- package/dist/client/assets/index-C5qFcFUg.css +1 -0
- package/dist/client/assets/index-tMyOolAm.js +24 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-Bh0R7Dhr.woff +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-C_uLvvQ5.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-ByoDsISC.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/syne-greek-600-normal-BJs4kvyH.woff +0 -0
- package/dist/client/assets/syne-greek-600-normal-BpFII5Ja.woff2 +0 -0
- package/dist/client/assets/syne-latin-600-normal-BiwQbQXw.woff2 +0 -0
- package/dist/client/assets/syne-latin-600-normal-dRu9QuIh.woff +0 -0
- package/dist/client/assets/syne-latin-ext-600-normal-DEzBMW8B.woff +0 -0
- package/dist/client/assets/syne-latin-ext-600-normal-ZnizrDKU.woff2 +0 -0
- package/dist/client/index.html +3 -2
- package/dist/server/server/app.js +22 -227
- package/dist/server/server/cli.js +3 -1
- package/dist/server/server/composition.js +25 -26
- package/dist/server/server/features/auth/application/ceremony-attempts.js +22 -0
- package/dist/server/server/features/auth/devices/revoke/endpoint.js +3 -1
- package/dist/server/server/features/auth/domain/errors.js +7 -0
- package/dist/server/server/features/auth/enrollment-tickets/endpoint.js +7 -2
- package/dist/server/server/features/auth/enrollment-tickets/status/endpoint.js +3 -1
- package/dist/server/server/features/auth/http/cookies.js +20 -0
- package/dist/server/server/features/auth/login/options/endpoint.js +22 -20
- package/dist/server/server/features/auth/login/verify/endpoint.js +6 -14
- package/dist/server/server/features/auth/logout/endpoint.js +2 -6
- package/dist/server/server/features/auth/register/options/endpoint.js +43 -11
- package/dist/server/server/features/auth/register/verify/endpoint.js +74 -18
- package/dist/server/server/features/auth/register-routes.js +41 -0
- package/dist/server/server/features/git/checkout-branch/endpoint.js +1 -1
- package/dist/server/server/features/git/clone-repository/endpoint.js +1 -1
- package/dist/server/server/features/git/register-routes.js +34 -0
- package/dist/server/server/features/plans/application/measurement-snapshot.js +1 -1
- package/dist/server/server/features/plans/get-measurement/endpoint.js +1 -3
- package/dist/server/server/features/plans/register-routes.js +22 -0
- package/dist/server/server/features/sessions/interaction/kind.js +7 -1
- package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +3 -1
- package/dist/server/server/features/sessions/model/relay-session.js +3 -1
- package/dist/server/server/features/sessions/promote-recent-thread/endpoint.js +1 -1
- package/dist/server/server/features/sessions/register-routes.js +138 -0
- package/dist/server/server/features/sessions/respond-interaction/endpoint.js +1 -1
- package/dist/server/server/features/sessions/select-model/endpoint.js +1 -1
- package/dist/server/server/features/sessions/start-session/endpoint.js +5 -2
- package/dist/server/server/features/sessions/start-session/use-case.js +1 -1
- package/dist/server/server/features/sessions/start-turn/endpoint.js +2 -1
- package/dist/server/server/features/skills/delete-profile/endpoint.js +12 -3
- package/dist/server/server/features/skills/list-available/endpoint.js +24 -6
- package/dist/server/server/features/skills/list-profiles/endpoint.js +29 -5
- package/dist/server/server/features/skills/model/skill-profile.js +23 -3
- package/dist/server/server/features/skills/register-routes.js +17 -0
- package/dist/server/server/features/skills/replace-profile/endpoint.js +20 -5
- package/dist/server/server/platform/auth/simple-webauthn-adapter.js +23 -3
- package/dist/server/server/platform/auth/sqlite-authorization-store.js +29 -6
- package/dist/server/server/platform/catalog/profile-command.js +8 -1
- package/dist/server/server/platform/codex/normalizer.js +50 -6
- package/dist/server/server/platform/codex/protocol-compatibility.js +19 -1
- package/dist/server/server/platform/codex/server-request.js +2 -0
- package/dist/server/server/platform/codex/session-runtime.js +235 -126
- package/dist/server/server/platform/codex/thread-plan-name.js +5 -2
- package/dist/server/server/platform/http/problem-handler.js +10 -0
- package/dist/server/server/platform/http/problem-reply.js +13 -0
- package/dist/server/server/platform/plans/filesystem-plan-status-source.js +11 -6
- package/dist/server/server/platform/plans/filesystem-workspace-plan-catalog.js +4 -1
- package/dist/server/server/platform/plans/plan-measurement-command.js +4 -1
- package/dist/server/server/platform/skills/codex-skill-catalog.js +29 -7
- package/dist/server/server/platform/skills/filesystem-skill-profile-store.js +5 -1
- package/dist/server/shared/contracts/quiz.js +30 -9
- package/package.json +4 -1
- package/dist/client/assets/index-B7KwEjTE.js +0 -19
- package/dist/client/assets/index-CMs5ZzxJ.css +0 -1
- package/dist/server/server/features/plans/application/native-plan-alignment.js +0 -39
|
@@ -9,9 +9,53 @@ import { createPlanMeasurementSnapshot, } from '../../features/plans/application
|
|
|
9
9
|
import { canRebindMissingRollout, rebindMissingRollout, } from '../../features/sessions/restore-session/use-case.js';
|
|
10
10
|
import { gestaltQuizDynamicTool } from '../../../shared/contracts/quiz.js';
|
|
11
11
|
import { threadPlanName } from './thread-plan-name.js';
|
|
12
|
+
/** One private owner for every resource acquired for a live Codex child. */
|
|
13
|
+
class SessionResource {
|
|
14
|
+
sessionId;
|
|
15
|
+
process;
|
|
16
|
+
planStatusLease;
|
|
17
|
+
planMeasurementToken;
|
|
18
|
+
unregister;
|
|
19
|
+
onDisposed;
|
|
20
|
+
disposed = false;
|
|
21
|
+
pendingRequests = new Map();
|
|
22
|
+
threadId;
|
|
23
|
+
pendingThreadName;
|
|
24
|
+
writtenThreadName;
|
|
25
|
+
capabilities = new Map();
|
|
26
|
+
constructor(sessionId, process, planStatusLease, planMeasurementToken, unregister, onDisposed) {
|
|
27
|
+
this.sessionId = sessionId;
|
|
28
|
+
this.process = process;
|
|
29
|
+
this.planStatusLease = planStatusLease;
|
|
30
|
+
this.planMeasurementToken = planMeasurementToken;
|
|
31
|
+
this.unregister = unregister;
|
|
32
|
+
this.onDisposed = onDisposed;
|
|
33
|
+
}
|
|
34
|
+
dispose() {
|
|
35
|
+
if (this.disposed)
|
|
36
|
+
return false;
|
|
37
|
+
this.disposed = true;
|
|
38
|
+
for (const pending of this.pendingRequests.values()) {
|
|
39
|
+
clearTimeout(pending.timer);
|
|
40
|
+
pending.reject(new Error('CODEX_SERVER_REQUEST_CANCELLED'));
|
|
41
|
+
}
|
|
42
|
+
this.pendingRequests.clear();
|
|
43
|
+
for (const unsubscribe of this.unregister)
|
|
44
|
+
unsubscribe();
|
|
45
|
+
this.process.close();
|
|
46
|
+
this.planStatusLease?.close();
|
|
47
|
+
this.onDisposed();
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
get active() {
|
|
51
|
+
return !this.disposed;
|
|
52
|
+
}
|
|
53
|
+
attach(unregister) {
|
|
54
|
+
this.unregister = unregister;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
12
57
|
export class CodexSessionRuntime {
|
|
13
58
|
launch;
|
|
14
|
-
processes;
|
|
15
59
|
onNotification;
|
|
16
60
|
onServerRequest;
|
|
17
61
|
onProcessExit;
|
|
@@ -19,9 +63,13 @@ export class CodexSessionRuntime {
|
|
|
19
63
|
planStatusSource;
|
|
20
64
|
onPlanStatus;
|
|
21
65
|
planMeasurementBaseUrl;
|
|
22
|
-
|
|
66
|
+
requestTimeoutMs;
|
|
67
|
+
maxPendingRequests;
|
|
68
|
+
constructor(launch,
|
|
69
|
+
// Kept as an ignored compatibility slot while callers migrate from the old
|
|
70
|
+
// correlated-map constructor shape. Live ownership is `sessions` only.
|
|
71
|
+
_legacyProcesses = undefined, onNotification, onServerRequest, onProcessExit, resolveSkills, planStatusSource, onPlanStatus, planMeasurementBaseUrl, requestTimeoutMs = 30_000, maxPendingRequests = 64) {
|
|
23
72
|
this.launch = launch;
|
|
24
|
-
this.processes = processes;
|
|
25
73
|
this.onNotification = onNotification;
|
|
26
74
|
this.onServerRequest = onServerRequest;
|
|
27
75
|
this.onProcessExit = onProcessExit;
|
|
@@ -29,53 +77,37 @@ export class CodexSessionRuntime {
|
|
|
29
77
|
this.planStatusSource = planStatusSource;
|
|
30
78
|
this.onPlanStatus = onPlanStatus;
|
|
31
79
|
this.planMeasurementBaseUrl = planMeasurementBaseUrl;
|
|
80
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
81
|
+
this.maxPendingRequests = maxPendingRequests;
|
|
82
|
+
void _legacyProcesses;
|
|
32
83
|
}
|
|
33
|
-
|
|
34
|
-
exitUnsubscribers = new Map();
|
|
35
|
-
threadIds = new Map();
|
|
36
|
-
planStatusLeases = new Map();
|
|
37
|
-
planMeasurementTokens = new Map();
|
|
38
|
-
pendingThreadNames = new Map();
|
|
39
|
-
writtenThreadNames = new Map();
|
|
84
|
+
sessions = new Map();
|
|
40
85
|
async start(session, now, settings = {}) {
|
|
41
|
-
const
|
|
86
|
+
const resource = await this.createResource(session);
|
|
42
87
|
try {
|
|
43
|
-
process.rpc.
|
|
44
|
-
process.rpc.onServerRequest((request) => this.holdServerRequest(session.id, request));
|
|
45
|
-
this.attachExitHandler(session.id, process);
|
|
46
|
-
await process.rpc.request('initialize', {
|
|
88
|
+
await resource.process.rpc.request('initialize', {
|
|
47
89
|
clientInfo: { name: 'gestalt-mobile', version: '0.1.0' },
|
|
48
90
|
capabilities: { experimentalApi: true },
|
|
49
91
|
});
|
|
50
|
-
const startedThreadId = await this.startThread(process, session, settings);
|
|
51
|
-
|
|
52
|
-
this.
|
|
92
|
+
const startedThreadId = await this.startThread(resource.process, session, settings);
|
|
93
|
+
resource.threadId = startedThreadId;
|
|
94
|
+
this.sessions.set(session.id, resource);
|
|
53
95
|
await this.writePendingThreadName(session.id);
|
|
54
96
|
return RelaySession.rehydrate(session).bindThread(startedThreadId, now).snapshot;
|
|
55
97
|
}
|
|
56
98
|
catch (error) {
|
|
57
|
-
|
|
99
|
+
resource.dispose();
|
|
58
100
|
throw error;
|
|
59
101
|
}
|
|
60
102
|
}
|
|
61
103
|
stop(sessionId) {
|
|
62
|
-
this.
|
|
63
|
-
this.exitUnsubscribers.delete(sessionId);
|
|
64
|
-
this.processes.get(sessionId)?.close();
|
|
65
|
-
this.processes.delete(sessionId);
|
|
66
|
-
this.threadIds.delete(sessionId);
|
|
67
|
-
this.pendingThreadNames.delete(sessionId);
|
|
68
|
-
this.writtenThreadNames.delete(sessionId);
|
|
69
|
-
this.clearPendingRequests(sessionId);
|
|
70
|
-
this.planMeasurementTokens.delete(sessionId);
|
|
71
|
-
this.releasePlanStatus(sessionId);
|
|
104
|
+
this.sessions.get(sessionId)?.dispose();
|
|
72
105
|
}
|
|
73
106
|
async release(sessionId) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
if (process && threadId) {
|
|
107
|
+
const resource = this.sessions.get(sessionId);
|
|
108
|
+
if (resource?.threadId) {
|
|
77
109
|
try {
|
|
78
|
-
await process.rpc.request('thread/unsubscribe', { threadId });
|
|
110
|
+
await resource.process.rpc.request('thread/unsubscribe', { threadId: resource.threadId });
|
|
79
111
|
}
|
|
80
112
|
catch {
|
|
81
113
|
// Closing the child still releases relay ownership if Codex has already exited.
|
|
@@ -85,66 +117,66 @@ export class CodexSessionRuntime {
|
|
|
85
117
|
}
|
|
86
118
|
/** Releases all relay-owned app-server children during graceful shutdown. */
|
|
87
119
|
stopAll() {
|
|
88
|
-
for (const
|
|
89
|
-
|
|
90
|
-
for (const sessionId of [...this.planStatusLeases.keys()])
|
|
91
|
-
this.releasePlanStatus(sessionId);
|
|
120
|
+
for (const resource of [...this.sessions.values()])
|
|
121
|
+
resource.dispose();
|
|
92
122
|
}
|
|
93
123
|
resolveServerRequest(sessionId, requestId, result) {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
if (!
|
|
124
|
+
const resource = this.sessions.get(sessionId);
|
|
125
|
+
const pending = resource?.pendingRequests.get(requestId);
|
|
126
|
+
if (!resource || !pending)
|
|
97
127
|
return false;
|
|
98
|
-
|
|
99
|
-
|
|
128
|
+
resource.pendingRequests.delete(requestId);
|
|
129
|
+
clearTimeout(pending.timer);
|
|
130
|
+
pending.resolve(result);
|
|
100
131
|
return true;
|
|
101
132
|
}
|
|
102
133
|
async startTurn(session, text, now) {
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
134
|
+
const resource = this.sessions.get(session.id);
|
|
135
|
+
if (!resource || !session.threadId)
|
|
105
136
|
throw new Error('CODEX_SESSION_NOT_RUNNING');
|
|
106
|
-
const result = (await process.rpc.request('turn/start', {
|
|
137
|
+
const result = decodeTurnStart(await resource.process.rpc.request('turn/start', {
|
|
107
138
|
threadId: session.threadId,
|
|
108
139
|
input: [{ type: 'text', text, text_elements: [] }],
|
|
109
140
|
...(session.model ? { model: session.model } : {}),
|
|
110
141
|
}));
|
|
111
|
-
|
|
112
|
-
throw new Error('CODEX_TURN_ID_MISSING');
|
|
113
|
-
return RelaySession.rehydrate(session).startTurn(result.turn.id, now).snapshot;
|
|
142
|
+
return RelaySession.rehydrate(session).startTurn(result, now).snapshot;
|
|
114
143
|
}
|
|
115
144
|
async interruptTurn(session, turnId) {
|
|
116
|
-
const
|
|
117
|
-
if (!
|
|
145
|
+
const resource = this.sessions.get(session.id);
|
|
146
|
+
if (!resource || !session.threadId)
|
|
118
147
|
throw new Error('CODEX_SESSION_NOT_RUNNING');
|
|
119
|
-
await process.rpc.request('turn/interrupt', { threadId: session.threadId, turnId });
|
|
148
|
+
await resource.process.rpc.request('turn/interrupt', { threadId: session.threadId, turnId });
|
|
120
149
|
}
|
|
121
150
|
async readPlanMeasurement(session) {
|
|
122
|
-
const
|
|
123
|
-
if (!
|
|
151
|
+
const resource = this.sessions.get(session.id);
|
|
152
|
+
if (!resource || !session.threadId)
|
|
124
153
|
throw new Error('CODEX_SESSION_NOT_RUNNING');
|
|
125
154
|
const [rateLimits, thread] = await Promise.all([
|
|
126
|
-
process.rpc.request('account/rateLimits/read', {}),
|
|
127
|
-
process.rpc.request('thread/read', {
|
|
155
|
+
resource.process.rpc.request('account/rateLimits/read', {}),
|
|
156
|
+
resource.process.rpc.request('thread/read', {
|
|
157
|
+
threadId: session.threadId,
|
|
158
|
+
includeTurns: true,
|
|
159
|
+
}),
|
|
128
160
|
]);
|
|
129
161
|
return createPlanMeasurementSnapshot({
|
|
130
162
|
capturedAt: new Date().toISOString(),
|
|
131
|
-
rateLimits: rateLimitWindows(rateLimits),
|
|
163
|
+
rateLimits: rateLimitWindows(decodeRateLimits(rateLimits)),
|
|
132
164
|
tokenUsage: threadTokenUsage(thread),
|
|
133
165
|
});
|
|
134
166
|
}
|
|
135
167
|
authorizePlanMeasurement(sessionId, authorization) {
|
|
136
|
-
const token = this.
|
|
168
|
+
const token = this.sessions.get(sessionId)?.planMeasurementToken;
|
|
137
169
|
return Boolean(token && authorization === `Bearer ${token}`);
|
|
138
170
|
}
|
|
139
171
|
async readHistory(session) {
|
|
140
|
-
const
|
|
141
|
-
if (!
|
|
172
|
+
const resource = this.sessions.get(session.id);
|
|
173
|
+
if (!resource || !session.threadId)
|
|
142
174
|
throw new Error('CODEX_SESSION_NOT_RUNNING');
|
|
143
|
-
const result = (await process.rpc.request('thread/read', {
|
|
175
|
+
const result = decodeThreadRead(await resource.process.rpc.request('thread/read', {
|
|
144
176
|
threadId: session.threadId,
|
|
145
177
|
includeTurns: true,
|
|
146
178
|
}));
|
|
147
|
-
const rawTurns = result
|
|
179
|
+
const rawTurns = result;
|
|
148
180
|
const activeTurn = rawTurns.find((turn) => turn.status === 'inProgress' && typeof turn.id === 'string');
|
|
149
181
|
return {
|
|
150
182
|
turns: rawTurns.map((turn) => ({
|
|
@@ -161,18 +193,15 @@ export class CodexSessionRuntime {
|
|
|
161
193
|
async restoreWithOutcome(session, now) {
|
|
162
194
|
if (!session.threadId)
|
|
163
195
|
throw new Error('CODEX_THREAD_ID_MISSING');
|
|
164
|
-
const
|
|
196
|
+
const resource = await this.createResource(session);
|
|
165
197
|
try {
|
|
166
|
-
process.rpc.
|
|
167
|
-
process.rpc.onServerRequest((request) => this.holdServerRequest(session.id, request));
|
|
168
|
-
this.attachExitHandler(session.id, process);
|
|
169
|
-
await process.rpc.request('initialize', {
|
|
198
|
+
await resource.process.rpc.request('initialize', {
|
|
170
199
|
clientInfo: { name: 'gestalt-mobile', version: '0.1.0' },
|
|
171
200
|
capabilities: { experimentalApi: true },
|
|
172
201
|
});
|
|
173
202
|
let result;
|
|
174
203
|
try {
|
|
175
|
-
await process.rpc.request('thread/resume', {
|
|
204
|
+
await resource.process.rpc.request('thread/resume', {
|
|
176
205
|
threadId: session.threadId,
|
|
177
206
|
cwd: session.workspacePath,
|
|
178
207
|
dynamicTools: [gestaltQuizDynamicTool],
|
|
@@ -186,63 +215,70 @@ export class CodexSessionRuntime {
|
|
|
186
215
|
catch (error) {
|
|
187
216
|
if (!canRebindMissingRollout(session, error))
|
|
188
217
|
throw error;
|
|
189
|
-
const replacementThreadId = await this.startThread(process, session, {
|
|
218
|
+
const replacementThreadId = await this.startThread(resource.process, session, {
|
|
219
|
+
model: session.model,
|
|
220
|
+
});
|
|
190
221
|
result = rebindMissingRollout(session, error, replacementThreadId, now);
|
|
191
222
|
}
|
|
192
|
-
|
|
193
|
-
this.
|
|
223
|
+
resource.threadId = result.session.threadId;
|
|
224
|
+
this.sessions.get(session.id)?.dispose();
|
|
225
|
+
this.sessions.set(session.id, resource);
|
|
194
226
|
await this.writePendingThreadName(session.id);
|
|
195
227
|
return result;
|
|
196
228
|
}
|
|
197
229
|
catch (error) {
|
|
198
|
-
|
|
230
|
+
resource.dispose();
|
|
199
231
|
throw error;
|
|
200
232
|
}
|
|
201
233
|
}
|
|
202
234
|
/** Best-effort metadata only: failures never affect plan or session execution. */
|
|
203
235
|
async syncThreadPlanName(sessionId, plan) {
|
|
204
|
-
this.
|
|
236
|
+
const resource = this.sessions.get(sessionId);
|
|
237
|
+
if (!resource)
|
|
238
|
+
return;
|
|
239
|
+
resource.pendingThreadName = threadPlanName(plan);
|
|
205
240
|
await this.writePendingThreadName(sessionId);
|
|
206
241
|
}
|
|
207
242
|
async writePendingThreadName(sessionId) {
|
|
208
|
-
const
|
|
209
|
-
const threadId =
|
|
210
|
-
const name =
|
|
211
|
-
if (!
|
|
243
|
+
const resource = this.sessions.get(sessionId);
|
|
244
|
+
const threadId = resource?.threadId;
|
|
245
|
+
const name = resource?.pendingThreadName;
|
|
246
|
+
if (!resource ||
|
|
247
|
+
!threadId ||
|
|
248
|
+
!name ||
|
|
249
|
+
resource.writtenThreadName === name ||
|
|
250
|
+
resource.capabilities.get('thread/name/set') === false)
|
|
212
251
|
return;
|
|
213
252
|
try {
|
|
214
|
-
await process.rpc.request('thread/name/set', { threadId, name });
|
|
215
|
-
|
|
253
|
+
await resource.process.rpc.request('thread/name/set', { threadId, name });
|
|
254
|
+
if (resource.active) {
|
|
255
|
+
resource.capabilities.set('thread/name/set', true);
|
|
256
|
+
resource.writtenThreadName = name;
|
|
257
|
+
}
|
|
216
258
|
}
|
|
217
|
-
catch {
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (isMethodNotFound(error))
|
|
261
|
+
resource.capabilities.set('thread/name/set', false);
|
|
218
262
|
// Unsupported servers and transient failures are bounded metadata failures.
|
|
219
263
|
}
|
|
220
264
|
}
|
|
221
|
-
holdServerRequest(
|
|
222
|
-
if (!this.onServerRequest?.(sessionId, request)) {
|
|
265
|
+
holdServerRequest(resource, request) {
|
|
266
|
+
if (!resource.active || !this.onServerRequest?.(resource.sessionId, request)) {
|
|
223
267
|
return Promise.reject(new Error('CODEX_SERVER_REQUEST_UNSUPPORTED'));
|
|
224
268
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
this.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
this.exitUnsubscribers.delete(sessionId);
|
|
236
|
-
this.releasePlanStatus(sessionId);
|
|
237
|
-
this.onProcessExit?.(sessionId);
|
|
238
|
-
}) ?? (() => { }));
|
|
269
|
+
if (resource.pendingRequests.size >= this.maxPendingRequests)
|
|
270
|
+
return Promise.reject(new Error('CODEX_SERVER_REQUEST_LIMIT'));
|
|
271
|
+
const requestId = String(request.id);
|
|
272
|
+
return new Promise((resolve, reject) => {
|
|
273
|
+
const timer = setTimeout(() => {
|
|
274
|
+
if (resource.pendingRequests.delete(requestId))
|
|
275
|
+
reject(new Error('CODEX_SERVER_REQUEST_TIMEOUT'));
|
|
276
|
+
}, this.requestTimeoutMs);
|
|
277
|
+
resource.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
278
|
+
});
|
|
239
279
|
}
|
|
240
280
|
async startThread(process, session, settings = {}) {
|
|
241
|
-
|
|
242
|
-
if (typeof result.thread?.id !== 'string' || !result.thread.id) {
|
|
243
|
-
throw new Error('CODEX_THREAD_ID_MISSING');
|
|
244
|
-
}
|
|
245
|
-
return result.thread.id;
|
|
281
|
+
return decodeThreadStart(await process.rpc.request('thread/start', this.threadStartParams(session, settings)));
|
|
246
282
|
}
|
|
247
283
|
threadStartParams(session, settings) {
|
|
248
284
|
return {
|
|
@@ -253,35 +289,17 @@ export class CodexSessionRuntime {
|
|
|
253
289
|
...(settings.sandbox ? { sandbox: settings.sandbox } : {}),
|
|
254
290
|
};
|
|
255
291
|
}
|
|
256
|
-
|
|
257
|
-
this.exitUnsubscribers.get(sessionId)?.();
|
|
258
|
-
this.exitUnsubscribers.delete(sessionId);
|
|
259
|
-
process.close();
|
|
260
|
-
this.clearPendingRequests(sessionId);
|
|
261
|
-
this.planMeasurementTokens.delete(sessionId);
|
|
262
|
-
this.releasePlanStatus(sessionId);
|
|
263
|
-
}
|
|
264
|
-
clearPendingRequests(sessionId) {
|
|
265
|
-
const prefix = `${sessionId}:`;
|
|
266
|
-
for (const key of this.pendingRequests.keys()) {
|
|
267
|
-
if (key.startsWith(prefix))
|
|
268
|
-
this.pendingRequests.delete(key);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
async launchForSession(session) {
|
|
292
|
+
async createResource(session) {
|
|
272
293
|
const lease = this.planStatusSource
|
|
273
294
|
? await this.planStatusSource.open({ id: session.id, workspacePath: session.workspacePath }, (update) => this.onPlanStatus?.(session.id, update))
|
|
274
295
|
: undefined;
|
|
275
|
-
if (lease)
|
|
276
|
-
this.planStatusLeases.set(session.id, lease);
|
|
277
296
|
try {
|
|
278
297
|
const token = randomUUID();
|
|
279
|
-
this.
|
|
280
|
-
return this.launch({
|
|
298
|
+
const process = this.launch({
|
|
281
299
|
profile: session.profile,
|
|
282
300
|
cwd: session.workspacePath,
|
|
283
301
|
skillsConfig: await this.resolveSkills?.(session),
|
|
284
|
-
...(
|
|
302
|
+
...(lease || this.planMeasurementBaseUrl
|
|
285
303
|
? {
|
|
286
304
|
environment: {
|
|
287
305
|
...(lease
|
|
@@ -297,16 +315,31 @@ export class CodexSessionRuntime {
|
|
|
297
315
|
}
|
|
298
316
|
: {}),
|
|
299
317
|
});
|
|
318
|
+
const resource = new SessionResource(session.id, process, lease, token, [], () => {
|
|
319
|
+
if (this.sessions.get(session.id) === resource)
|
|
320
|
+
this.sessions.delete(session.id);
|
|
321
|
+
});
|
|
322
|
+
const notificationUnsubscribe = process.rpc.onNotification((notification) => {
|
|
323
|
+
if (resource.active)
|
|
324
|
+
this.onNotification?.(session.id, notification);
|
|
325
|
+
});
|
|
326
|
+
const requestUnsubscribe = process.rpc.onServerRequest((request) => this.holdServerRequest(resource, request));
|
|
327
|
+
const exitUnsubscribe = process.onExit?.(() => {
|
|
328
|
+
if (resource.dispose())
|
|
329
|
+
this.onProcessExit?.(session.id);
|
|
330
|
+
}) ?? (() => { });
|
|
331
|
+
// The resource's private unsubscribe list is populated before it is published.
|
|
332
|
+
resource.attach([notificationUnsubscribe, requestUnsubscribe, exitUnsubscribe]);
|
|
333
|
+
return resource;
|
|
300
334
|
}
|
|
301
335
|
catch (error) {
|
|
302
|
-
|
|
336
|
+
lease?.close();
|
|
303
337
|
throw error;
|
|
304
338
|
}
|
|
305
339
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
340
|
+
}
|
|
341
|
+
function isMethodNotFound(error) {
|
|
342
|
+
return Boolean(error && typeof error === 'object' && error.code === -32601);
|
|
310
343
|
}
|
|
311
344
|
function rateLimitWindows(value) {
|
|
312
345
|
const limits = asRecord(value)?.rateLimits;
|
|
@@ -321,6 +354,21 @@ function rateLimitWindows(value) {
|
|
|
321
354
|
: [];
|
|
322
355
|
});
|
|
323
356
|
}
|
|
357
|
+
function decodeRateLimits(value) {
|
|
358
|
+
const limits = asRecord(value)?.rateLimits;
|
|
359
|
+
if (!Array.isArray(limits) || limits.length > 32)
|
|
360
|
+
return undefined;
|
|
361
|
+
const decoded = limits.flatMap((limit) => {
|
|
362
|
+
const record = asRecord(limit);
|
|
363
|
+
return typeof record?.windowDurationMins === 'number' &&
|
|
364
|
+
Number.isFinite(record.windowDurationMins) &&
|
|
365
|
+
typeof record.usedPercent === 'number' &&
|
|
366
|
+
Number.isFinite(record.usedPercent)
|
|
367
|
+
? [{ windowDurationMins: record.windowDurationMins, usedPercent: record.usedPercent }]
|
|
368
|
+
: [];
|
|
369
|
+
});
|
|
370
|
+
return decoded.length === limits.length ? { rateLimits: decoded } : undefined;
|
|
371
|
+
}
|
|
324
372
|
function threadTokenUsage(value) {
|
|
325
373
|
const turns = asRecord(asRecord(value)?.thread)?.turns;
|
|
326
374
|
if (!Array.isArray(turns))
|
|
@@ -346,3 +394,64 @@ function threadTokenUsage(value) {
|
|
|
346
394
|
function asRecord(value) {
|
|
347
395
|
return value && typeof value === 'object' ? value : undefined;
|
|
348
396
|
}
|
|
397
|
+
function decodeThreadStart(value) {
|
|
398
|
+
const id = asRecord(asRecord(value)?.thread)?.id;
|
|
399
|
+
if (typeof id !== 'string' || !id || id.length > 256)
|
|
400
|
+
throw new Error('CODEX_THREAD_ID_MISSING');
|
|
401
|
+
return id;
|
|
402
|
+
}
|
|
403
|
+
function decodeTurnStart(value) {
|
|
404
|
+
const id = asRecord(asRecord(value)?.turn)?.id;
|
|
405
|
+
if (typeof id !== 'string' || !id || id.length > 256)
|
|
406
|
+
throw new Error('CODEX_TURN_ID_MISSING');
|
|
407
|
+
return id;
|
|
408
|
+
}
|
|
409
|
+
function decodeThreadRead(value) {
|
|
410
|
+
const turns = asRecord(asRecord(value)?.thread)?.turns;
|
|
411
|
+
if (!Array.isArray(turns) || turns.length > 10_000)
|
|
412
|
+
throw new Error('CODEX_THREAD_READ_MALFORMED');
|
|
413
|
+
return turns.map((turn) => {
|
|
414
|
+
const record = asRecord(turn);
|
|
415
|
+
if (!record)
|
|
416
|
+
throw new Error('CODEX_THREAD_READ_MALFORMED');
|
|
417
|
+
return {
|
|
418
|
+
id: boundedString(record.id),
|
|
419
|
+
status: boundedString(record.status),
|
|
420
|
+
startedAt: typeof record.startedAt === 'number' ? record.startedAt : undefined,
|
|
421
|
+
completedAt: typeof record.completedAt === 'number' ? record.completedAt : undefined,
|
|
422
|
+
items: Array.isArray(record.items) ? record.items.flatMap(decodeHistoryItem) : undefined,
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function boundedString(value, max = 64_000) {
|
|
427
|
+
return typeof value === 'string' && value.length <= max ? value : undefined;
|
|
428
|
+
}
|
|
429
|
+
function decodeHistoryItem(value) {
|
|
430
|
+
const item = asRecord(value);
|
|
431
|
+
const id = boundedString(item?.id, 256);
|
|
432
|
+
const type = boundedString(item?.type, 64);
|
|
433
|
+
if (!item || !id || !type)
|
|
434
|
+
return [];
|
|
435
|
+
if (type === 'agentMessage')
|
|
436
|
+
return [
|
|
437
|
+
{
|
|
438
|
+
id,
|
|
439
|
+
type,
|
|
440
|
+
...(boundedString(item.text) ? { text: boundedString(item.text) } : {}),
|
|
441
|
+
...(boundedString(item.phase, 64) ? { phase: boundedString(item.phase, 64) } : {}),
|
|
442
|
+
},
|
|
443
|
+
];
|
|
444
|
+
if (type === 'plan' && boundedString(item.text))
|
|
445
|
+
return [{ id, type, text: boundedString(item.text) }];
|
|
446
|
+
if (type === 'commandExecution' && boundedString(item.command) && boundedString(item.status, 64))
|
|
447
|
+
return [
|
|
448
|
+
{
|
|
449
|
+
id,
|
|
450
|
+
type,
|
|
451
|
+
command: boundedString(item.command),
|
|
452
|
+
status: boundedString(item.status, 64),
|
|
453
|
+
...(typeof item.exitCode === 'number' ? { exitCode: item.exitCode } : {}),
|
|
454
|
+
},
|
|
455
|
+
];
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
@@ -12,12 +12,15 @@ export function threadPlanName(plan, limit = 120) {
|
|
|
12
12
|
? `L1 ${l1.indexOf(active) + 1}/${l1.length}`
|
|
13
13
|
: awaiting
|
|
14
14
|
? `Review ${l1.indexOf(awaiting) + 1}/${l1.length}`
|
|
15
|
-
: l1.length > 0 &&
|
|
15
|
+
: l1.length > 0 &&
|
|
16
|
+
l1.every((step) => step.state === 'DONE' && step.reviewStatus === 'REVIEWED')
|
|
16
17
|
? 'Complete'
|
|
17
18
|
: 'Authoring';
|
|
18
19
|
return `${truncateTitle(plan.title, Math.max(1, limit - suffix.length - 3))} — ${suffix}`;
|
|
19
20
|
}
|
|
20
21
|
function truncateTitle(value, limit) {
|
|
21
22
|
const characters = Array.from(value);
|
|
22
|
-
return characters.length <= limit
|
|
23
|
+
return characters.length <= limit
|
|
24
|
+
? value
|
|
25
|
+
: `${characters.slice(0, Math.max(0, limit - 1)).join('')}…`;
|
|
23
26
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
4
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
*/
|
|
6
|
+
import { problem as createProblem } from './problem.js';
|
|
6
7
|
export function registerProblemHandler(app, serveSpa = false) {
|
|
7
8
|
app.setNotFoundHandler((request, reply) => {
|
|
8
9
|
if (serveSpa && request.method === 'GET' && !request.url.startsWith('/api/')) {
|
|
@@ -19,6 +20,15 @@ export function registerProblemHandler(app, serveSpa = false) {
|
|
|
19
20
|
return reply.code(404).type('application/problem+json').send(problem);
|
|
20
21
|
});
|
|
21
22
|
app.setErrorHandler((error, _request, reply) => {
|
|
23
|
+
if (typeof error === 'object' &&
|
|
24
|
+
error !== null &&
|
|
25
|
+
'code' in error &&
|
|
26
|
+
error.code === 'FST_ERR_CTP_BODY_TOO_LARGE') {
|
|
27
|
+
return reply
|
|
28
|
+
.code(413)
|
|
29
|
+
.type('application/problem+json')
|
|
30
|
+
.send(createProblem('PAYLOAD_TOO_LARGE', 413, 'The request body exceeds this endpoint limit.'));
|
|
31
|
+
}
|
|
22
32
|
const problem = {
|
|
23
33
|
type: 'urn:gestalt-mobile:error:internal',
|
|
24
34
|
title: 'Internal server error',
|
|
@@ -0,0 +1,13 @@
|
|
|
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 './problem.js';
|
|
7
|
+
/** Send the established RFC 7807-compatible response without deciding its semantics. */
|
|
8
|
+
export function sendProblem(reply, code, status, detail) {
|
|
9
|
+
return reply
|
|
10
|
+
.code(status)
|
|
11
|
+
.type('application/problem+json')
|
|
12
|
+
.send(problem(code, status, detail));
|
|
13
|
+
}
|
|
@@ -29,10 +29,7 @@ export class FilesystemPlanStatusSource {
|
|
|
29
29
|
await mkdir(this.dismissalDirectory, { recursive: true, mode: 0o700 });
|
|
30
30
|
await mkdir(statusDirectory, { recursive: true, mode: 0o700 });
|
|
31
31
|
if (process.platform !== 'win32') {
|
|
32
|
-
await Promise.all([
|
|
33
|
-
chmod(this.dismissalDirectory, 0o700),
|
|
34
|
-
chmod(statusDirectory, 0o700),
|
|
35
|
-
]);
|
|
32
|
+
await Promise.all([chmod(this.dismissalDirectory, 0o700), chmod(statusDirectory, 0o700)]);
|
|
36
33
|
}
|
|
37
34
|
const lease = new ActiveLease(statusDirectory, session.id, session.workspacePath, listener, this.planReadFilesystem, (identity) => this.isDismissed(session.id, identity), (statusPath) => this.activeStatusPaths.set(session.id, statusPath), () => {
|
|
38
35
|
this.leases.delete(session.id);
|
|
@@ -97,7 +94,9 @@ export class FilesystemPlanStatusSource {
|
|
|
97
94
|
const next = new Set(dismissed).add(identity);
|
|
98
95
|
const candidate = join(this.dismissalDirectory, `.${randomUUID()}.dismissals.tmp`);
|
|
99
96
|
try {
|
|
100
|
-
await this.dismissalFilesystem.writeFile(candidate, JSON.stringify([...next]), {
|
|
97
|
+
await this.dismissalFilesystem.writeFile(candidate, JSON.stringify([...next]), {
|
|
98
|
+
mode: 0o600,
|
|
99
|
+
});
|
|
101
100
|
await this.dismissalFilesystem.rename(candidate, path);
|
|
102
101
|
}
|
|
103
102
|
catch (error) {
|
|
@@ -272,7 +271,13 @@ class ActiveLease {
|
|
|
272
271
|
const previousStatusPath = this.activeStatusPath;
|
|
273
272
|
this.activeStatusPath = statusPath;
|
|
274
273
|
this.onActiveStatusPath(statusPath);
|
|
275
|
-
const update = {
|
|
274
|
+
const update = {
|
|
275
|
+
kind: 'updated',
|
|
276
|
+
plan: result.plan,
|
|
277
|
+
identity,
|
|
278
|
+
planPath,
|
|
279
|
+
reason: signal.reason,
|
|
280
|
+
};
|
|
276
281
|
const serializedUpdate = JSON.stringify(update);
|
|
277
282
|
if (serializedUpdate !== this.lastEmittedUpdate) {
|
|
278
283
|
this.lastEmittedUpdate = serializedUpdate;
|
|
@@ -78,7 +78,10 @@ export class FilesystemWorkspacePlanCatalog {
|
|
|
78
78
|
return { kind: 'unavailable' };
|
|
79
79
|
const source = await this.filesystem.readFile(canonical, 'utf8');
|
|
80
80
|
const after = await this.filesystem.stat(canonical);
|
|
81
|
-
if (!after.isFile() ||
|
|
81
|
+
if (!after.isFile() ||
|
|
82
|
+
after.size !== before.size ||
|
|
83
|
+
after.ino !== before.ino ||
|
|
84
|
+
after.dev !== before.dev)
|
|
82
85
|
return { kind: 'unavailable' };
|
|
83
86
|
const parsed = parseSupervisedPlan({ source, planPath: canonical, workspacePath });
|
|
84
87
|
return parsed.kind === 'available' ? parsed : { kind: 'unavailable' };
|
|
@@ -9,5 +9,8 @@ const execute = promisify(execFile);
|
|
|
9
9
|
export const PLAN_MEASUREMENT_COMMAND_TIMEOUT_MS = 15_000;
|
|
10
10
|
/** Invokes the explicitly configured Org Plan helper without a shell. */
|
|
11
11
|
export async function checkpointPlanMeasurement(helperPath, planPath, stepId, snapshot) {
|
|
12
|
-
await execute(helperPath, ['measure', 'checkpoint', planPath, stepId, JSON.stringify(snapshot)], {
|
|
12
|
+
await execute(helperPath, ['measure', 'checkpoint', planPath, stepId, JSON.stringify(snapshot)], {
|
|
13
|
+
shell: false,
|
|
14
|
+
timeout: PLAN_MEASUREMENT_COMMAND_TIMEOUT_MS,
|
|
15
|
+
});
|
|
13
16
|
}
|