gestalt-mobile 0.28.0 → 0.29.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-BtlbAtm9.css +1 -0
- package/dist/client/assets/{index-Bz23puhw.js → index-CRULnRRB.js} +20 -20
- package/dist/client/index.html +2 -2
- package/dist/server/server/composition.js +44 -3
- package/dist/server/server/features/autopilot/application/policy.js +10 -1
- package/dist/server/server/features/autopilot/application/service.js +62 -1
- package/dist/server/server/features/autopilot/domain/supervised-lifecycle.js +40 -1
- package/dist/server/server/features/org-plan-checkpoint/application/validate.js +20 -0
- package/dist/server/server/features/sessions/get-history/endpoint.js +1 -0
- package/dist/server/server/features/sessions/interaction/kind.js +1 -0
- package/dist/server/server/features/sessions/interaction/response-validator.js +1 -1
- package/dist/server/server/platform/codex/normalizer.js +22 -17
- package/dist/server/server/platform/codex/server-request.js +7 -0
- package/dist/server/server/platform/codex/session-runtime.js +58 -4
- package/dist/server/server/platform/persistence/sqlite-autopilot-store.js +6 -2
- package/dist/server/shared/contracts/org-plan-checkpoint.js +131 -0
- package/package.json +1 -1
- package/dist/client/assets/index-B7x1bL59.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -16,8 +16,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
16
16
|
<link rel="icon" href="/icons/gestalt-mobile-192.png" />
|
|
17
17
|
<link rel="apple-touch-icon" href="/icons/gestalt-mobile-180.png" />
|
|
18
18
|
<title>Gestalt Mobile</title>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-CRULnRRB.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BtlbAtm9.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="app"></div>
|
|
@@ -36,6 +36,8 @@ import { RelaySession, } from './features/sessions/model/relay-session.js';
|
|
|
36
36
|
import { AgentActivityRegistry } from './features/agent-activity/registry.js';
|
|
37
37
|
import { decodeAgentActivityFacts } from './platform/codex/activity-facts.js';
|
|
38
38
|
import { resolvedServerRequestId, toPendingInteraction } from './platform/codex/server-request.js';
|
|
39
|
+
import { parseOrgPlanCheckpoint, toOrgPlanCheckpointToolResponse, } from '../shared/contracts/org-plan-checkpoint.js';
|
|
40
|
+
import { validOrgPlanCheckpoint } from './features/org-plan-checkpoint/application/validate.js';
|
|
39
41
|
import { isValidInteractionResponse, isValidQuizInteractionResponse, } from './features/sessions/interaction/response-validator.js';
|
|
40
42
|
import { promoteRecentThread } from './features/sessions/promote-recent-thread/use-case.js';
|
|
41
43
|
import { FilesystemSkillProfileStore } from './platform/skills/filesystem-skill-profile-store.js';
|
|
@@ -379,7 +381,7 @@ export async function composeRelayApp(options) {
|
|
|
379
381
|
autopilot.planStatusChanged(sessionId);
|
|
380
382
|
};
|
|
381
383
|
runtime = options.startAppServers
|
|
382
|
-
? new CodexSessionRuntime(options.launchAppServer ?? launchCodexAppServer, undefined, (sessionId, notification) => {
|
|
384
|
+
? new CodexSessionRuntime(options.launchAppServer ?? launchCodexAppServer, undefined, (sessionId, notification, origin) => {
|
|
383
385
|
const occurredAt = new Date().toISOString();
|
|
384
386
|
for (const activityFact of decodeAgentActivityFacts(sessionId, occurredAt, notification))
|
|
385
387
|
activity.observe(activityFact);
|
|
@@ -395,7 +397,18 @@ export async function composeRelayApp(options) {
|
|
|
395
397
|
}
|
|
396
398
|
}
|
|
397
399
|
const currentSession = sessions.find(sessionId);
|
|
398
|
-
const
|
|
400
|
+
const completionTurnId = notification.method === 'turn/completed' &&
|
|
401
|
+
notification.params &&
|
|
402
|
+
typeof notification.params === 'object' &&
|
|
403
|
+
typeof notification.params.turn?.id === 'string'
|
|
404
|
+
? notification.params.turn.id
|
|
405
|
+
: undefined;
|
|
406
|
+
// A completion with no thread identity is only root-owned when it exactly
|
|
407
|
+
// matches the durable active root turn; unknown child turns never fall back.
|
|
408
|
+
const resolvedOrigin = origin.kind === 'unknown' && completionTurnId === currentSession?.activeTurnId
|
|
409
|
+
? { ...origin, kind: 'root' }
|
|
410
|
+
: origin;
|
|
411
|
+
const normalized = normalizeCodexNotification(sessionId, 0, occurredAt, notification, currentSession?.workspacePath, currentSession?.activeTurnId, resolvedOrigin);
|
|
399
412
|
if (!normalized)
|
|
400
413
|
return;
|
|
401
414
|
let completedSession;
|
|
@@ -412,11 +425,35 @@ export async function composeRelayApp(options) {
|
|
|
412
425
|
events.publish(journal.append(sessionId, normalized.type, normalized.payload, normalized.occurredAt));
|
|
413
426
|
if (completedSession)
|
|
414
427
|
events.publish(journal.append(sessionId, 'session.updated', completedSession, occurredAt));
|
|
415
|
-
}, (sessionId, request) => {
|
|
428
|
+
}, (sessionId, request, origin) => {
|
|
416
429
|
const rawInteraction = toPendingInteraction(request);
|
|
417
430
|
const session = withPendingInteractions(sessions.find(sessionId));
|
|
418
431
|
if (!rawInteraction || !session)
|
|
419
432
|
return false;
|
|
433
|
+
if (rawInteraction.kind === 'orgPlanCheckpoint') {
|
|
434
|
+
const checkpoint = parseOrgPlanCheckpoint(rawInteraction.payload);
|
|
435
|
+
const retained = supervisedPlans.find(sessionId);
|
|
436
|
+
const identity = supervisedPlans.identity(sessionId);
|
|
437
|
+
const rootOwned = origin.kind === 'root' && origin.physicalTurnId === session.activeTurnId;
|
|
438
|
+
if (!checkpoint ||
|
|
439
|
+
!retained ||
|
|
440
|
+
!identity ||
|
|
441
|
+
checkpoint.planIdentity !== identity ||
|
|
442
|
+
!rootOwned)
|
|
443
|
+
return false;
|
|
444
|
+
const valid = validOrgPlanCheckpoint({
|
|
445
|
+
checkpoint,
|
|
446
|
+
plan: retained,
|
|
447
|
+
planIdentity: identity,
|
|
448
|
+
rootOwned,
|
|
449
|
+
hasActiveL1Writer: (position) => hasActiveL1Writer(activity.snapshot(sessionId, new Date().toISOString()), position),
|
|
450
|
+
});
|
|
451
|
+
if (!valid)
|
|
452
|
+
return false;
|
|
453
|
+
if (!autopilot.checkpointAccepted(sessionId, checkpoint, session.activeTurnId, new Date().toISOString()))
|
|
454
|
+
return false;
|
|
455
|
+
return (runtime?.resolveServerRequest(sessionId, rawInteraction.requestId, toOrgPlanCheckpointToolResponse()) === true);
|
|
456
|
+
}
|
|
420
457
|
const interaction = {
|
|
421
458
|
...rawInteraction,
|
|
422
459
|
turnId: session.activeTurnId,
|
|
@@ -959,3 +996,7 @@ function resolveStateDatabasePath(root, stateHome) {
|
|
|
959
996
|
const legacyPath = legacyRelayStatePath(root, stateHome);
|
|
960
997
|
return existsSync(legacyPath) ? legacyPath : currentPath;
|
|
961
998
|
}
|
|
999
|
+
function hasActiveL1Writer(snapshot, position) {
|
|
1000
|
+
return snapshot.subagents.some((child) => child.canonicalPosition === position &&
|
|
1001
|
+
['working', 'awaitingAgent', 'awaitingHuman'].includes(child.state));
|
|
1002
|
+
}
|
|
@@ -59,8 +59,17 @@ export function decideAutopilot(input) {
|
|
|
59
59
|
return { kind: 'disable', reason: 'manualDisabled' };
|
|
60
60
|
if (!plan)
|
|
61
61
|
return { kind: 'disable', reason: 'planRequired' };
|
|
62
|
-
if (executionComplete(plan))
|
|
62
|
+
if (executionComplete(plan)) {
|
|
63
|
+
// Checkpoint-aware sessions deliberately reserve a later root turn for
|
|
64
|
+
// terminal whole-branch review. The Org projection remains authoritative:
|
|
65
|
+
// this only controls whether a completed L1 may end the current report turn.
|
|
66
|
+
if (state.checkpoints?.protocolVersion === 1 && !state.checkpoints.terminalReviewAccepted)
|
|
67
|
+
return {
|
|
68
|
+
kind: 'scheduleContinuation',
|
|
69
|
+
at: new Date(Date.parse(now) + policy.backoffMs(state.consecutiveNoProgress)).toISOString(),
|
|
70
|
+
};
|
|
63
71
|
return { kind: 'complete' };
|
|
72
|
+
}
|
|
64
73
|
// Quiz, approval, and other held requests are ordinary session work. Only a
|
|
65
74
|
// validated Org attention record may turn an incomplete plan into a human stop.
|
|
66
75
|
if (hasPendingInteraction)
|
|
@@ -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 { createHash } from 'node:crypto';
|
|
6
7
|
import { autopilotSnapshot, disabledAutopilot, } from '../domain/autopilot-session.js';
|
|
7
8
|
import { classifyExecutorOutcome, decideSupervisedLifecycle, executorIdentity, validStructuredBlock, } from '../domain/supervised-lifecycle.js';
|
|
8
9
|
import { classifyAgentActivity, decideAutopilot, executionComplete, } from './policy.js';
|
|
@@ -294,11 +295,31 @@ export class AutopilotCoordinator {
|
|
|
294
295
|
state?.stopReason === 'planRemoved' ||
|
|
295
296
|
state?.stopReason === 'planReplaced' ||
|
|
296
297
|
state?.stopReason === 'sessionEnded';
|
|
298
|
+
const checkpointBoundary = Boolean(state?.checkpoints?.pendingTurnId &&
|
|
299
|
+
state.checkpoints.planIdentity === this.deps.plan(sessionId)?.identity);
|
|
297
300
|
const finalAllowed = !state ||
|
|
298
301
|
!plan ||
|
|
299
302
|
explicitlyStopped ||
|
|
300
|
-
|
|
303
|
+
checkpointBoundary ||
|
|
304
|
+
(!state.checkpoints && executionComplete(plan)) ||
|
|
301
305
|
validStructuredBlock(state.blocking);
|
|
306
|
+
if (checkpointBoundary && state?.checkpoints) {
|
|
307
|
+
const occurredAt = this.deps.now();
|
|
308
|
+
this.persist({
|
|
309
|
+
...state,
|
|
310
|
+
checkpoints: { ...state.checkpoints, pendingTurnId: null },
|
|
311
|
+
updatedAt: occurredAt,
|
|
312
|
+
}, undefined, [
|
|
313
|
+
{
|
|
314
|
+
sessionId,
|
|
315
|
+
type: state.checkpoints.terminalReviewAccepted
|
|
316
|
+
? 'org-plan.terminal-review-reported'
|
|
317
|
+
: 'org-plan.milestone-reported',
|
|
318
|
+
payload: { turnId: state.checkpoints.pendingTurnId },
|
|
319
|
+
occurredAt,
|
|
320
|
+
},
|
|
321
|
+
]);
|
|
322
|
+
}
|
|
302
323
|
if (!finalAllowed) {
|
|
303
324
|
const occurredAt = this.deps.now();
|
|
304
325
|
this.commit({
|
|
@@ -316,6 +337,46 @@ export class AutopilotCoordinator {
|
|
|
316
337
|
this.activitySettled(sessionId, 'rootFinalAttempt');
|
|
317
338
|
return finalAllowed;
|
|
318
339
|
}
|
|
340
|
+
/** Records an already validated root-only checkpoint; it never changes Org state. */
|
|
341
|
+
checkpointAccepted(sessionId, checkpoint, turnId, occurredAt) {
|
|
342
|
+
const prior = this.deps.store.find(sessionId);
|
|
343
|
+
const retained = this.deps.plan(sessionId);
|
|
344
|
+
if (!prior || !retained || !turnId || checkpoint.planIdentity !== retained.identity)
|
|
345
|
+
return false;
|
|
346
|
+
const previous = prior.checkpoints;
|
|
347
|
+
if (previous && previous.planIdentity !== retained.identity)
|
|
348
|
+
return false;
|
|
349
|
+
const reported = previous?.reportedL1Ids ?? [];
|
|
350
|
+
const key = createHash('sha256').update(JSON.stringify(checkpoint)).digest('hex');
|
|
351
|
+
const acceptedKeys = previous?.acceptedKeys ?? [];
|
|
352
|
+
if (acceptedKeys.includes(key))
|
|
353
|
+
return true;
|
|
354
|
+
if (checkpoint.kind === 'l1Accepted' && reported.includes(checkpoint.l1Id))
|
|
355
|
+
return false;
|
|
356
|
+
if (checkpoint.kind === 'terminalReviewAccepted' && previous?.terminalReviewAccepted)
|
|
357
|
+
return false;
|
|
358
|
+
const checkpoints = {
|
|
359
|
+
protocolVersion: 1,
|
|
360
|
+
planIdentity: retained.identity,
|
|
361
|
+
reportedL1Ids: checkpoint.kind === 'l1Accepted' ? [...reported, checkpoint.l1Id] : reported,
|
|
362
|
+
acceptedKeys: [...acceptedKeys, key],
|
|
363
|
+
pendingTurnId: turnId,
|
|
364
|
+
terminalReviewAccepted: checkpoint.kind === 'terminalReviewAccepted' || previous?.terminalReviewAccepted === true,
|
|
365
|
+
};
|
|
366
|
+
this.persist({ ...prior, checkpoints, updatedAt: occurredAt }, undefined, [
|
|
367
|
+
{
|
|
368
|
+
sessionId,
|
|
369
|
+
type: checkpoint.kind === 'l1Accepted'
|
|
370
|
+
? 'org-plan.milestone-checkpointed'
|
|
371
|
+
: 'org-plan.terminal-review-checkpointed',
|
|
372
|
+
payload: checkpoint.kind === 'l1Accepted'
|
|
373
|
+
? { l1Id: checkpoint.l1Id, position: checkpoint.position, turnId }
|
|
374
|
+
: { turnId },
|
|
375
|
+
occurredAt,
|
|
376
|
+
},
|
|
377
|
+
]);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
319
380
|
/** Handles only plan lifecycle safety; ordinary plan mutations are ignored. */
|
|
320
381
|
planStatusChanged(sessionId) {
|
|
321
382
|
const prior = this.deps.store.find(sessionId);
|
|
@@ -24,8 +24,12 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
24
24
|
if (blockingValue && !blocking)
|
|
25
25
|
return undefined;
|
|
26
26
|
const executorValue = record(root.executor);
|
|
27
|
+
const checkpointsValue = record(root.checkpoints);
|
|
28
|
+
const checkpoints = checkpointsValue ? parseCheckpoints(checkpointsValue) : undefined;
|
|
29
|
+
if (checkpointsValue && !checkpoints)
|
|
30
|
+
return undefined;
|
|
27
31
|
if (!executorValue)
|
|
28
|
-
return blocking ? { blocking } : {};
|
|
32
|
+
return { ...(blocking ? { blocking } : {}), ...(checkpoints ? { checkpoints } : {}) };
|
|
29
33
|
const processesValue = executorValue.ownedProcesses;
|
|
30
34
|
if (!Array.isArray(processesValue) || processesValue.length > 64)
|
|
31
35
|
return undefined;
|
|
@@ -131,6 +135,41 @@ export function parsePersistedSupervisedLifecycle(value) {
|
|
|
131
135
|
continuationCount,
|
|
132
136
|
},
|
|
133
137
|
...(blocking ? { blocking } : {}),
|
|
138
|
+
...(checkpoints ? { checkpoints } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function parseCheckpoints(value) {
|
|
142
|
+
const planIdentity = boundedText(value.planIdentity);
|
|
143
|
+
if (value.protocolVersion !== 1 || !planIdentity)
|
|
144
|
+
return undefined;
|
|
145
|
+
if (!Array.isArray(value.reportedL1Ids) || value.reportedL1Ids.length > 128)
|
|
146
|
+
return undefined;
|
|
147
|
+
const reportedL1Ids = value.reportedL1Ids
|
|
148
|
+
.map((id) => boundedText(id))
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
if (reportedL1Ids.length !== value.reportedL1Ids.length ||
|
|
151
|
+
new Set(reportedL1Ids).size !== reportedL1Ids.length)
|
|
152
|
+
return undefined;
|
|
153
|
+
if (!Array.isArray(value.acceptedKeys) || value.acceptedKeys.length > 128)
|
|
154
|
+
return undefined;
|
|
155
|
+
const acceptedKeys = value.acceptedKeys
|
|
156
|
+
.map((key) => boundedText(key))
|
|
157
|
+
.filter(Boolean);
|
|
158
|
+
if (acceptedKeys.length !== value.acceptedKeys.length ||
|
|
159
|
+
new Set(acceptedKeys).size !== acceptedKeys.length)
|
|
160
|
+
return undefined;
|
|
161
|
+
const pendingTurnId = value.pendingTurnId === null ? null : boundedText(value.pendingTurnId);
|
|
162
|
+
if (pendingTurnId === undefined)
|
|
163
|
+
return undefined;
|
|
164
|
+
if (typeof value.terminalReviewAccepted !== 'boolean')
|
|
165
|
+
return undefined;
|
|
166
|
+
return {
|
|
167
|
+
protocolVersion: 1,
|
|
168
|
+
planIdentity,
|
|
169
|
+
reportedL1Ids,
|
|
170
|
+
acceptedKeys,
|
|
171
|
+
pendingTurnId,
|
|
172
|
+
terminalReviewAccepted: value.terminalReviewAccepted,
|
|
134
173
|
};
|
|
135
174
|
}
|
|
136
175
|
export function classifyExecutorOutcome(input) {
|
|
@@ -0,0 +1,20 @@
|
|
|
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 validOrgPlanCheckpoint(input) {
|
|
7
|
+
const { checkpoint, plan, planIdentity, rootOwned } = input;
|
|
8
|
+
if (!rootOwned || checkpoint.planIdentity !== planIdentity)
|
|
9
|
+
return false;
|
|
10
|
+
if (checkpoint.kind === 'terminalReviewAccepted')
|
|
11
|
+
return plan.steps.every((step) => step.state === 'DONE' && step.reviewStatus === 'REVIEWED');
|
|
12
|
+
const l1 = plan.steps.find((step) => step.id === checkpoint.l1Id);
|
|
13
|
+
if (!l1)
|
|
14
|
+
return false;
|
|
15
|
+
const position = `L${plan.steps.indexOf(l1) + 1}`;
|
|
16
|
+
return (checkpoint.position === position &&
|
|
17
|
+
l1.state === 'DONE' &&
|
|
18
|
+
l1.reviewStatus === 'REVIEWED' &&
|
|
19
|
+
!input.hasActiveL1Writer(position));
|
|
20
|
+
}
|
|
@@ -83,6 +83,7 @@ function toActivityHistory(events, throughSequence) {
|
|
|
83
83
|
label: payload.label,
|
|
84
84
|
detail: payload.detail,
|
|
85
85
|
...(typeof payload.turnId === 'string' ? { turnId: payload.turnId } : {}),
|
|
86
|
+
...(typeof payload.actorTurnId === 'string' ? { actorTurnId: payload.actorTurnId } : {}),
|
|
86
87
|
...(Number.isFinite(occurredAt) ? { occurredAt } : {}),
|
|
87
88
|
...(changes.length ? { changes } : {}),
|
|
88
89
|
});
|
|
@@ -11,7 +11,7 @@ export function isValidInteractionResponse(kind, value) {
|
|
|
11
11
|
return false;
|
|
12
12
|
if (kind === 'userInput')
|
|
13
13
|
return isValidUserInputResponse(value);
|
|
14
|
-
if (kind === 'quiz' || kind === 'orgPlanAttention')
|
|
14
|
+
if (kind === 'quiz' || kind === 'orgPlanAttention' || kind === 'orgPlanCheckpoint')
|
|
15
15
|
return false;
|
|
16
16
|
if (kind === 'permissionsApproval')
|
|
17
17
|
return isRecord(value.permissions) && (value.scope === 'turn' || value.scope === 'session');
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { countDiffLines } from '../../../shared/contracts/file-change.js';
|
|
7
7
|
import { relative } from 'node:path';
|
|
8
|
-
export function normalizeCodexNotification(sessionId, sequence, occurredAt, notification, workspacePath, activeTurnId) {
|
|
8
|
+
export function normalizeCodexNotification(sessionId, sequence, occurredAt, notification, workspacePath, activeTurnId, origin = { kind: 'root' }) {
|
|
9
9
|
const decoded = decodeNotification(notification);
|
|
10
10
|
if (!decoded)
|
|
11
11
|
return null;
|
|
12
|
-
if (decoded.method === 'item/agentMessage/delta')
|
|
12
|
+
if (decoded.method === 'item/agentMessage/delta') {
|
|
13
|
+
if (origin.kind !== 'root')
|
|
14
|
+
return null;
|
|
13
15
|
return {
|
|
14
16
|
sessionId,
|
|
15
17
|
sequence,
|
|
@@ -18,12 +20,11 @@ export function normalizeCodexNotification(sessionId, sequence, occurredAt, noti
|
|
|
18
20
|
payload: {
|
|
19
21
|
text: decoded.params.delta,
|
|
20
22
|
...(decoded.params.itemId ? { itemId: decoded.params.itemId } : {}),
|
|
21
|
-
...(
|
|
22
|
-
? { turnId: decoded.params.turnId ?? activeTurnId }
|
|
23
|
-
: {}),
|
|
23
|
+
...(activeTurnId ? { turnId: activeTurnId } : {}),
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
}
|
|
27
|
+
if (decoded.method === 'turn/completed' && origin.kind === 'root')
|
|
27
28
|
return {
|
|
28
29
|
sessionId,
|
|
29
30
|
sequence,
|
|
@@ -37,13 +38,15 @@ export function normalizeCodexNotification(sessionId, sequence, occurredAt, noti
|
|
|
37
38
|
type: 'fileChange',
|
|
38
39
|
status: 'in_progress',
|
|
39
40
|
changes: decoded.params.changes,
|
|
40
|
-
}, workspacePath, activeTurnId, decoded.params.turnId);
|
|
41
|
+
}, workspacePath, activeTurnId, decoded.params.turnId, origin);
|
|
41
42
|
return activity
|
|
42
43
|
? { sessionId, sequence, occurredAt, type: 'activity.updated', payload: activity }
|
|
43
44
|
: null;
|
|
44
45
|
}
|
|
45
46
|
if (decoded.method === 'item/started' || decoded.method === 'item/completed') {
|
|
46
|
-
const message =
|
|
47
|
+
const message = origin.kind === 'root'
|
|
48
|
+
? safeAgentMessage(decoded.params.item, activeTurnId, decoded.params.turnId)
|
|
49
|
+
: null;
|
|
47
50
|
if (message)
|
|
48
51
|
return {
|
|
49
52
|
sessionId,
|
|
@@ -52,7 +55,7 @@ export function normalizeCodexNotification(sessionId, sequence, occurredAt, noti
|
|
|
52
55
|
type: decoded.method === 'item/started' ? 'agentMessageStarted' : 'agentMessageCompleted',
|
|
53
56
|
payload: message,
|
|
54
57
|
};
|
|
55
|
-
const activity = safeActivity(decoded.params.item, workspacePath, activeTurnId, decoded.params.turnId);
|
|
58
|
+
const activity = safeActivity(decoded.params.item, workspacePath, activeTurnId, decoded.params.turnId, origin);
|
|
56
59
|
if (activity)
|
|
57
60
|
return { sessionId, sequence, occurredAt, type: 'activity.updated', payload: activity };
|
|
58
61
|
}
|
|
@@ -120,19 +123,21 @@ export function decodeNotification(input) {
|
|
|
120
123
|
}
|
|
121
124
|
return null;
|
|
122
125
|
}
|
|
123
|
-
function safeActivity(item, workspacePath, activeTurnId, notificationTurnId) {
|
|
126
|
+
function safeActivity(item, workspacePath, activeTurnId, notificationTurnId, origin = { kind: 'root' }) {
|
|
124
127
|
if (!item || typeof item !== 'object')
|
|
125
128
|
return null;
|
|
126
129
|
const value = item;
|
|
127
130
|
if (typeof value.id !== 'string' || typeof value.type !== 'string')
|
|
128
131
|
return null;
|
|
129
|
-
const owner =
|
|
130
|
-
? { turnId:
|
|
131
|
-
:
|
|
132
|
-
? { turnId:
|
|
133
|
-
:
|
|
134
|
-
? { turnId:
|
|
135
|
-
:
|
|
132
|
+
const owner = origin.kind === 'child' && activeTurnId
|
|
133
|
+
? { turnId: activeTurnId, actorTurnId: origin.physicalTurnId }
|
|
134
|
+
: notificationTurnId && origin.kind === 'root'
|
|
135
|
+
? { turnId: notificationTurnId }
|
|
136
|
+
: safeId(value.turnId)
|
|
137
|
+
? { turnId: value.turnId }
|
|
138
|
+
: activeTurnId && origin.kind === 'root'
|
|
139
|
+
? { turnId: activeTurnId }
|
|
140
|
+
: {};
|
|
136
141
|
const status = typeof value.status === 'string' ? ` · ${value.status}` : '';
|
|
137
142
|
if (value.type === 'commandExecution' && typeof value.command === 'string')
|
|
138
143
|
return { id: value.id, label: `Command${status}`, detail: value.command, ...owner };
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { GESTALT_QUIZ_TOOL_NAME, parseQuiz } from '../../../shared/contracts/quiz.js';
|
|
7
7
|
import { GESTALT_ORG_PLAN_ATTENTION_TOOL_NAME, parseOrgPlanAttention, } from '../../../shared/contracts/org-plan-attention.js';
|
|
8
|
+
import { GESTALT_ORG_PLAN_CHECKPOINT_TOOL_NAME, parseOrgPlanCheckpoint, } from '../../../shared/contracts/org-plan-checkpoint.js';
|
|
8
9
|
export function toPendingInteraction(input) {
|
|
9
10
|
if (!Number.isSafeInteger(input.id) || !isRecord(input.params))
|
|
10
11
|
return null;
|
|
@@ -19,6 +20,12 @@ export function toPendingInteraction(input) {
|
|
|
19
20
|
? { requestId: String(input.id), kind: 'orgPlanAttention', payload: attention }
|
|
20
21
|
: null;
|
|
21
22
|
}
|
|
23
|
+
if (input.params.tool === GESTALT_ORG_PLAN_CHECKPOINT_TOOL_NAME) {
|
|
24
|
+
const checkpoint = parseOrgPlanCheckpoint(input.params.arguments);
|
|
25
|
+
return checkpoint
|
|
26
|
+
? { requestId: String(input.id), kind: 'orgPlanCheckpoint', payload: checkpoint }
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
22
29
|
return null;
|
|
23
30
|
}
|
|
24
31
|
const kind = {
|
|
@@ -9,6 +9,7 @@ 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 { gestaltOrgPlanAttentionDynamicTool } from '../../../shared/contracts/org-plan-attention.js';
|
|
12
|
+
import { gestaltOrgPlanCheckpointDynamicTool } from '../../../shared/contracts/org-plan-checkpoint.js';
|
|
12
13
|
import { countDiffLines } from '../../../shared/contracts/file-change.js';
|
|
13
14
|
import { threadPlanName } from './thread-plan-name.js';
|
|
14
15
|
import { WriterAcquisitionError, } from '../../features/sessions/application/writer-acquisition.js';
|
|
@@ -32,6 +33,8 @@ class SessionResource {
|
|
|
32
33
|
spawnedAgentModels = new Map();
|
|
33
34
|
attemptedAgentModelRecovery = new Set();
|
|
34
35
|
ownedChildProcesses = new Map();
|
|
36
|
+
turnThreads = new Map();
|
|
37
|
+
childThreads = new Set();
|
|
35
38
|
constructor(sessionId, process, planStatusLease, planMeasurementToken, unregister, onDisposed) {
|
|
36
39
|
this.sessionId = sessionId;
|
|
37
40
|
this.process = process;
|
|
@@ -561,7 +564,11 @@ export class CodexSessionRuntime {
|
|
|
561
564
|
? { approvalPolicy: session.executionPolicy.approvalPolicy }
|
|
562
565
|
: {}),
|
|
563
566
|
...(session.executionPolicy?.sandbox ? { sandbox: session.executionPolicy.sandbox } : {}),
|
|
564
|
-
dynamicTools: [
|
|
567
|
+
dynamicTools: [
|
|
568
|
+
gestaltQuizDynamicTool,
|
|
569
|
+
gestaltOrgPlanAttentionDynamicTool,
|
|
570
|
+
gestaltOrgPlanCheckpointDynamicTool,
|
|
571
|
+
],
|
|
565
572
|
});
|
|
566
573
|
result = {
|
|
567
574
|
session: RelaySession.rehydrate(session).restore(now).supportsAttentionTool(now).snapshot,
|
|
@@ -629,7 +636,8 @@ export class CodexSessionRuntime {
|
|
|
629
636
|
resource.pendingRequests.set(requestId, { resolve, reject });
|
|
630
637
|
let accepted = false;
|
|
631
638
|
try {
|
|
632
|
-
accepted =
|
|
639
|
+
accepted =
|
|
640
|
+
this.onServerRequest?.(resource.sessionId, request, this.resolveNotificationOrigin(resource, request)) === true;
|
|
633
641
|
}
|
|
634
642
|
catch {
|
|
635
643
|
// Publication failures are protocol failures, not permission to retain
|
|
@@ -646,7 +654,11 @@ export class CodexSessionRuntime {
|
|
|
646
654
|
return {
|
|
647
655
|
cwd: session.workspacePath,
|
|
648
656
|
approvalPolicy: session.executionPolicy?.approvalPolicy ?? 'on-request',
|
|
649
|
-
dynamicTools: [
|
|
657
|
+
dynamicTools: [
|
|
658
|
+
gestaltQuizDynamicTool,
|
|
659
|
+
gestaltOrgPlanAttentionDynamicTool,
|
|
660
|
+
gestaltOrgPlanCheckpointDynamicTool,
|
|
661
|
+
],
|
|
650
662
|
...(session.model ? { model: session.model } : {}),
|
|
651
663
|
...(session.executionPolicy?.sandbox ? { sandbox: session.executionPolicy.sandbox } : {}),
|
|
652
664
|
};
|
|
@@ -709,7 +721,7 @@ export class CodexSessionRuntime {
|
|
|
709
721
|
// The notification callback reconciles the durable interaction record.
|
|
710
722
|
pending?.reject(new Error('CODEX_SERVER_REQUEST_CLEARED'));
|
|
711
723
|
}
|
|
712
|
-
this.onNotification?.(session.id, notification);
|
|
724
|
+
this.onNotification?.(session.id, notification, this.resolveNotificationOrigin(resource, notification));
|
|
713
725
|
});
|
|
714
726
|
const requestUnsubscribe = process.rpc.onServerRequest((request) => this.holdServerRequest(resource, request));
|
|
715
727
|
if (!resource.active) {
|
|
@@ -726,6 +738,48 @@ export class CodexSessionRuntime {
|
|
|
726
738
|
throw error;
|
|
727
739
|
}
|
|
728
740
|
}
|
|
741
|
+
resolveNotificationOrigin(resource, notification) {
|
|
742
|
+
const params = asRecord(notification.params);
|
|
743
|
+
if (!params)
|
|
744
|
+
return { kind: 'unknown' };
|
|
745
|
+
const item = asRecord(params.item);
|
|
746
|
+
if (item?.type === 'collabAgentToolCall' || item?.type === 'collabToolCall') {
|
|
747
|
+
for (const value of [item.receiverThreadId, item.newThreadId])
|
|
748
|
+
if (boundedString(value, 256) && resource.childThreads.size < 64)
|
|
749
|
+
resource.childThreads.add(boundedString(value, 256));
|
|
750
|
+
if (Array.isArray(item.receiverThreadIds))
|
|
751
|
+
for (const value of item.receiverThreadIds)
|
|
752
|
+
if (boundedString(value, 256) && resource.childThreads.size < 64)
|
|
753
|
+
resource.childThreads.add(boundedString(value, 256));
|
|
754
|
+
const states = asRecord(item.agentsStates);
|
|
755
|
+
if (states)
|
|
756
|
+
for (const childId of Object.keys(states))
|
|
757
|
+
if (boundedString(childId, 256) && resource.childThreads.size < 64)
|
|
758
|
+
resource.childThreads.add(childId);
|
|
759
|
+
}
|
|
760
|
+
const turn = asRecord(params.turn);
|
|
761
|
+
const turnId = boundedString(params.turnId, 256) ??
|
|
762
|
+
boundedString(turn?.id, 256) ??
|
|
763
|
+
boundedString(item?.turnId, 256);
|
|
764
|
+
if (notification.method === 'turn/started' && turnId && boundedString(params.threadId, 256)) {
|
|
765
|
+
if (resource.turnThreads.has(turnId) || resource.turnThreads.size < 256)
|
|
766
|
+
resource.turnThreads.set(turnId, boundedString(params.threadId, 256));
|
|
767
|
+
}
|
|
768
|
+
const resolvedThreadId = boundedString(params.threadId, 256) ??
|
|
769
|
+
(turnId ? resource.turnThreads.get(turnId) : undefined);
|
|
770
|
+
const origin = {
|
|
771
|
+
kind: resolvedThreadId && resolvedThreadId === resource.threadId
|
|
772
|
+
? 'root'
|
|
773
|
+
: resolvedThreadId && resource.childThreads.has(resolvedThreadId)
|
|
774
|
+
? 'child'
|
|
775
|
+
: 'unknown',
|
|
776
|
+
...(turnId ? { physicalTurnId: turnId } : {}),
|
|
777
|
+
...(resolvedThreadId ? { physicalThreadId: resolvedThreadId } : {}),
|
|
778
|
+
};
|
|
779
|
+
if (notification.method === 'turn/completed' && turnId)
|
|
780
|
+
resource.turnThreads.delete(turnId);
|
|
781
|
+
return origin;
|
|
782
|
+
}
|
|
729
783
|
}
|
|
730
784
|
function isMethodNotFound(error) {
|
|
731
785
|
return Boolean(error && typeof error === 'object' && error.code === -32601);
|
|
@@ -57,8 +57,12 @@ export class SqliteAutopilotStore {
|
|
|
57
57
|
save(state) {
|
|
58
58
|
this.db
|
|
59
59
|
.prepare('INSERT INTO autopilot_sessions (session_id,state,requested_enabled,plan_identity,plan_fingerprint,generation,no_progress_count,next_evaluation_at,last_control_id,stop_reason,lifecycle_json,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT(session_id) DO UPDATE SET state=excluded.state,requested_enabled=excluded.requested_enabled,plan_identity=excluded.plan_identity,plan_fingerprint=excluded.plan_fingerprint,generation=excluded.generation,no_progress_count=excluded.no_progress_count,next_evaluation_at=excluded.next_evaluation_at,last_control_id=excluded.last_control_id,stop_reason=excluded.stop_reason,lifecycle_json=excluded.lifecycle_json,updated_at=excluded.updated_at')
|
|
60
|
-
.run(state.sessionId, state.state, state.requestedEnabled ? 1 : 0, state.planIdentity, state.planFingerprint, state.generation, state.consecutiveNoProgress, state.nextEvaluationAt, state.lastControlId, state.stopReason, state.executor || state.blocking
|
|
61
|
-
? JSON.stringify({
|
|
60
|
+
.run(state.sessionId, state.state, state.requestedEnabled ? 1 : 0, state.planIdentity, state.planFingerprint, state.generation, state.consecutiveNoProgress, state.nextEvaluationAt, state.lastControlId, state.stopReason, state.executor || state.blocking || state.checkpoints
|
|
61
|
+
? JSON.stringify({
|
|
62
|
+
executor: state.executor,
|
|
63
|
+
blocking: state.blocking,
|
|
64
|
+
checkpoints: state.checkpoints,
|
|
65
|
+
})
|
|
62
66
|
: null, state.updatedAt);
|
|
63
67
|
}
|
|
64
68
|
findControl(sessionId, controlId) {
|